diff --git a/.gitattributes b/.gitattributes index 1ab4d1b1..46d95c52 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,3 @@ # Append-only policy: these files use merge=union (append-only) plus CI enforcement (see docs/append-only-invariants.md). AGENTS.md merge=union -docs/decision-log.md merge=union TASKS-DAG.md merge=union -docs/execution-plan.md merge=union diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 74a2b685..339b03fc 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -68,15 +68,7 @@ case "${_auto_fmt}" in ;; esac -# 4) Docs guard: require docs updates on any Rust changes -STAGED=$(git diff --cached --name-only) -RUST_CHANGED=$(echo "$STAGED" | grep -E '\\.rs$' || true) -if [[ -n "$RUST_CHANGED" ]]; then - echo "$STAGED" | grep -Fx 'docs/execution-plan.md' >/dev/null || { echo 'pre-commit: docs/execution-plan.md must be updated when Rust files change.' >&2; exit 1; } - echo "$STAGED" | grep -Fx 'docs/decision-log.md' >/dev/null || { echo 'pre-commit: docs/decision-log.md must be updated when Rust files change.' >&2; exit 1; } -fi - -# 5) Task list guard: prevent contradictory duplicated checklist items +# 4) Task list guard: prevent contradictory duplicated checklist items if [[ -f scripts/check_task_lists.sh ]]; then if [[ ! -x scripts/check_task_lists.sh ]]; then echo "pre-commit: scripts/check_task_lists.sh exists but is not executable. Run: chmod +x scripts/check_task_lists.sh" >&2 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e4e485b0..4006f515 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,7 +14,6 @@ Links - Closes # / Refs # Checklist -- [ ] Docs Guard satisfied (updated docs/execution-plan.md and docs/decision-log.md when non-doc files changed) - [ ] CI green (fmt, clippy, tests, rustdoc, audit/deny) - [ ] Kept PR small and focused (one thing) diff --git a/.github/workflows/append-only-check.yml b/.github/workflows/append-only-check.yml deleted file mode 100644 index b08ddc79..00000000 --- a/.github/workflows/append-only-check.yml +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# © James Ross Ω FLYING•ROBOTS -name: Append-only Guard - -on: - pull_request: - workflow_dispatch: - -jobs: - append-only: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Enforce append-only policy - env: - APPEND_ONLY_BASE: origin/${{ github.base_ref || 'main' }} - run: node scripts/check-append-only.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f395c1ae..7cc6bdb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,38 +161,6 @@ jobs: - name: cargo test (warp-core, musl, det_fixed) run: cargo test -p warp-core --features det_fixed --target x86_64-unknown-linux-musl - docs: - name: Docs Guard - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Ensure execution plan and decision log were updated - run: | - set -euo pipefail - base_ref="${{ github.event.pull_request.base.ref }}" - git fetch origin "$base_ref" --depth=1 - changed=$(git diff --name-only "origin/${base_ref}"..."${{ github.sha }}") - if [ -z "$changed" ]; then - exit 0 - fi - non_doc=$(echo "$changed" | grep -vE '(^docs/)|(\.md$)' || true) - if [ -z "$non_doc" ]; then - exit 0 - fi - echo "Non-doc files changed:" - echo "$non_doc" - echo "$changed" | grep -Fx 'docs/execution-plan.md' >/dev/null || { - echo 'docs/execution-plan.md must be updated when non-doc files change.'; - exit 1; - } - echo "$changed" | grep -Fx 'docs/decision-log.md' >/dev/null || { - echo 'docs/decision-log.md must be updated when non-doc files change.'; - exit 1; - } - rust-version: name: Rust Version Guard runs-on: ubuntu-latest @@ -257,6 +225,59 @@ jobs: fi "$script" + determinism-guards: + name: Determinism Guards + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install ripgrep + run: sudo apt-get update && sudo apt-get install -y ripgrep + - name: Ban globals + shell: bash + run: | + set -euo pipefail + script="scripts/ban-globals.sh" + if [[ ! -x "$script" ]]; then + echo "Error: $script is missing or not executable" >&2 + ls -la scripts || true + exit 1 + fi + "$script" + - name: Ban nondeterminism + shell: bash + run: | + set -euo pipefail + script="scripts/ban-nondeterminism.sh" + if [[ ! -x "$script" ]]; then + echo "Error: $script is missing or not executable" >&2 + ls -la scripts || true + exit 1 + fi + "$script" + - name: Ban unordered ABI containers + shell: bash + run: | + set -euo pipefail + script="scripts/ban-unordered-abi.sh" + if [[ ! -x "$script" ]]; then + echo "Error: $script is missing or not executable" >&2 + ls -la scripts || true + exit 1 + fi + "$script" + + dind-pr: + name: DIND (PR set) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.90.0 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - name: Run DIND PR suite (full hash) + run: node scripts/dind-run-suite.mjs --tags pr + # MSRV job removed per policy: use @stable everywhere playwright: diff --git a/.gitignore b/.gitignore index 5896c7c7..db6a271f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* +dind-report.json # Env .env diff --git a/ADR-0003-Materialization-Bus.md b/ADR-0003-Materialization-Bus.md new file mode 100644 index 00000000..712a8386 --- /dev/null +++ b/ADR-0003-Materialization-Bus.md @@ -0,0 +1,183 @@ + + +# ADR-000X: Causality-First API — Ingress + MaterializationPort, No Direct Graph Writes + +- **Status:** Accepted +- **Date:** 2026-01-14 +- **Deciders:** James Ross Ω (and the increasingly judgmental kernel) +- **Context:** Echo / WARP deterministic runtime; web + tooling + inspector ecosystem; strict determinism and provenance requirements. + +--- + +## Context + +Echo/WARP is a deterministic system where: + +1. **WARP State** is a projection (a derived worldline snapshot). +2. **Causality** is the true API (the event/intent ledger that produces state). + +We want: + +- Generic web-based devtools/inspectors +- Local-in-browser runtime mode (WASM) +- Remote runtime mode (native process + WebSocket) +- High reliability at the boundary (retries, disconnects) +- **Zero non-deterministic mutation paths** +- Clean separation of concerns: + - **warp-core stays pure** + - boundary “weirdness” lives at ports/adapters + +We reject exposing raw engine primitives (tx/apply/insert) to tools. Tools should not mutate state “directly”. They should emit causal events. + +> Note: “causal” (not casual). The kernel is not wearing Crocs. + +--- + +## Decision + +### 1) All writes go through **Ingress** (the causal boundary) + +**Rule:** _Literally everything that touches the graph MUST go through the inbox/ingress._ + +- No public `apply(rule, scope)` +- No public `insert_node(...)` +- No public `tx_begin/commit/abort` +- No direct graph mutations from JS/TS or tools + +Instead, all writes are: + +- `ingest_intent(intent_bytes)` (canonical bytes only) +- runtime assigns canonical sequence numbers +- kernel applies rewrites during ticks as internal mechanics + +This makes the ingress/ledger the **API for causality**. + +--- + +### 2) Reads are bus-first via **MaterializationBus**, crossed by a **MaterializationPort** + +We distinguish: + +- **MaterializationBus (internal runtime)**: derived updates emitted during ticks +- **MaterializationPort (boundary API)**: subscriptions, batching, replay(1), transport + +For application UI, we prefer: +- “no direct reads” from WARP state (when feasible) +- UI driven by **bus materializations** (channels) + +Direct state reads exist only for inspectors and are capability-gated. + +--- + +### 3) Two transports share one protocol (bytes-only frames) + +The same binary protocol runs over: + +- **Local Mode:** WASM exports returning `Uint8Array` +- **Remote Mode:** WebSocket **binary** messages (same frames) + +No JSON protocol. No stringly-typed nonsense at the boundary. + +--- + +### 4) Resiliency (“Nine Tails”) lives at the Port via idempotent ingress + +Retries must not create duplicate causality. + +- Define `intent_id = H(intent_bytes)` (hash of canonical intent bytes) +- Ingress is **idempotent**: `intent_id -> seq_assigned` +- Retries return `DUPLICATE` + original seq (no new ledger entry) + +This provides “at-least-once delivery” with “exactly-once causality”. + +--- + +### 5) If/when needed, outbound side effects use a **Causal Outbox (Egress)** + +We may later introduce: + +- **EgressQueue / CausalOutbox**: outbound messages written into the graph during ticks +- Delivery agent sends messages (transport is at-least-once) +- **Acks are causal**: delivery success writes back via Ingress as an ack intent +- Idempotent key: `msg_id = H(message_bytes)` + +This preserves determinism while supporting real-world IO. + +--- + +## System Model + +### Core entities + +- **Ledger (Causality):** append-only input event stream +- **WARP State (Worldline):** deterministic projection of ledger +- **Ingress:** only way to add causal events +- **MaterializationBus:** ephemeral derived outputs from ticks +- **MaterializationPort:** subscription/bridge for bus delivery +- **InspectorPort:** optional direct state reads (gated) + +--- + +## API Surfaces + +### A) WarpIngress (write-only) + +- `ingest_intent(intent_bytes) -> ACKI | ERR!` +- `ingest_intents(batch_bytes) -> ACKI* | ERR!` (optional later) + +Notes: +- intent must be canonical bytes (e.g. `EINT` envelope v1) +- kernel assigns canonical `seq` +- idempotent by `intent_id = H(intent_bytes)` + +--- + +### B) MaterializationPort (bus-driven reads) + +The bus is: + +- **Unidirectional:** `tick -> emit -> subscribers` +- **Ephemeral:** you must be subscribed to see the stream +- **Stateless relative to ledger:** not a source of truth +- **Replay(1) per channel:** caches last materialized value for late joiners + +Port operations (conceptual): + +- `view_subscribe(channels, replay_last) -> sub_id` +- `view_replay_last(sub_id, max_bytes) -> VOPS` +- `view_drain(sub_id, max_bytes) -> VOPS` +- `view_unsubscribe(sub_id)` + +Channel identity: +- `channel_id = 32 bytes` (TypeId-derived; no strings in ABI) + +View ops are deterministic and coalesced (recommended): +- last-write-wins per channel per tick + +--- + +### C) InspectorPort (direct reads, gated) + +Used only for devtools/inspectors; not required for app UI: + +- `read_node(node_id) -> NODE | ERR!` +- `read_attachment(node_id) -> ATTM | ERR!` +- `read_edges_from(node_id, limit) -> EDGE | ERR!` +- `read_edges_to(node_id, limit) -> EDGE | ERR!` + +All enumerations must be canonical order (sorted). + +--- + +## Protocol: FrameV1 (bytes-only) + +All boundary messages use the same framing: + +```text +FrameV1: + magic[4] ASCII (e.g. 'VOPS', 'HEAD') + ver_u16 = 1 + kind_u16 (optional if magic is sufficient; reserved for future) + len_u32 payload byte length + payload[len] +(all integers little-endian) diff --git a/ADR-0004-No-Global-State.md b/ADR-0004-No-Global-State.md new file mode 100644 index 00000000..89d0aa2b --- /dev/null +++ b/ADR-0004-No-Global-State.md @@ -0,0 +1,176 @@ + + +# ADR-000Y: No Global State in Echo — Dependency Injection Only + +- **Status:** Accepted +- **Date:** 2026-01-14 + +## Context + +Global mutable state undermines determinism, testability, and provenance. It creates hidden initialization dependencies (“did `install_*` run?”), makes behavior environment-dependent, and complicates WASM vs native parity. Rust patterns like `OnceLock`/`LazyLock` are safe but still encode hidden global wiring. + +Echo’s architecture benefits from explicit dependency graphs: runtime/kernel as values, ports as components, and deterministic construction. + +## Decision + +### 1) Global state is banned + +Forbidden in Echo/WARP runtime and core crates: +- `OnceLock`, `LazyLock`, `lazy_static!`, `once_cell`, `thread_local!` +- `static mut` +- process-wide “install_*” patterns for runtime dependencies + +Allowed: +- `const` and immutable `static` data (tables, magic bytes, version tags) +- pure functions and types + +### 2) Dependencies are injected explicitly + +All runtime dependencies must be carried by structs and passed explicitly: + +- `EchoKernel { engine, registry, ingress, bus, … }` +- `MaterializationBus` lives inside the runtime +- Registry/codec providers are fields or type parameters (compile-time wiring preferred) + +### 3) Enforced by tooling + +We add a CI ban script that fails builds if forbidden patterns appear in protected crates. Exceptions require explicit allowlisting and justification. + +## Consequences + +- Determinism audits become simpler (no hidden wiring) +- Tests construct runtimes with explicit dependencies +- WASM and native implementations share the same dependency model +- Eliminates global init ordering bugs and accidental shared state across tools + +## Appendix: Ban Global State + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# Ban global state patterns in Echo/WARP core crates. +# Goal: no hidden wiring, no init-order dependency, no mutable process-wide state. +# +# Allowed: +# - const +# - immutable static data (e.g. magic bytes, lookup tables) +# +# Forbidden: +# - OnceLock/LazyLock/lazy_static/once_cell/thread_local/static mut +# - "install_*" global init patterns (heuristic) +# +# Usage: +# ./scripts/ban-globals.sh +# +# Optional env: +# BAN_GLOBALS_PATHS="crates/warp-core crates/warp-wasm crates/flyingrobots-echo-wasm" +# BAN_GLOBALS_ALLOWLIST=".ban-globals-allowlist" + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +PATHS_DEFAULT="crates/warp-core crates/warp-wasm crates/flyingrobots-echo-wasm crates/echo-wasm-abi" +PATHS="${BAN_GLOBALS_PATHS:-$PATHS_DEFAULT}" + +ALLOWLIST="${BAN_GLOBALS_ALLOWLIST:-.ban-globals-allowlist}" + +# ripgrep is fast and consistent +if ! command -v rg >/dev/null 2>&1; then + echo "ERROR: ripgrep (rg) is required." >&2 + exit 2 +fi + +# Patterns are conservative on purpose. +# If you truly need an exception, add an allowlist line with a justification. +PATTERNS=( + '\bOnceLock\b' + '\bLazyLock\b' + '\blazy_static!\b' + '\bonce_cell\b' + '\bthread_local!\b' + '\bstatic mut\b' + '\bunsafe\s*\{' # optional: uncomment if you want "no unsafe" too + '\binstall_[a-zA-Z0-9_]*\b' # heuristic: discourages "install_registry" style globals +) + +# You may want to allow `unsafe` in some crates; if so, delete the unsafe pattern above. + +echo "ban-globals: scanning paths:" +for p in $PATHS; do echo " - $p"; done +echo + +# Build rg args +RG_ARGS=(--hidden --no-ignore --glob '!.git/*' --glob '!target/*' --glob '!**/node_modules/*') + +# Apply allowlist as inverted matches (each line is a regex or fixed substring) +# Allowlist format: +# \t +# or: +# +ALLOW_RG_EXCLUDES=() +if [[ -f "$ALLOWLIST" ]]; then + # Read first column (pattern) per line, ignore comments + while IFS= read -r line; do + [[ -z "$line" ]] && continue + [[ "$line" =~ ^# ]] && continue + pat="${line%%$'\t'*}" + pat="${pat%% *}" # also allow space-separated + [[ -z "$pat" ]] && continue + # Exclude lines matching allowlisted pattern + ALLOW_RG_EXCLUDES+=(--glob "!$pat") + done < "$ALLOWLIST" +fi + +violations=0 + +for pat in "${PATTERNS[@]}"; do + echo "Checking: $pat" + # We can't "glob exclude by line"; allowlist is file-level. Keep it simple: + # If you need surgical exceptions, prefer moving code or refactoring. + if rg "${RG_ARGS[@]}" -n -S "$pat" $PATHS; then + echo + violations=$((violations+1)) + else + echo " OK" + fi + echo +done + +if [[ $violations -ne 0 ]]; then + echo "ban-globals: FAILED ($violations pattern group(s) matched)." + echo "Fix the code or (rarely) justify an exception in $ALLOWLIST." + exit 1 +fi + +echo "ban-globals: PASSED." +``` + +### Optional: Allow List File + +```text +# Keep this tiny. If it grows, you’re lying to yourself. + +# Example (file-level exclusion): +# crates/some-crate/src/vendor/* vendored code, cannot change +``` + +### CI Script + +```bash +./scripts/ban-globals.sh +``` + +## Appendix B: README Brag + +```markdown +## Determinism Doctrine: No Global State + +Echo forbids global mutable state. No init-once singletons, no hidden wiring, no process-wide “install_*” hooks. +All dependencies (registry, codecs, ports, buses) are injected explicitly via runtime structs. + +Why: global state breaks provenance, complicates replay, and creates “it depends how you booted it” bugs. + +Enforcement: `./scripts/ban-globals.sh` runs in CI and rejects forbidden patterns (`OnceLock`, `LazyLock`, `lazy_static`, `thread_local`, `static mut`, etc.). +See ADR-000Y: **No Global State**. +``` diff --git a/ADR-0005-Physics.md b/ADR-0005-Physics.md new file mode 100644 index 00000000..ab184f78 --- /dev/null +++ b/ADR-0005-Physics.md @@ -0,0 +1,124 @@ + + +# ADR-00ZZ: Physics as Deterministic Scheduled Rewrites (Footprints + Phases) + +- **Status:** Accepted +- **Date:** 2026-01-14 + +## Context + +Echo runs deterministic ticks over a WARP graph. Some subsystems (physics, constraints, layout) require multi-pass updates and must remain: + +- deterministic across platforms +- schedulable using independence/footprints (confluence-friendly) +- isolated from the public causality API (Inbox/Ingress remains sacred) + +Physics introduces: +- broadphase candidate generation +- narrowphase contact computation (possibly swept / TOI) +- iterative constraint resolution +- multi-body coupling (piles) that emerges from pairwise contacts + +We must avoid designs that serialize derived work into an “inbox-like” ordered stream, which destroys independence batching and confluence benefits. + +## Decision + +### 1) Physics runs as an internal tick phase, not as causal ingress + +Physics is a system phase executed during `step()`. It does not emit causal events into the ledger. +Causal inputs remain: ingested intents (and optionally a causal `dt` parameter). + +### 2) Contacts are modeled as rewrite candidates with explicit footprints + +Physics resolution is expressed as rewrite candidates operating over multiple bodies. + +- Each candidate represents a contact constraint between bodies A and B. +- Footprint writeset includes `{A, B}` (and any shared state they mutate). +- Candidates commute iff their footprints are disjoint. + +This maps directly onto Echo’s scheduler: deterministic conflict resolution and maximal independence batching. + +### 3) Candidate selection is set-based, not queue-based + +Broadphase produces a *set* of candidates. The scheduler: +- canonicalizes ordering +- selects a maximal independent subset (disjoint footprints) +- applies them in deterministic order +- repeats for a fixed number of solver iterations + +We do **not** re-inject derived candidates into a “micro-inbox” stream, because ordered queues erase concurrency benefits. + +### 4) Deterministic ordering rules are mandatory + +All physics candidate generation and application must be canonical: + +- Bodies enumerated in sorted `NodeId` order +- Candidate pair key: `(min(A,B), max(A,B))` +- If swept/TOI is used, include `toi_q` (quantized) in the sort key +- If a manifold produces multiple contacts, include a stable `feature_id` in the key + +Recommended canonical key: +`(toi_q, min_id, max_id, feature_id)` + +### 5) Two-pass / multi-pass is implemented as phases + bounded iterations + +Physics tick phase executes as: + +1. **Integrate (predict)** into working state (or `next`) deterministically. +2. **Generate contact candidates** deterministically (broadphase + narrowphase). +3. **Solve** using K fixed iterations: + - each iteration selects a maximal independent subset via footprints + - apply in canonical order +4. **Finalize (commit)**: swap/commit working state for the tick. + +Iteration budgets are fixed: +- `K_SOLVER_ITERS` (e.g., 4–10) +- Optional `MAX_CCD_STEPS` if swept collisions are enabled + +Optional early exit is allowed only if the quiescence check is deterministic (e.g., no writes occurred or hash unchanged). + +### 6) Swept collisions (CCD) are supported via earliest-TOI banding (optional) + +If CCD is enabled: + +- Compute TOI per candidate pair in `[0, dt]` +- Quantize TOI to an integer bucket `toi_q` +- Choose the minimum bucket `toi_q*` +- Collect all candidates with `toi_q <= toi_q* + ε_bucket` into the solve set +- Solve as above (K iters) +- Advance remaining time and repeat up to `MAX_CCD_STEPS` + +N-body “simultaneous collisions” are treated as connected components in the footprint graph (collision islands), not as N-ary collision events. + +### 7) MaterializationBus emits only post-phase, post-commit outputs + +The UI-facing MaterializationBus (channels) emits after physics stabilization for the tick. +No half-updated physics state is observable via materializations. + +### 8) Inspector visibility is via trace channels, not causal ledger + +Optional debug-only materializations may be emitted: +- `trace/physics/candidates` +- `trace/physics/selected` +- `trace/physics/islands` +- `trace/physics/iters` + +These are outputs, not inputs. + +## Consequences + +### Positive +- Physics reuses existing determinism machinery (scheduler + footprints) +- Preserves maximal independence batching (confluence-friendly) +- Multi-pass behavior is explicit, bounded, and reproducible +- Works in both wasm and native runtimes with the same semantics + +### Negative / Tradeoffs +- Requires careful canonical ordering in broadphase/narrowphase +- Requires fixed iteration budgets for convergence (predictable but not “perfect”) +- CCD adds complexity; may be deferred until needed + +## Notes + +Physics is not a public API. It is an internal deterministic phase driven by causal inputs. +Candidate sets + footprint scheduling preserve concurrency benefits; queueing derived work does not. diff --git a/ADR-0006-Ban-Non-Determinism.md b/ADR-0006-Ban-Non-Determinism.md new file mode 100644 index 00000000..c5c659c6 --- /dev/null +++ b/ADR-0006-Ban-Non-Determinism.md @@ -0,0 +1,221 @@ + + +## T2000 on 'em + +We already have the **ban-globals** drill sergeant. Now we add the rest of the “you will cry” suite: + +- **ban nondeterministic APIs** +- **ban unordered containers in ABI-ish structs** +- **ban time/rand/JSON** +- **fail CI hard**. + +Below is a clean, repo-friendly setup. + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# Determinism Drill Sergeant: ban nondeterministic APIs and patterns +# +# Usage: +# ./scripts/ban-nondeterminism.sh +# +# Optional env: +# DETERMINISM_PATHS="crates/warp-core crates/warp-wasm crates/flyingrobots-echo-wasm" +# DETERMINISM_ALLOWLIST=".ban-nondeterminism-allowlist" + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +if ! command -v rg >/dev/null 2>&1; then + echo "ERROR: ripgrep (rg) is required." >&2 + exit 2 +fi + +PATHS_DEFAULT="crates/warp-core crates/warp-wasm crates/flyingrobots-echo-wasm crates/echo-wasm-abi" +PATHS="${DETERMINISM_PATHS:-$PATHS_DEFAULT}" + +ALLOWLIST="${DETERMINISM_ALLOWLIST:-.ban-nondeterminism-allowlist}" + +RG_ARGS=(--hidden --no-ignore --glob '!.git/*' --glob '!target/*' --glob '!**/node_modules/*') + +# You can allow file-level exceptions via allowlist (keep it tiny). +ALLOW_GLOBS=() +if [[ -f "$ALLOWLIST" ]]; then + while IFS= read -r line; do + [[ -z "$line" ]] && continue + [[ "$line" =~ ^# ]] && continue + pat="${line%%$'\t'*}" + pat="${pat%% *}" + [[ -z "$pat" ]] && continue + ALLOW_GLOBS+=(--glob "!$pat") + done < "$ALLOWLIST" +fi + +# Patterns: conservative and intentionally annoying. +# If you hit a false positive, refactor; don't immediately allowlist. +PATTERNS=( + # Time / entropy (core determinism killers) + '\bstd::time::SystemTime\b' + '\bSystemTime::now\b' + '\bstd::time::Instant\b' + '\bInstant::now\b' + '\bstd::thread::sleep\b' + '\b(tokio|async_std)::time::sleep\b' + + # Randomness + '\brand::\b' + '\bgetrandom::\b' + '\bfastrand::\b' + + # Unordered containers that will betray you if they cross a boundary + '\bstd::collections::HashMap\b' + '\bstd::collections::HashSet\b' + '\bhashbrown::HashMap\b' + '\bhashbrown::HashSet\b' + + # JSON & “helpful” serialization in core paths + '\bserde_json::\b' + '\bserde_wasm_bindgen::\b' + + # Float nondeterminism hotspots (you can tune these) + '\b(f32|f64)::NAN\b' + '\b(f32|f64)::INFINITY\b' + '\b(f32|f64)::NEG_INFINITY\b' + '\.sin\(' + '\.cos\(' + '\.tan\(' + '\.sqrt\(' + '\.pow[f]?\(' + + # Host/environment variability + '\bstd::env::\b' + '\bstd::fs::\b' + '\bstd::process::\b' + + # Concurrency primitives (optional—uncomment if you want core to be single-thread-only) + # '\bstd::sync::Mutex\b' + # '\bstd::sync::RwLock\b' + # '\bstd::sync::atomic::\b' +) + +echo "ban-nondeterminism: scanning paths:" +for p in $PATHS; do echo " - $p"; done +echo + +violations=0 +for pat in "${PATTERNS[@]}"; do + echo "Checking: $pat" + if rg "${RG_ARGS[@]}" "${ALLOW_GLOBS[@]}" -n -S "$pat" $PATHS; then + echo + violations=$((violations+1)) + else + echo " OK" + fi + echo +done + +if [[ $violations -ne 0 ]]; then + echo "ban-nondeterminism: FAILED ($violations pattern group(s) matched)." + echo "Fix the code or (rarely) justify an exception in $ALLOWLIST." + exit 1 +fi + +echo "ban-nondeterminism: PASSED." +## +``` + +### Optional Allow-List + +```text +# Example: +# crates/some-crate/tests/* tests can use time/rand, not core +``` + +## ABI Police + +### `scripts/ban-unordered-abi.sh` + +This one is narrower: + +- **ban HashMap/HashSet inside anything that looks like ABI/codec/message structs**. + +It’s opinionated: if it’s named like it crosses a boundary, it must be ordered. + +```bash +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +if ! command -v rg >/dev/null 2>&1; then + echo "ERROR: ripgrep (rg) is required." >&2 + exit 2 +fi + +# Adjust these to your repo conventions +ABI_HINTS=( + "abi" + "codec" + "message" + "frame" + "packet" + "envelope" + "dto" + "wire" +) + +RG_ARGS=(--hidden --no-ignore --glob '!.git/*' --glob '!target/*' --glob '!**/node_modules/*') + +# Find Rust files likely involved in ABI/wire formats. +files=$(rg "${RG_ARGS[@]}" -l -g'*.rs' "$(printf '%s|' "${ABI_HINTS[@]}")" crates/ || true) + +if [[ -z "${files}" ]]; then + echo "ban-unordered-abi: no ABI-ish files found (by heuristic). OK." + exit 0 +fi + +echo "ban-unordered-abi: scanning ABI-ish Rust files..." +violations=0 + +# HashMap/HashSet are not allowed in ABI-ish types. Use Vec<(K,V)> sorted, BTreeMap, IndexMap with explicit canonicalization, etc. +if rg "${RG_ARGS[@]}" -n -S '\b(HashMap|HashSet)\b' $files; then + violations=$((violations+1)) +fi + +if [[ $violations -ne 0 ]]; then + echo "ban-unordered-abi: FAILED. Unordered containers found in ABI-ish code." + echo "Fix by using Vec pairs + sorting, or BTreeMap + explicit canonical encode ordering." + exit 1 +fi + +echo "ban-unordered-abi: PASSED." +``` + +## CI Wire It In + +```bash +./scripts/ban-globals.sh +./scripts/ban-nondeterminism.sh +./scripts/ban-unordered-abi.sh +cargo fmt --check +cargo clippy --all-targets --all-features -- -D warnings +cargo test --all-features +``` + +## README Brag + +```markdown +## Determinism Drill Sergeant (Non-Negotiable) + +Echo is a deterministic system. We enforce this with automated bans. + +- **No global state**: no `OnceLock`, `LazyLock`, `lazy_static`, `thread_local`, `static mut`, or `install_*` singletons. + - Enforced by: `./scripts/ban-globals.sh` +- **No nondeterminism in core**: no time, randomness, JSON convenience layers, unordered ABI containers, or host-environment dependencies in protected crates. + - Enforced by: `./scripts/ban-nondeterminism.sh` and `./scripts/ban-unordered-abi.sh` + +If your change trips these scripts, the fix is not “add an allowlist line.” +The fix is **refactor the design** so determinism is true by construction. +``` diff --git a/AGENTS.md b/AGENTS.md index f629521d..39fe4a2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,16 +6,14 @@ Welcome to the **Echo** project. This file captures expectations for any LLM age ## Core Principles - **Honor the Vision**: Echo is a deterministic, multiverse-aware ECS. Consult `docs/architecture-outline.md` before touching runtime code. -- **Document Ruthlessly**: Every meaningful design choice should land in `docs/` (specs, diagrams, memorials) or other durable repo artifacts (e.g. `docs/decision-log.md`). +- **Document Ruthlessly**: Every meaningful design choice should land in `docs/` (specs, diagrams, ADRs) or PR descriptions. - **Docstrings Aren't Optional**: Public APIs across crates (`warp-core`, `warp-ffi`, `warp-wasm`, etc.) must carry rustdoc comments that explain intent, invariants, and usage. Treat missing docs as a failing test. - **Determinism First**: Avoid introducing sources of nondeterminism without a mitigation plan. - **Temporal Mindset**: Think in timelines—branching, merging, entropy budgets. Feature work should map to Chronos/Kairos/Aion axes where appropriate. ## Timeline Logging -- Start each session by updating *Today’s Intent* in `docs/execution-plan.md`. -- Capture milestones, blockers, and decisions directly in this repo (e.g. `docs/decision-log.md`, relevant specs, or PR descriptions). -- When wrapping up, record outcomes and next steps in the Decision Log and ensure any impacted docs stay in sync. -- AGENTS.md, `docs/decision-log.md`, `TASKS-DAG.md`, and `docs/execution-plan.md` are append-only; see `docs/append-only-invariants.md` plus `scripts/check-append-only.js` for the enforcement plan that CI will run before merges. +- Capture milestones, blockers, and decisions in relevant specs, ADRs, or PR descriptions. +- AGENTS.md and `TASKS-DAG.md` are append-only; see `docs/append-only-invariants.md` plus `scripts/check-append-only.js` for the enforcement plan that CI will run before merges. ## Workflows & Automation - The contributor playbook lives in `docs/workflows.md` (policy + blessed commands + automation). @@ -42,14 +40,12 @@ Welcome to the **Echo** project. This file captures expectations for any LLM age - Use explicit closing keywords in the PR body: include a line like `Closes #` so the issue auto‑closes on merge. - Keep PRs single‑purpose: 1 PR = 1 thing. Avoid bundling unrelated changes. - Branch naming: prefer `echo/` or `timeline/` and include the issue number in the PR title. -- Docs Guard: when a PR touches non‑doc code, update `docs/execution-plan.md` and `docs/decision-log.md` in the same PR. -- Project hygiene: assign the PR’s linked issue to the correct Milestone and Board column (Blocked/Ready/Done) as part of the PR. +- Project hygiene: assign the PR's linked issue to the correct Milestone and Board column (Blocked/Ready/Done) as part of the PR. ### Git Hooks & Local CI - Install repo hooks once with `make hooks` (configures `core.hooksPath`). - Formatting: pre-commit auto-fixes with `cargo fmt` by default. Set `ECHO_AUTO_FMT=0` to run check-only instead. - Toolchain: pre-commit verifies your active toolchain matches `rust-toolchain.toml`. -- Docs Guard: when core API files change, the hook requires updating `docs/execution-plan.md` and `docs/decision-log.md` (mirrors the CI check). - SPDX header policy (source): every source file must start with exactly: - `// SPDX-License-Identifier: Apache-2.0` - `// © James Ross Ω FLYING•ROBOTS ` @@ -62,8 +58,4 @@ Welcome to the **Echo** project. This file captures expectations for any LLM age In short: no one cares about a tidy commit graph, but everyone cares if you rewrite commits on origin. -## Contact Threads -- Docs `decision-log.md`: Chronological design decisions. -- Docs `execution-plan.md`: Working map of tasks, intent, and progress. - -Safe travels in the multiverse. Logged timelines are happy timelines. 🌀 +Safe travels in the multiverse. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..96a2cb29 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ + + +# Changelog + +## Unreleased + +- Added `crates/echo-dind-harness` to the Echo workspace (moved from flyingrobots.dev). +- Added `crates/echo-dind-tests` as the stable DIND test app (no dependency on flyingrobots.dev). +- Moved DIND scenarios and generator scripts into `testdata/dind` and `scripts/`. +- Added convergence scopes in DIND manifest; `converge` now compares projected hashes. +- Documented convergence scope semantics and added a guarded `--scope` override. +- Wired determinism guard scripts and DIND PR suite into CI. +- Added spec for canonical inbox sequencing and deterministic scheduler tie-breaks. +- Added determinism guard scripts: `scripts/ban-globals.sh`, `scripts/ban-nondeterminism.sh`, and `scripts/ban-unordered-abi.sh`. +- Added `ECHO_ROADMAP.md` to capture phased plans aligned with recent ADRs. +- Removed legacy wasm encode helpers from `warp-wasm` (TS encoders are the protocol source of truth). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e8abf91..ed542d19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,12 +21,12 @@ Welcome, chrononaut! Echo thrives when timelines collaborate. Please read this g ## Project Philosophy Echo is a deterministic, renderer-agnostic engine. We prioritize: - **Determinism**: every change must preserve reproducible simulations. -- **Documentation**: specs and decision logs live alongside code. +- **Documentation**: specs live alongside code. - **Temporal Tooling**: features support branching timelines and merges. ## Getting Started 1. Clone the repo and run `cargo check` to ensure the Rust workspace builds. -2. Read `docs/architecture-outline.md` and `docs/execution-plan.md`. +2. Read `docs/architecture-outline.md`. 3. Review `AGENTS.md` for collaboration norms before touching runtime code. 4. Optional: develop inside the devcontainer for toolchain parity with CI. - Open in VS Code → "Reopen in Container" (requires the Dev Containers extension). @@ -36,7 +36,6 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize: ## Branching & Workflow - Keep `main` pristine. Create feature branches like `echo/` or `timeline/`. - Before starting work, ensure `git status` is clean. If not, resolve or coordinate with the human operator. -- Before each session, update the “Today’s Intent” section in `docs/execution-plan.md` so future collaborators can follow the timeline. - PR review loops are procedural: follow `docs/procedures/PR-SUBMISSION-REVIEW-LOOP.md` and use `docs/procedures/EXTRACT-PR-COMMENTS.md` to extract actionable CodeRabbitAI feedback per round. ## Testing Expectations @@ -51,8 +50,7 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize: ## Documentation & Telemetry - Update relevant docs in `docs/` whenever behavior or architecture changes. -- Record major decisions or deviations in the execution plan or decision log tables. -- Capture breadcrumbs for future Codex agents in `docs/decision-log.md` or related specs. +- Record major architectural decisions in ADRs (`docs/adr/`) or PR descriptions. ## Submitting Changes 1. Run `cargo fmt`, `cargo clippy`, and `cargo test`. @@ -74,7 +72,6 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize: - Pre-commit runs: - cargo fmt (auto-fix by default; set `ECHO_AUTO_FMT=0` for check-only) - Toolchain pin verification (matches `rust-toolchain.toml`) - - A minimal docs-guard: when core API files change, it requires updating `docs/execution-plan.md` and `docs/decision-log.md` (mirrors CI) - To auto-fix formatting on commit: `ECHO_AUTO_FMT=1 git commit -m "message"` #### Partial Staging & rustfmt @@ -91,10 +88,9 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize: - rustup toolchain install 1.90.0 - rustup override set 1.90.0 -- When any Rust code changes (.rs anywhere), update both `docs/execution-plan.md` and `docs/decision-log.md` with intent and a brief rationale. The hook enforces this. ## Communication -- Major updates should land in `docs/execution-plan.md` and `docs/decision-log.md`; rely on GitHub discussions or issues for longer-form proposals. +- Rely on GitHub discussions or issues for longer-form proposals. - Respect the temporal theme—leave the codebase cleaner for the next timeline traveler. Thanks for helping forge Echo’s spine. 🌀 diff --git a/Cargo.lock b/Cargo.lock index 53a02934..df7ff687 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -502,6 +502,9 @@ name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] [[package]] name = "calloop" @@ -1117,6 +1120,43 @@ dependencies = [ "echo-app-core", ] +[[package]] +name = "echo-dind-harness" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "echo-dind-tests", + "echo-wasm-abi", + "hex", + "serde", + "serde_json", + "warp-core", +] + +[[package]] +name = "echo-dind-tests" +version = "0.1.0" +dependencies = [ + "bytes", + "echo-dry-tests", + "echo-wasm-abi", + "warp-core", +] + +[[package]] +name = "echo-dry-tests" +version = "0.1.0" +dependencies = [ + "blake3", + "bytes", + "echo-app-core", + "echo-graph", + "serde", + "serde_json", + "warp-core", +] + [[package]] name = "echo-graph" version = "0.1.0" @@ -1127,6 +1167,10 @@ dependencies = [ "serde", ] +[[package]] +name = "echo-registry-api" +version = "0.1.0" + [[package]] name = "echo-session-client" version = "0.1.0" @@ -1186,8 +1230,13 @@ dependencies = [ name = "echo-wasm-abi" version = "0.1.0" dependencies = [ + "ciborium", + "half", + "proptest", "serde", + "serde-value", "serde_json", + "thiserror 1.0.69", ] [[package]] @@ -1196,10 +1245,25 @@ version = "0.1.0" dependencies = [ "echo-wasm-abi", "serde", + "serde-wasm-bindgen", "serde_json", "wasm-bindgen", ] +[[package]] +name = "echo-wesley-gen" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "prettyplease", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + [[package]] name = "ecolor" version = "0.32.3" @@ -2600,16 +2664,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minicov" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" -dependencies = [ - "cc", - "walkdir", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3941,6 +3995,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4942,6 +5007,7 @@ version = "0.1.0" dependencies = [ "blake3", "criterion", + "echo-dry-tests", "rand 0.8.5", "rustc-hash 2.1.1", "warp-core", @@ -4957,6 +5023,8 @@ version = "0.1.0" dependencies = [ "blake3", "bytes", + "ciborium", + "echo-dry-tests", "hex", "libm", "proptest", @@ -5016,10 +5084,13 @@ name = "warp-wasm" version = "0.1.0" dependencies = [ "console_error_panic_hook", + "echo-registry-api", + "echo-wasm-abi", "js-sys", - "warp-core", + "serde", + "serde-value", + "serde_json", "wasm-bindgen", - "wasm-bindgen-test", "web-sys", ] @@ -5096,38 +5167,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-bindgen-test" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e90e66d265d3a1efc0e72a54809ab90b9c0c515915c67cdf658689d2c22c6c" -dependencies = [ - "async-trait", - "cast", - "js-sys", - "libm", - "minicov", - "nu-ansi-term", - "num-traits", - "oorandom", - "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7150335716dce6028bead2b848e72f47b45e7b9422f64cccdc23bedca89affc1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "wasm-streams" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 02a64159..22362899 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,15 @@ members = [ "crates/echo-session-service", "crates/echo-session-client", "crates/echo-graph", + "crates/echo-dind-harness", + "crates/echo-dind-tests", "crates/echo-wasm-abi", + "crates/echo-registry-api", "crates/echo-session-ws-gateway", "specs/spec-000-rewrite", "crates/echo-wasm-bindings", + "crates/echo-wesley-gen", + "crates/echo-dry-tests", "xtask" ] resolver = "2" @@ -31,7 +36,10 @@ rust-version = "1.90.0" [workspace.dependencies] echo-app-core = { version = "0.1.0", path = "crates/echo-app-core" } echo-config-fs = { version = "0.1.0", path = "crates/echo-config-fs" } +echo-dind-tests = { version = "0.1.0", path = "crates/echo-dind-tests" } +echo-dry-tests = { version = "0.1.0", path = "crates/echo-dry-tests" } echo-graph = { version = "0.1.0", path = "crates/echo-graph" } +echo-registry-api = { version = "0.1.0", path = "crates/echo-registry-api" } echo-session-client = { version = "0.1.0", path = "crates/echo-session-client" } echo-session-proto = { version = "0.1.0", path = "crates/echo-session-proto" } echo-wasm-abi = { version = "0.1.0", path = "crates/echo-wasm-abi" } diff --git a/DETERMINISM-AUDIT.md b/DETERMINISM-AUDIT.md new file mode 100644 index 00000000..7bb1a26d --- /dev/null +++ b/DETERMINISM-AUDIT.md @@ -0,0 +1,307 @@ + + +# Determinism Audit for warp-core + +**Date:** 2026-01-13 +**Auditor:** Claude (with human oversight) + +## Executive Summary + +### TL;DR +The refactor targeting "serde removal" was attacking the wrong problem. **Serde itself isn't the enemy—non-deterministic data structures (HashMap/HashSet), non-deterministic serialization formats (JSON), and platform-variant float behavior are.** + +## Key Findings + +### ✅ GOOD: Already Deterministic + +1. **Core data structures use BTreeMap/BTreeSet throughout** + - `tick_patch.rs`: Uses BTreeMap for op deduplication (line 331-338) + - `snapshot.rs`: Uses BTreeSet for reachability (line 91-92) + - `scheduler.rs`: Final ready set uses stable radix sort (20-pass LSD, lines 383-450) + - All SlotIds and ops are explicitly sorted before hashing + +2. **Explicit canonical ordering everywhere** + - WarpOp::sort_key() provides canonical ordering (tick_patch.rs:203-283) + - Edges sorted by EdgeId before hashing (snapshot.rs:185-194) + - Scheduler uses deterministic radix sort for candidate ordering + +3. **Clean identifier types** + - All IDs are Blake3 hashes ([u8; 32]) + - All derive PartialOrd, Ord for stable comparison + - No hidden nondeterminism in identity + +### ⚠️ CONCERNS: Needs Investigation + +1. **HashMap/HashSet Usage (Non-Critical)** + - `engine_impl.rs`: HashMap for rule registries (NOT part of state hash) + - `scheduler.rs`: HashMap for pending txs (intermediate, final output is sorted) + - `attachment.rs`: HashMap for codec registry (NOT part of state) + - **Assessment**: These are internal bookkeeping, not part of canonical encoding + - **Action**: Audit that none of these leak into hashing/signing code paths + +2. **Float Usage (f32/f64) - CRITICAL AREA** + + - **CRITICAL FINDINGS (2026-01-13):** + - **Yes**, floats flow into canonical hashes. + - **Yes**, the system is sensitive to 1 ULP differences (confirmed by `tests/determinism_audit.rs`). + - **Implication**: `F32Scalar` (the default) relies on hardware `f32` arithmetic. If `a + b` varies by 1 ULP across platforms (x87 vs SSE vs NEON), the state hash **will diverge**. + - **Mitigation**: The `det_fixed` feature flag replaces `F32Scalar` with `DFix64` (Q32.32 fixed-point), providing guaranteed bit-perfect cross-platform determinism at the cost of performance. + - **Recommendation**: Use `det_fixed` for consensus-critical deployments. `F32Scalar` is acceptable for local-only or homogeneous-hardware deployments ("optimistic determinism"). + + **Location: `math/scalar.rs`** + - F32Scalar wraps f32 with canonicalization: + - NaN → 0x7fc00000 (canonical quiet NaN) + - Subnormals → +0.0 + - -0.0 → +0.0 + - **PROBLEM**: f32 arithmetic itself is NOT deterministic across platforms + - x87 FPU vs SSE may produce different intermediate results + - Rounding modes can vary + - Denormal handling varies by CPU flags + + **Location: `payload.rs`** + - Heavy f32 usage for motion payloads + - Comments mention "deterministic quantization to Q32.32" + - Has v0 (f32) and v2 (fixed-point?) formats + - `decode_motion_payload_v0`: Reads f32 from bytes (line 258) + - `encode_motion_payload_v0`: Writes f32 to bytes (line 216) + - **CRITICAL QUESTION**: Are these f32 values EVER used in: + - State hashing (compute_state_root)? + - Patch digests? + - Receipt digests? + - Signature inputs? + + **Location: `math/quat.rs`** + - Uses `[f32; 4]` for quaternion storage + - Arithmetic operations on quaternions + - **CONCERN**: Quaternion normalization/multiplication may be non-deterministic + + **ACTION REQUIRED**: + - Grep for all places F32Scalar/f32 values flow into hash computations + - Verify motion payloads are ONLY boundary data (not hashed) + - If floats DO affect hashes, replace with fixed-point (Q32.32 or similar) + +3. **Serde Feature Gates** + - `receipt.rs:123`: Has `#[cfg(feature = "serde")]` for `rule_id_short()` helper + - `snapshot.rs:77`: Has `#[cfg(feature = "serde")]` for `hash_hex()` helper + - `math/scalar.rs:110,120`: Serde impls for F32Scalar + - `serializable.rs`: Entire module was using serde derives + - **Assessment**: These are UI/debug helpers, NOT canonical encoding + - **Action**: Can keep serde feature gate for convenience IF: + - It's ONLY used with deterministic CBOR encoder + - NEVER used with serde_json + - JSON is only for debug/view layer + +### 🔥 CRITICAL: What Actually Needs Fixing + +1. **Enforce CBOR-only wire format** + - Make deterministic CBOR (echo-wasm-abi) the ONLY protocol boundary + - JSON can exist for debug/viewing ONLY (never canonical) + - Add compile-time checks/lints to prevent JSON usage + +2. **Audit float usage in hash paths** + - Search for all paths where f32/F32Scalar flows into: + - compute_state_root + - compute_patch_digest + - compute_tick_receipt_digest + - Any signature/commitment computation + - If found, replace with fixed-point Q32.32 + +3. **Add determinism tests** + - Test: Encode same patch 1000x → identical bytes + - Test: Encode across different runs → identical bytes + - Test: Encode same state → identical state_root + - Bonus: Cross-compile test (native + wasm) for identical hashes + +## Search Targets for Detailed Audit + +```bash +# Find HashMap/HashSet in critical paths +rg "HashMap|HashSet" crates/warp-core/src/{snapshot,tick_patch,receipt,cmd}.rs + +# Find float usage in critical paths +rg "\bf32\b|\bf64\b|F32Scalar" crates/warp-core/src/{snapshot,tick_patch,receipt,cmd}.rs + +# Find serde_json usage (should be ZERO in warp-core) +rg "serde_json" crates/warp-core/ + +# Find ciborium usage (should be ZERO except in tests) +rg "ciborium::(from_reader|into_writer)" crates/warp-core/src/ + +# Find all hashing/digest computation sites +rg "Hasher::new|finalize\(\)" crates/warp-core/src/ -A5 +``` + +## What to Revert from Previous Refactor + +**REVERT:** +- ❌ Removal of serde from Cargo.toml dependencies (it's fine if used with CBOR) +- ❌ Removal of all `#[cfg_attr(feature = "serde", derive(...))]` annotations + +**KEEP:** +1. ✅ Removal of serde_json dependency from warp-core +2. ✅ clippy.toml lint rules forbidding serde_json/ciborium +3. ✅ Manual JSON formatting in telemetry.rs +4. ✅ Use of deterministic CBOR in cmd.rs +5. ✅ Documentation about determinism requirements + +## Proposed Refactor Plan (3 Commits) + +### Commit 1: Revert overly-aggressive serde removal + document audit +**What:** +- Revert warp-core/Cargo.toml: Add serde back to dependencies +- Revert removed `#[cfg_attr(feature = "serde", ...)]` lines on core types +- Keep serde_json in dev-dependencies only +- Keep clippy lint rules (they prevent serde_json abuse) +- Add this DETERMINISM-AUDIT.md document +- Update CLAUDE-NOTES.md with corrected understanding + +**Why:** +- Serde with deterministic CBOR is fine +- The real problem is JSON/HashMap/floats, not serde derives +- We need derives for convenience with CBOR encoding + +**Files:** +- `crates/warp-core/Cargo.toml` +- `DETERMINISM-AUDIT.md` (NEW) +- `CLAUDE-NOTES.md` +- Revert cfg_attr removals in: attachment.rs, ident.rs, record.rs, receipt.rs, tx.rs, tick_patch.rs, snapshot.rs, warp_state.rs, graph.rs + +**Commit message:** +``` +fix(warp): revert overly-aggressive serde removal + +The previous refactor incorrectly treated serde as the source of +non-determinism. The real issues are: +1. Non-deterministic data structures (HashMap/HashSet) +2. Non-deterministic formats (JSON) +3. Platform-variant floats + +Serde itself is fine when used with deterministic encoders like our +canonical CBOR implementation (echo-wasm-abi). + +This commit: +- Restores serde dependency (with derives on core types) +- Keeps serde_json removed from dependencies +- Keeps clippy lints forbidding serde_json +- Adds DETERMINISM-AUDIT.md documenting real risks + +Next steps: Audit float usage in hash paths (see DETERMINISM-AUDIT.md) +``` + +### Commit 2: Complete float determinism audit + add tests +**What:** +- Grep every usage of f32/F32Scalar in snapshot.rs, tick_patch.rs, receipt.rs +- Document findings: Do floats flow into hashes? If yes, replace with Q32.32 +- Add determinism tests: + - test_patch_digest_repeatable: Encode same patch 100x → same bytes + - test_state_root_repeatable: Compute state_root 100x → same hash + - test_receipt_digest_repeatable: Encode same receipt 100x → same bytes +- Document in DETERMINISM-AUDIT.md whether floats are safe or need replacement + +**Files:** +- `crates/warp-core/src/snapshot.rs` (tests) +- `crates/warp-core/src/tick_patch.rs` (tests) +- `crates/warp-core/src/receipt.rs` (tests) +- `DETERMINISM-AUDIT.md` (updated with audit results) + +**Commit message:** +``` +test(warp): add determinism audit tests for core hashing + +Adds repeatability tests for: +- Patch digest computation +- State root computation +- Receipt digest computation + +These tests verify byte-for-byte identical outputs across multiple +encode operations on the same input. + +[If floats found in hash paths:] +CRITICAL: Audit revealed f32 usage in [X] - requires follow-up to +replace with fixed-point Q32.32 representation. + +[If floats NOT in hash paths:] +Audit confirmed: f32 values are boundary-only and never flow into +canonical hash computation. +``` + +### Commit 3: Enforce CBOR-only boundary + cleanup +**What:** +- Update serializable.rs to use deterministic CBOR encoding +- Remove remaining #[cfg(feature = "serde")] gates that are now unnecessary +- Add module-level docs explaining: CBOR for wire, JSON for debug only +- Update any wasm boundary code to explicitly use echo-wasm-abi::encode_cbor + +**Files:** +- `crates/warp-core/src/serializable.rs` +- `crates/warp-core/src/lib.rs` (docs) +- `crates/warp-wasm/` (ensure CBOR boundary) + +**Commit message:** +``` +refactor(warp): enforce CBOR-only protocol boundary + +Makes deterministic CBOR (echo-wasm-abi) the canonical encoding for +all protocol boundaries. JSON is relegated to debug/view layer only. + +Changes: +- serializable.rs uses CBOR encoding only +- Removed unnecessary serde feature gates +- Added docs: "CBOR for protocol, JSON for debug" +- warp-wasm boundary uses explicit CBOR encode/decode + +Determinism guarantee: All canonical artifacts (patches, receipts, +snapshots) are encoded via echo-wasm-abi::encode_cbor with: +- Sorted map keys +- Canonical integer/float widths +- No indefinite lengths +- No CBOR tags +``` + +## Key Principles (Corrected) + +1. **Determinism sources are data structures + formats, NOT serde** + - HashMap/HashSet iteration order → use BTreeMap/BTreeSet ✅ (already done) + - JSON object key order → use CBOR for wire format ✅ (in progress) + - Float arithmetic variance → audit + replace if in hash paths ⚠️ (TODO) + +2. **CBOR for wire, JSON for debug** + - Protocol boundary: Always CBOR (echo-wasm-abi) + - Debug/viewing: JSON is fine (never canonical) + - No serde_json in warp-core runtime dependencies + +3. **Serde is OK with deterministic encoders** + - serde::Serialize with CBOR → deterministic ✅ + - serde::Serialize with JSON → non-deterministic ❌ + - Keep serde derives for convenience with CBOR + +4. **Test everything** + - Byte-for-byte identical encoding across runs + - Ideally test native + wasm produce same hashes + - Test patches, receipts, snapshots independently + +## Outstanding Questions + +1. **Are motion payload f32 values EVER hashed?** + - Check: Does AtomPayload flow into any digest computation? + - If yes: Must replace with Q32.32 fixed-point + - If no: Boundary-only f32 is acceptable + +2. **Do quaternions (Quat) flow into state hashing?** + - Check: Are Quat values stored in AttachmentValue::Atom? + - Check: Does snapshot.rs hash quaternion payloads? + - If yes: Replace with fixed-point representation + +3. **Is RFC 8785 (JCS - canonical JSON) needed?** + - Current plan: No, use CBOR exclusively for wire format + - JSON only for debug/human-readable views + - Re-evaluate if JSON wire format is required later + +## Next Actions (Immediate) + +1. ✅ Create this audit document +2. ✅ Implement Commit 1 (Revert + Document) +3. ✅ Run full audit for f32 in hash paths (Confirmed: Floats affect hashes) +4. ✅ Add determinism tests (`crates/warp-core/tests/determinism_audit.rs`) +5. ⏳ Implement Commit 3 (Enforce CBOR-only boundary) +6. ⏳ Update CLAUDE-NOTES.md with final status diff --git a/DIND-MISSION-PHASE3.md b/DIND-MISSION-PHASE3.md new file mode 100644 index 00000000..ff23ed93 --- /dev/null +++ b/DIND-MISSION-PHASE3.md @@ -0,0 +1,33 @@ + + +# RUSTAGEDDON TRIALS: DIND Phase 3+ + +We are moving from "determinism exists" to "determinism is inevitable". + +## Phase 3: Torture Mode +- [ ] **1. Update `echo-dind-harness` CLI:** + - [ ] Add `Torture { scenario: PathBuf, runs: u32, threads: Option }` subcommand. + - [ ] Implement repeated in-process execution loop. + - [ ] Compare full hash chain across runs. + - [ ] Report first divergence (run index, step index, expected vs actual). + +## Phase 4: The Drills (Real Scenarios) +- [ ] **2. Scenario 010: Dense Rewrite Saturation** + - [ ] Create `scripts/gen_dense_rewrite.mjs`. + - [ ] Generate 1k-5k ops (node/edge churn). + - [ ] Record golden hashes. +- [ ] **3. Scenario 030: Error Determinism** + - [ ] Create `scripts/gen_error_determinism.mjs`. + - [ ] Generate invalid ops (bad payloads, invalid IDs). + - [ ] Assert state hash stability (no partial commits). + +## Phase 5: Randomized Construction +- [ ] **4. Randomized Order Drill** + - [ ] Create `scripts/gen_randomized_order.mjs`. + - [ ] Generate equivalent graph states via permuted op orders. + - [ ] Verify final state hashes match. + +## CI & Policy +- [ ] **5. CI Integration** + - [ ] Add `make dind` or `cargo xtask dind` to run suite. + - [ ] Add grep-checks for `SystemTime`, `Instant`, `rand`, `HashMap`. diff --git a/DIND-MISSION-PHASE5.md b/DIND-MISSION-PHASE5.md new file mode 100644 index 00000000..083ca9c6 --- /dev/null +++ b/DIND-MISSION-PHASE5.md @@ -0,0 +1,35 @@ + + +# RUSTAGEDDON TRIALS: DIND Phase 5 (The Shuffle) + +This phase tests robustness against insertion order and HashMap iteration leaks. + +## Doctrine +- **Invariant A (Self-Consistency):** A specific shuffled transcript must be deterministic across runs/platforms. +- **Invariant B (Convergence):** Different shuffles of *commutative* operations must yield the same final state hash. + +## Prerequisite: ID Stability +- Current Status: IDs are hashes of string labels (e.g., `make_node_id("label")`). +- This means IDs *are* stable/explicit provided the labels are deterministic. +- If we shuffle `InsertNode("A")` and `InsertNode("B")`, the resulting IDs are `hash("node:A")` and `hash("node:B")` regardless of order. +- **Verdict:** We are ready for Invariant B (Convergence). + +## Tasks +- [ ] **1. Randomized Generator (`scripts/bootstrap_randomized_order.mjs`):** + - [ ] Input: `--seed`, `--out`. + - [ ] Use seeded Xorshift32 (already implemented in dense rewrite script, extract/reuse?). + - [ ] Pattern: + - Create N nodes with deterministic labels (`node_0`..`node_N`). + - Shuffle creation order. + - Create M edges connecting random pairs (deterministic pairs based on seed, but shuffled insertion). + - Set K attachments (shuffled). + - **Critical:** Ensure no duplicate edges/attachments that would trigger overwrite behavior unless intended. +- [ ] **2. Generate Scenarios (`050_randomized_order_small`):** + - [ ] Generate 10 seeds (0001..0010). + - [ ] Record goldens for all. +- [ ] **3. Harness Update (`echo-dind-harness`):** + - [ ] Add `Converge { scenarios: Vec }` command. + - [ ] Runs all inputs, asserts final state hashes are identical. +- [ ] **4. CI Integration:** + - [ ] Run seeds 1-3 in PR check. + - [ ] Run `converge` on 1-3. diff --git a/DIND-MISSION.md b/DIND-MISSION.md new file mode 100644 index 00000000..50e1e3f7 --- /dev/null +++ b/DIND-MISSION.md @@ -0,0 +1,46 @@ + + +# RUSTAGEDDON TRIALS: DIND (Deterministic Ironclad Nightmare Drills) + +This mission implements a rigorous determinism verification suite for the Continuum engine. + +## Doctrine +We do not "hope" for determinism. We assert inevitability. +1. Same inputs ⇒ same outputs (byte-for-byte). +2. Same inputs ⇒ same intermediate states. +3. Same inputs ⇒ same errors. +4. Across runs, threads, and platforms. + +## Phase 1: The Heartbeat (Canonical State Hash) +- [ ] **1. Implement `canonical_state_hash` in `warp-core`:** + - [ ] Create a `CanonicalHash` trait or method on `GraphStore`. + - [ ] Must traverse nodes/edges in sorted order (by ID). + - [ ] Must serialize attachments deterministically (already done via "Mr Clean", but double check iteration order). + - [ ] Use BLAKE3. + - [ ] Expose this via `EchoKernel::state_hash()`. + +## Phase 2: The Harness (DIND Runner) +- [ ] **2. Create `crates/echo-dind-harness`:** + - [ ] CLI tool to run scenarios. + - [ ] Input: `.eintlog` (Sequence of `pack_intent_v1` bytes). + - [ ] Output: `hashes.json` (Array of state hashes after each op). + - [ ] Logic: Init kernel -> Apply Op -> Hash -> Repeat -> Assert match. + +## Phase 3: The Drills (Scenarios & Stress) +- [ ] **3. Create DIND Scenarios (`vendor/echo/testdata/dind/`):** + - [ ] `000_smoke_transcript.eintlog`: 50 ops, basic state changes. + - [ ] `010_graph_rewrite_dense.eintlog`: Saturation test (1k steps). + - [ ] `020_conflict_policy.eintlog`: Abort vs Retry stability. +- [ ] **4. Add Regression Test:** + - [ ] Add a standard Rust test that runs the harness against committed `hashes.json` for the smoke scenario. + +## Phase 4: Policy Enforcement +- [ ] **5. Ban Nondeterminism:** + - [ ] Verify `std::collections::HashMap` is not iterated in hash-sensitive paths (or use `BTreeMap` / sorted iterators). + - [ ] Add CI grep-check for `std::time`, `rand::thread_rng`. + +## Execution Order +1. Implement `canonical_state_hash` (The Prerequisite). +2. Create the Harness + Smoke Scenario (The MVP). +3. Lock it in with a regression test. +4. Expand scenarios. diff --git a/ECHO_ROADMAP.md b/ECHO_ROADMAP.md new file mode 100644 index 00000000..2c9736a0 --- /dev/null +++ b/ECHO_ROADMAP.md @@ -0,0 +1,134 @@ + + +# ECHO_ROADMAP — Phased Plan (Post-ADR Alignment) + +This roadmap re-syncs active work with recent ADRs: +- ADR-0003: Causality-first API + MaterializationBus/Port +- ADR-0004: No global state / explicit dependency injection +- ADR-0005: Physics as deterministic scheduled rewrites +- ADR-0006: Ban non-determinism via CI guards + +It also incorporates the latest DIND status from `GEMINI_CONTINUE_NOTES.md`. + +--- + +## Phase 0 — Repo Hygiene & Ownership +Goal: eliminate structural drift and restore correct ownership boundaries. + +- Move `crates/echo-dind-harness/` to the Echo repo (submodule) where it belongs. + - Remove the crate from this workspace once moved. + - Ensure any references/scripts in this repo point to the Echo submodule path. +- Audit for other Echo-owned crates/docs accidentally mirrored here. +- Update docs to reflect the correct location of DIND tooling. + +Exit criteria: +- `crates/echo-dind-harness/` no longer exists in this repo. +- A clear pointer exists for where to run DIND locally (Echo repo). + +--- + +## Phase 1 — Determinism Guardrails (ADR-0004 + ADR-0006) +Goal: codify the “no global state / no nondeterminism” doctrine and enforce it in CI. + +- Add CI scripts: + - `scripts/ban-globals.sh` (ADR-0004) + - `scripts/ban-nondeterminism.sh` and `scripts/ban-unordered-abi.sh` (ADR-0006) +- Wire scripts into CI for core crates (warp-core, warp-wasm, app wasm). +- Add minimal allowlist files (empty by default). +- Document determinism rules in README / doctrine doc. + +Exit criteria: +- CI fails on banned patterns. +- No global init (`install_*` style) in runtime core. + +--- + +## Phase 2 — Causality-First Boundary (ADR-0003) +Goal: enforce ingress-only writes and bus-first reads. + +- Define/confirm canonical intent envelopes for ingress (bytes-only). +- Ensure all write paths use ingress; remove any public “direct mutation” APIs. +- Implement MaterializationBus + MaterializationPort boundary: + - `view_subscribe`, `view_drain`, `view_replay_last`, `view_unsubscribe` + - channel IDs are byte-based (TypeId-derived), no strings in ABI +- Ensure UI uses materializations rather than direct state reads (except inspector). +- Define InspectorPort as a gated, separate API (optional). + +Exit criteria: +- No direct mutation path exposed to tools/UI. +- UI can run solely on materialized channels (or has a plan to get there). + +--- + +## Phase 3 — Physics Pipeline (ADR-0005) +Goal: implement deterministic physics as scheduled rewrites. + +- Implement tick phases: + 1) Integrate (predict) + 2) Candidate generation (broadphase + narrowphase) + 3) Solver iterations with footprint scheduling + 4) Finalize (commit) +- Canonical ordering: + - candidate keys: `(toi_q, min_id, max_id, feature_id)` + - deterministic iteration order for bodies and contacts +- Add optional trace channels for physics (debug materializations). +- Ensure physics outputs only emit post-commit. + +Exit criteria: +- Physics determinism across wasm/native with fixed seeds and inputs. +- No queue-based “micro-inbox” for derived physics work. + +--- + +## Phase 4 — DIND Mission Continuation (from GEMINI_CONTINUE_NOTES) +Goal: complete Mission 3 polish and Mission 4 performance envelope. + +### Mission 3 (Polish / Verification) +- Badge scoping: clarify scope (“PR set”) and platforms. +- Badge truth source: generate from CI artifacts only. +- Matrix audit: confirm explicit aarch64 coverage needs. + +### Mission 4 (Performance Envelope) +- Add `perf` command to DIND harness: + - `perf --baseline --tolerance 15%` + - track `time_ms`, `steps`, `time_per_step` + - optional: max nodes/edges, allocations +- Add baseline: `testdata/dind/perf_baseline.json` +- CI: + - PR: core scenarios, release build, fail on >15% regression + - Nightly: full suite, upload perf artifacts + +Exit criteria: +- DIND perf regressions fail CI. +- Stable baseline file committed. + +--- + +## Phase 5 — App-Repo Integration (flyingrobots.dev specific) +Goal: keep app-specific wasm boundary clean and deterministic. + +- Ensure TS encoders are the source of truth for binary protocol. +- Keep WASM as a thin bridge (no placeholder exports). +- Verify handshake matches registry version / codec / schema hash. +- Add or update tests verifying canonical ordering and envelope bytes. + +Exit criteria: +- ABI tests use TS encoders, not wasm placeholder exports. +- wasm build + vitest pass. + +--- + +## Open Questions / Dependencies +- Precise target crates for determinism guardrails in this repo vs Echo repo. +- Whether InspectorPort needs to exist in flyingrobots.dev or only in Echo. +- Final home for DIND artifacts: Echo repo or shared tooling repo. + +--- + +## Suggested Execution Order +1) Phase 0 (move DIND harness) to prevent ownership drift. +2) Phase 1 guardrails to lock determinism. +3) Phase 2 boundary enforcement (ingress + bus). +4) Phase 3 physics pipeline. +5) Phase 4 DIND polish/perf. +6) Phase 5 app integration clean-up. diff --git a/README.md b/README.md index 1c1c2e3e..6cd40886 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Instead of treating a game/simulation as a pile of mutable objects, Echo treats ## Project Status -**Status:** active R&D. The deterministic core (`warp-core`) and a session/tooling pipeline are implemented; higher‑level “world / ECS / systems” layers and the full timeline tree mechanics are specced and landing incrementally. +**Status (2026-01):** Active R&D. The deterministic core (`warp-core`) and session/tooling pipeline are implemented. Higher-level layers (ECS storage, system scheduler, timeline tree) are specced but not yet built. See [`docs/architecture-outline.md`](docs/architecture-outline.md) for per-section implementation status. ## Buckle Up @@ -23,7 +23,7 @@ Start here: - Start Here: [`docs/guide/start-here.md`](docs/guide/start-here.md) - Non-programmer on-ramp: [`docs/guide/eli5.md`](docs/guide/eli5.md) - WARP primer: [`docs/guide/warp-primer.md`](docs/guide/warp-primer.md) -- Project tour: [`docs/notes/project-tour-2025-12-28.md`](docs/notes/project-tour-2025-12-28.md) +- Docs map: [`docs/docs-index.md`](docs/docs-index.md) - AIΩN bridge doc: [`docs/aion-papers-bridge.md`](docs/aion-papers-bridge.md) - Architecture outline: [`docs/architecture-outline.md`](docs/architecture-outline.md) - Commit hashing spec: [`docs/spec-merkle-commit.md`](docs/spec-merkle-commit.md) @@ -86,6 +86,8 @@ If you’re building anything that benefits from “Git‑like” properties for - `crates/echo-app-core` / `crates/echo-config-fs` — “tool hexagon” ports + filesystem config adapter. - `crates/warp-ffi` / `crates/warp-wasm` — bindings around `warp-core`. - `crates/warp-benches` — Criterion microbenchmarks (scheduler drain, snapshot hash, etc.). +- `crates/echo-dind-harness` — determinism drill runner (DIND suite; cross‑platform hash verification). +- `crates/echo-dind-tests` — stable test app used by the DIND harness. ### Living specs (teaching slice) @@ -93,8 +95,7 @@ If you’re building anything that benefits from “Git‑like” properties for - `crates/echo-wasm-abi` — WASM‑friendly DTO schema for specs. - `crates/echo-wasm-bindings` — demo kernel + rewrite history (teaching slice; not the production engine). -For a deeper “tour” oriented around invariants and entry points, see -[`docs/notes/project-tour-2025-12-28.md`](docs/notes/project-tour-2025-12-28.md). +For a deeper tour, see [`docs/docs-index.md`](docs/docs-index.md). --- @@ -166,10 +167,18 @@ make spec-000-dev ## Contributions -- Start with `CONTRIBUTING.md` and `docs/execution-plan.md`. -- Echo is docs‑driven: behavior changes should be reflected in specs and logged in `docs/decision-log.md`. +- Start with `CONTRIBUTING.md`. +- Echo is docs-driven: behavior changes should be reflected in specs and ADRs. - Determinism is sacred: avoid wall‑clock time, uncontrolled randomness, and unspecified iteration order. +### Determinism guard scripts + +Echo enforces determinism guardrails via scripts in `scripts/`: + +- `scripts/ban-globals.sh` +- `scripts/ban-nondeterminism.sh` +- `scripts/ban-unordered-abi.sh` + ## Workflows Echo has a few “official workflows” (policy + blessed scripts/entrypoints), documented here: diff --git a/TASKS-DAG.md b/TASKS-DAG.md index d75e3d3f..b3bcec5e 100644 --- a/TASKS-DAG.md +++ b/TASKS-DAG.md @@ -5,12 +5,15 @@ This living list documents open issues and the inferred dependencies contributors capture while planning. Run `scripts/generate-tasks-dag.js` (requires Node.js + Graphviz `dot`) to render the DAG found at `docs/assets/dags/tasks-dag.svg`. ## [#19: Spec: Persistent Store (on-disk)](https://github.com/flyingrobots/echo/issues/19) +- Status: Open - Blocked by: - [#28: Draft spec document (header/ULEB128/property/string-pool)](https://github.com/flyingrobots/echo/issues/28) - Confidence: strong - Evidence: Inferred: Epic completion depends on Draft Spec task +- Evidence: `crates/echo-config-fs` exists for tool preferences, but no dedicated graph store crate (e.g. `echo-store`) exists yet. ## [#20: Spec: Commit/Manifest Signing](https://github.com/flyingrobots/echo/issues/20) +- Status: Open - Blocked by: - [#32: Draft signing spec](https://github.com/flyingrobots/echo/issues/32) - Confidence: strong @@ -29,6 +32,7 @@ This living list documents open issues and the inferred dependencies contributor - Evidence: Inferred: Epic completion depends on constituent task ## [#21: Spec: Security Contexts (FFI/WASM/CLI)](https://github.com/flyingrobots/echo/issues/21) +- Status: Open - Blocked by: - [#37: Draft security contexts spec](https://github.com/flyingrobots/echo/issues/37) - Confidence: strong @@ -44,156 +48,198 @@ This living list documents open issues and the inferred dependencies contributor - Evidence: Inferred: Epic completion depends on constituent task ## [#22: Benchmarks & CI Regression Gates](https://github.com/flyingrobots/echo/issues/22) +- Status: Completed - (No detected dependencies) ## [#23: CLI: verify/bench/inspect](https://github.com/flyingrobots/echo/issues/23) +- Status: Open +- Evidence: `crates/warp-cli/src/main.rs` is currently a placeholder printing "Hello, world!". - (No detected dependencies) ## [#24: Editor Hot-Reload (spec + impl)](https://github.com/flyingrobots/echo/issues/24) +- Status: Open - (No detected dependencies) ## [#25: Importer: TurtlGraph → Echo store](https://github.com/flyingrobots/echo/issues/25) +- Status: Open - (No detected dependencies) ## [#26: Plugin ABI (C) v0](https://github.com/flyingrobots/echo/issues/26) +- Status: In Progress - (No detected dependencies) ## [#27: Add golden test vectors (encoder/decoder)](https://github.com/flyingrobots/echo/issues/27) +- Status: Completed - (No detected dependencies) ## [#28: Draft spec document (header/ULEB128/property/string-pool)](https://github.com/flyingrobots/echo/issues/28) +- Status: Completed - Blocks: - [#19: Spec: Persistent Store (on-disk)](https://github.com/flyingrobots/echo/issues/19) - Confidence: strong - Evidence: Inferred: Epic completion depends on Draft Spec task ## [#29: Prototype header+string-pool encoder](https://github.com/flyingrobots/echo/issues/29) +- Status: Open - (No detected dependencies) ## [#30: Prototype header+string-pool decoder](https://github.com/flyingrobots/echo/issues/30) +- Status: Open - (No detected dependencies) ## [#32: Draft signing spec](https://github.com/flyingrobots/echo/issues/32) +- Status: Completed - Blocks: - [#20: Spec: Commit/Manifest Signing](https://github.com/flyingrobots/echo/issues/20) - Confidence: strong - Evidence: Inferred: Epic completion depends on Draft Spec task ## [#33: CI: sign release artifacts (dry run)](https://github.com/flyingrobots/echo/issues/33) +- Status: Open - Blocks: - [#20: Spec: Commit/Manifest Signing](https://github.com/flyingrobots/echo/issues/20) - Confidence: strong - Evidence: Inferred: Epic completion depends on constituent task ## [#34: CLI verify path](https://github.com/flyingrobots/echo/issues/34) +- Status: Open - Blocks: - [#20: Spec: Commit/Manifest Signing](https://github.com/flyingrobots/echo/issues/20) - Confidence: strong - Evidence: Inferred: Epic completion depends on constituent task ## [#35: Key management doc](https://github.com/flyingrobots/echo/issues/35) +- Status: Open - Blocks: - [#20: Spec: Commit/Manifest Signing](https://github.com/flyingrobots/echo/issues/20) - Confidence: strong - Evidence: Inferred: Epic completion depends on constituent task ## [#36: CI: verify signatures](https://github.com/flyingrobots/echo/issues/36) +- Status: Open - Blocks: - [#20: Spec: Commit/Manifest Signing](https://github.com/flyingrobots/echo/issues/20) - Confidence: strong - Evidence: Inferred: Epic completion depends on constituent task ## [#37: Draft security contexts spec](https://github.com/flyingrobots/echo/issues/37) +- Status: Completed - Blocks: - [#21: Spec: Security Contexts (FFI/WASM/CLI)](https://github.com/flyingrobots/echo/issues/21) - Confidence: strong - Evidence: Inferred: Epic completion depends on Draft Spec task ## [#38: FFI limits and validation](https://github.com/flyingrobots/echo/issues/38) +- Status: In Progress - Blocks: - [#21: Spec: Security Contexts (FFI/WASM/CLI)](https://github.com/flyingrobots/echo/issues/21) - Confidence: strong - Evidence: Inferred: Epic completion depends on constituent task ## [#39: WASM input validation](https://github.com/flyingrobots/echo/issues/39) +- Status: In Progress - Blocks: - [#21: Spec: Security Contexts (FFI/WASM/CLI)](https://github.com/flyingrobots/echo/issues/21) - Confidence: strong - - Evidence: Inferred: Epic completion depends on constituent task + - Evidence: `crates/warp-wasm/src/lib.rs` implements `validate_object_against_args` for schema checks. ## [#40: Unit tests for denials](https://github.com/flyingrobots/echo/issues/40) +- Status: Open - Blocks: - [#21: Spec: Security Contexts (FFI/WASM/CLI)](https://github.com/flyingrobots/echo/issues/21) - Confidence: strong - Evidence: Inferred: Epic completion depends on constituent task ## [#41: README+docs](https://github.com/flyingrobots/echo/issues/41) +- Status: Completed - (No detected dependencies) ## [#47: Scaffold CLI subcommands](https://github.com/flyingrobots/echo/issues/47) +- Status: Open - (No detected dependencies) ## [#48: Implement 'verify'](https://github.com/flyingrobots/echo/issues/48) +- Status: Open - (No detected dependencies) ## [#49: Implement 'bench'](https://github.com/flyingrobots/echo/issues/49) +- Status: Open - (No detected dependencies) ## [#50: Implement 'inspect'](https://github.com/flyingrobots/echo/issues/50) +- Status: Open - (No detected dependencies) ## [#51: Docs/man pages](https://github.com/flyingrobots/echo/issues/51) +- Status: Open - (No detected dependencies) ## [#75: Draft hot-reload spec](https://github.com/flyingrobots/echo/issues/75) +- Status: Open - (No detected dependencies) ## [#76: File watcher/debounce](https://github.com/flyingrobots/echo/issues/76) +- Status: Open - (No detected dependencies) ## [#77: Atomic snapshot swap](https://github.com/flyingrobots/echo/issues/77) +- Status: Open - (No detected dependencies) ## [#78: Editor gate + tests](https://github.com/flyingrobots/echo/issues/78) +- Status: Open - (No detected dependencies) ## [#79: Docs/logging](https://github.com/flyingrobots/echo/issues/79) +- Status: Open - (No detected dependencies) ## [#80: Draft importer spec](https://github.com/flyingrobots/echo/issues/80) +- Status: Open - (No detected dependencies) ## [#81: Minimal reader](https://github.com/flyingrobots/echo/issues/81) +- Status: Open - (No detected dependencies) ## [#82: Echo store loader](https://github.com/flyingrobots/echo/issues/82) +- Status: Open - (No detected dependencies) ## [#83: Integrity verification](https://github.com/flyingrobots/echo/issues/83) +- Status: Open - (No detected dependencies) ## [#84: Sample + tests](https://github.com/flyingrobots/echo/issues/84) +- Status: Open - (No detected dependencies) ## [#85: Draft C ABI spec](https://github.com/flyingrobots/echo/issues/85) +- Status: Completed - (No detected dependencies) ## [#86: C header + host loader](https://github.com/flyingrobots/echo/issues/86) +- Status: In Progress - (No detected dependencies) ## [#87: Version negotiation](https://github.com/flyingrobots/echo/issues/87) +- Status: Open - (No detected dependencies) ## [#88: Capability tokens](https://github.com/flyingrobots/echo/issues/88) +- Status: Open - (No detected dependencies) ## [#89: Example plugin + tests](https://github.com/flyingrobots/echo/issues/89) +- Status: Open - (No detected dependencies) ## [#103: Policy: Require PR↔Issue linkage and 'Closes #…' in PRs](https://github.com/flyingrobots/echo/issues/103) +- Status: Open - (No detected dependencies) ## [#170: TT1: StreamsFrame inspector support (backlog + cursors + admission decisions)](https://github.com/flyingrobots/echo/issues/170) +- Status: Open - Blocks: - [#171: TT2: Time Travel MVP (pause/rewind/buffer/catch-up)](https://github.com/flyingrobots/echo/issues/171) - Confidence: medium @@ -216,6 +262,7 @@ This living list documents open issues and the inferred dependencies contributor - Evidence: Inferred: TT1 Implementation blocks on TT1 Spec clarifications ## [#171: TT2: Time Travel MVP (pause/rewind/buffer/catch-up)](https://github.com/flyingrobots/echo/issues/171) +- Status: Open - Blocks: - [#172: TT3: Rulial diff / worldline compare MVP](https://github.com/flyingrobots/echo/issues/172) - Confidence: weak @@ -232,108 +279,135 @@ This living list documents open issues and the inferred dependencies contributor - Evidence: Inferred: TT2 task depends on TT1 Inspector scaffolding ## [#172: TT3: Rulial diff / worldline compare MVP](https://github.com/flyingrobots/echo/issues/172) +- Status: Open - Blocked by: - [#171: TT2: Time Travel MVP (pause/rewind/buffer/catch-up)](https://github.com/flyingrobots/echo/issues/171) - Confidence: weak - Evidence: Inferred: TT3 task depends on TT2 MVP ## [#173: S1: Deterministic Rhai surface (sandbox + claims/effects)](https://github.com/flyingrobots/echo/issues/173) +- Status: Open - (No detected dependencies) ## [#174: W1: Wesley as a boundary grammar (hashable view artifacts)](https://github.com/flyingrobots/echo/issues/174) +- Status: Open - (No detected dependencies) ## [#185: M1: Domain-separated hash contexts for core commitments (state_root/patch_digest/commit_id)](https://github.com/flyingrobots/echo/issues/185) +- Status: Completed - (No detected dependencies) ## [#186: M1: Domain-separated digest context for RenderGraph canonical bytes](https://github.com/flyingrobots/echo/issues/186) +- Status: Completed - (No detected dependencies) ## [#187: M4: Worldline convergence property suite (replay-from-patches converges)](https://github.com/flyingrobots/echo/issues/187) +- Status: Open - (No detected dependencies) ## [#188: M4: Kernel nondeterminism tripwires (forbid ambient HostTime/entropy sources)](https://github.com/flyingrobots/echo/issues/188) +- Status: Open - (No detected dependencies) ## [#189: M4: Concurrency litmus suite for scheduler determinism (overlap detection + canonical reduction)](https://github.com/flyingrobots/echo/issues/189) +- Status: Open - (No detected dependencies) ## [#190: M4: Determinism torture harness (1-thread vs N-thread + snapshot/restore fuzz)](https://github.com/flyingrobots/echo/issues/190) +- Status: Open - (No detected dependencies) ## [#191: TT0: Session stream time fields (HistoryTime ordering vs HostTime telemetry)](https://github.com/flyingrobots/echo/issues/191) +- Status: Open - (No detected dependencies) ## [#192: TT0: TTL/deadline semantics are ticks/epochs only (no host-time semantic deadlines)](https://github.com/flyingrobots/echo/issues/192) +- Status: Open - (No detected dependencies) ## [#193: W1: Schema hash chain pinning (SDL→IR→bundle) recorded in receipts](https://github.com/flyingrobots/echo/issues/193) +- Status: Open - (No detected dependencies) ## [#194: W1: SchemaDelta vocabulary (read-only MVP) + wesley patch dry-run plan](https://github.com/flyingrobots/echo/issues/194) +- Status: Open - (No detected dependencies) ## [#195: Backlog: JS-ABI packet checksum v2 (domain-separated hasher context)](https://github.com/flyingrobots/echo/issues/195) +- Status: Open - (No detected dependencies) ## [#198: W1: Provenance as query semantics (tick directive + proof objects + deterministic cursors)](https://github.com/flyingrobots/echo/issues/198) +- Status: Open - (No detected dependencies) ## [#199: TT3: Wesley worldline diff (compare query outputs/proofs across ticks)](https://github.com/flyingrobots/echo/issues/199) +- Status: Open - Blocked by: - [#171: TT2: Time Travel MVP (pause/rewind/buffer/catch-up)](https://github.com/flyingrobots/echo/issues/171) - Confidence: weak - Evidence: Inferred: TT3 task depends on TT2 MVP ## [#202: Spec: Provenance Payload (PP) v1 (canonical envelope for artifact lineage + signatures)](https://github.com/flyingrobots/echo/issues/202) +- Status: In Progress - (No detected dependencies) ## [#203: TT1: Constraint Lens panel (admission/scheduler explain-why + counterfactual sliders)](https://github.com/flyingrobots/echo/issues/203) +- Status: Open - (No detected dependencies) ## [#204: TT3: Provenance heatmap (blast radius / cohesion over time)](https://github.com/flyingrobots/echo/issues/204) +- Status: Open - Blocked by: - [#171: TT2: Time Travel MVP (pause/rewind/buffer/catch-up)](https://github.com/flyingrobots/echo/issues/171) - Confidence: weak - Evidence: Inferred: TT3 task depends on TT2 MVP ## [#205: TT2: Reliving debugger MVP (scrub timeline + causal slice + fork branch)](https://github.com/flyingrobots/echo/issues/205) +- Status: Open - Blocked by: - [#170: TT1: StreamsFrame inspector support (backlog + cursors + admission decisions)](https://github.com/flyingrobots/echo/issues/170) - Confidence: medium - Evidence: Inferred: TT2 task depends on TT1 Inspector scaffolding ## [#206: M2.1: DPO concurrency theorem coverage (critical pair / rule composition litmus tests)](https://github.com/flyingrobots/echo/issues/206) +- Status: Open - (No detected dependencies) ## [#207: Backlog: Run noisy-line test for naming (Echo / WARP / Wesley / Engram)](https://github.com/flyingrobots/echo/issues/207) +- Status: Open - (No detected dependencies) ## [#222: Demo 2: Splash Guy — deterministic rules + state model](https://github.com/flyingrobots/echo/issues/222) +- Status: Open - Blocks: - [#226: Demo 2: Splash Guy — docs: networking-first course modules](https://github.com/flyingrobots/echo/issues/226) - Confidence: medium - Evidence: Inferred: Docs follow Implementation ## [#223: Demo 2: Splash Guy — lockstep input protocol + two-peer harness](https://github.com/flyingrobots/echo/issues/223) +- Status: Open - Blocks: - [#226: Demo 2: Splash Guy — docs: networking-first course modules](https://github.com/flyingrobots/echo/issues/226) - Confidence: medium - Evidence: Inferred: Docs follow Implementation ## [#224: Demo 2: Splash Guy — controlled desync lessons (make it fail on purpose)](https://github.com/flyingrobots/echo/issues/224) +- Status: Open - Blocks: - [#226: Demo 2: Splash Guy — docs: networking-first course modules](https://github.com/flyingrobots/echo/issues/226) - Confidence: medium - Evidence: Inferred: Docs follow Implementation ## [#225: Demo 2: Splash Guy — minimal rendering / visualization path](https://github.com/flyingrobots/echo/issues/225) +- Status: Open - Blocks: - [#226: Demo 2: Splash Guy — docs: networking-first course modules](https://github.com/flyingrobots/echo/issues/226) - Confidence: medium - Evidence: Inferred: Docs follow Implementation ## [#226: Demo 2: Splash Guy — docs: networking-first course modules](https://github.com/flyingrobots/echo/issues/226) +- Status: Open - Blocked by: - [#222: Demo 2: Splash Guy — deterministic rules + state model](https://github.com/flyingrobots/echo/issues/222) - Confidence: medium @@ -349,6 +423,7 @@ This living list documents open issues and the inferred dependencies contributor - Evidence: Inferred: Docs follow Implementation ## [#231: Demo 3: Tumble Tower — Stage 0 physics (2D AABB stacking)](https://github.com/flyingrobots/echo/issues/231) +- Status: In Progress - Blocks: - [#238: Demo 3: Tumble Tower — docs course (physics ladder)](https://github.com/flyingrobots/echo/issues/238) - Confidence: medium @@ -356,8 +431,10 @@ This living list documents open issues and the inferred dependencies contributor - [#232: Demo 3: Tumble Tower — Stage 1 physics (rotation + angular, OBB contacts)](https://github.com/flyingrobots/echo/issues/232) - Confidence: strong - Evidence: Inferred: Stage 1 physics depends on Stage 0 +- Evidence: `crates/warp-geom` implements primitives (AABB, Transform), but solver logic for "stacking" is not yet visible in the top-level modules. ## [#232: Demo 3: Tumble Tower — Stage 1 physics (rotation + angular, OBB contacts)](https://github.com/flyingrobots/echo/issues/232) +- Status: Open - Blocks: - [#238: Demo 3: Tumble Tower — docs course (physics ladder)](https://github.com/flyingrobots/echo/issues/238) - Confidence: medium @@ -371,6 +448,7 @@ This living list documents open issues and the inferred dependencies contributor - Evidence: Inferred: Stage 1 physics depends on Stage 0 ## [#233: Demo 3: Tumble Tower — Stage 2 physics (friction + restitution)](https://github.com/flyingrobots/echo/issues/233) +- Status: Open - Blocks: - [#238: Demo 3: Tumble Tower — docs course (physics ladder)](https://github.com/flyingrobots/echo/issues/238) - Confidence: medium @@ -384,6 +462,7 @@ This living list documents open issues and the inferred dependencies contributor - Evidence: Inferred: Stage 2 physics depends on Stage 1 ## [#234: Demo 3: Tumble Tower — Stage 3 physics (sleeping + stack stability)](https://github.com/flyingrobots/echo/issues/234) +- Status: Open - Blocks: - [#238: Demo 3: Tumble Tower — docs course (physics ladder)](https://github.com/flyingrobots/echo/issues/238) - Confidence: medium @@ -394,24 +473,28 @@ This living list documents open issues and the inferred dependencies contributor - Evidence: Inferred: Stage 3 physics depends on Stage 2 ## [#235: Demo 3: Tumble Tower — lockstep harness + per-tick fingerprinting](https://github.com/flyingrobots/echo/issues/235) +- Status: Open - Blocks: - [#238: Demo 3: Tumble Tower — docs course (physics ladder)](https://github.com/flyingrobots/echo/issues/238) - Confidence: medium - Evidence: Inferred: Docs follow Implementation ## [#236: Demo 3: Tumble Tower — controlled desync breakers (physics edition)](https://github.com/flyingrobots/echo/issues/236) +- Status: Open - Blocks: - [#238: Demo 3: Tumble Tower — docs course (physics ladder)](https://github.com/flyingrobots/echo/issues/238) - Confidence: medium - Evidence: Inferred: Docs follow Implementation ## [#237: Demo 3: Tumble Tower — visualization (2D view + debug overlays)](https://github.com/flyingrobots/echo/issues/237) +- Status: Open - Blocks: - [#238: Demo 3: Tumble Tower — docs course (physics ladder)](https://github.com/flyingrobots/echo/issues/238) - Confidence: medium - Evidence: Inferred: Docs follow Implementation ## [#238: Demo 3: Tumble Tower — docs course (physics ladder)](https://github.com/flyingrobots/echo/issues/238) +- Status: Open - Blocked by: - [#231: Demo 3: Tumble Tower — Stage 0 physics (2D AABB stacking)](https://github.com/flyingrobots/echo/issues/231) - Confidence: medium @@ -436,27 +519,32 @@ This living list documents open issues and the inferred dependencies contributor - Evidence: Inferred: Docs follow Implementation ## [#239: Tooling: Reliving debugger UX (Constraint Lens + Provenance Heatmap)](https://github.com/flyingrobots/echo/issues/239) +- Status: Open - (No detected dependencies) ## [#243: TT1: dt policy (fixed timestep vs admitted dt stream)](https://github.com/flyingrobots/echo/issues/243) +- Status: Open - Blocks: - [#170: TT1: StreamsFrame inspector support (backlog + cursors + admission decisions)](https://github.com/flyingrobots/echo/issues/170) - Confidence: medium - Evidence: Inferred: TT1 Implementation blocks on TT1 Spec clarifications ## [#244: TT1: TimeStream retention + spool compaction + wormhole density](https://github.com/flyingrobots/echo/issues/244) +- Status: Open - Blocks: - [#170: TT1: StreamsFrame inspector support (backlog + cursors + admission decisions)](https://github.com/flyingrobots/echo/issues/170) - Confidence: medium - Evidence: Inferred: TT1 Implementation blocks on TT1 Spec clarifications ## [#245: TT1: Merge semantics for admitted stream facts across worldlines](https://github.com/flyingrobots/echo/issues/245) +- Status: Open - Blocks: - [#170: TT1: StreamsFrame inspector support (backlog + cursors + admission decisions)](https://github.com/flyingrobots/echo/issues/170) - Confidence: medium - Evidence: Inferred: TT1 Implementation blocks on TT1 Spec clarifications ## [#246: TT1: Security/capabilities for fork/rewind/merge in multiplayer](https://github.com/flyingrobots/echo/issues/246) +- Status: Open - Blocks: - [#170: TT1: StreamsFrame inspector support (backlog + cursors + admission decisions)](https://github.com/flyingrobots/echo/issues/170) - Confidence: medium @@ -465,4 +553,4 @@ This living list documents open issues and the inferred dependencies contributor --- Rendering note (2026-01-09): -- The tasks DAG renderer intentionally omits isolated nodes (issues with no incoming or outgoing edges) to keep the visualization readable. The script computes `connectedNodeIds` from edges, builds `filteredNodes`, and logs how many nodes were dropped. See `scripts/generate-tasks-dag.js` for the filtering logic. +- The tasks DAG renderer intentionally omits isolated nodes (issues with no incoming or outgoing edges) to keep the visualization readable. The script computes `connectedNodeIds` from edges, builds `filteredNodes`, and logs how many nodes were dropped. See `scripts/generate-tasks-dag.js` for the filtering logic. \ No newline at end of file diff --git a/WASM-TASKS.md b/WASM-TASKS.md index 0277c42d..670455d4 100644 --- a/WASM-TASKS.md +++ b/WASM-TASKS.md @@ -15,7 +15,7 @@ Policy: write failing tests first, then implement; check off tasks only when tes - [x] Add `wasm-bindgen` feature to kernel crate (or shim crate) and expose minimal WARP graph/rewrite API (add node, set field, connect, tombstone, materialize). - [x] Create shared DTO crate (`echo-wasm-abi`) with serde + wasm-bindgen-friendly types for graph and rewrite log; reuse in UI. -- [ ] Failing tests: wasm-bindgen unit test exercising add/set/connect/tombstone round-trip serialization. +- [x] wasm-bindgen unit tests exercising add/set/connect/tombstone round-trip serialization. ## P1 — UI MVP (Living Spec) diff --git a/crates/echo-dind-harness/Cargo.toml b/crates/echo-dind-harness/Cargo.toml new file mode 100644 index 00000000..e4d76cff --- /dev/null +++ b/crates/echo-dind-harness/Cargo.toml @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +[package] +name = "echo-dind-harness" +version = "0.1.0" +edition = "2021" +rust-version = "1.90.0" +license = "Apache-2.0" +description = "DIND determinism harness for Echo" +repository = "https://github.com/flyingrobots/echo" +readme = "README.md" +keywords = ["echo", "dind", "test"] +categories = ["development-tools"] + +[lib] +path = "src/lib.rs" + +[[bin]] +name = "echo-dind-harness" +path = "src/bin/echo-dind.rs" + +[dependencies] +anyhow = "1.0" +clap = { version = "4.4", features = ["derive"] } +hex = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +echo-wasm-abi = { workspace = true } +echo-dind-tests = { workspace = true, features = ["dind_ops"] } +warp-core = { workspace = true } diff --git a/crates/echo-dind-harness/README.md b/crates/echo-dind-harness/README.md new file mode 100644 index 00000000..751dddcd --- /dev/null +++ b/crates/echo-dind-harness/README.md @@ -0,0 +1,8 @@ + + +# Echo DIND Harness + +Determinism test runner for Echo/WARP. + +This harness uses the pinned test app in `crates/echo-dind-tests` and the +scenario corpus in `testdata/dind`. diff --git a/crates/echo-dind-harness/src/bin/echo-dind.rs b/crates/echo-dind-harness/src/bin/echo-dind.rs new file mode 100644 index 00000000..d4950f7c --- /dev/null +++ b/crates/echo-dind-harness/src/bin/echo-dind.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! CLI entry point for the DIND harness. + +use anyhow::Result; +use echo_dind_harness::dind::entrypoint; + +fn main() -> Result<()> { + entrypoint() +} diff --git a/crates/echo-dind-harness/src/dind.rs b/crates/echo-dind-harness/src/dind.rs new file mode 100644 index 00000000..e93242ba --- /dev/null +++ b/crates/echo-dind-harness/src/dind.rs @@ -0,0 +1,674 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! DIND (Deterministic Ironclad Nightmare Drills) scenario runner. +//! +//! This module provides the CLI and core logic for running determinism test scenarios. + +use anyhow::{bail, Context, Result}; +use clap::{Parser, Subcommand}; +use echo_dind_tests::generated::codecs::SCHEMA_HASH; +use echo_dind_tests::EchoKernel; +use echo_wasm_abi::{read_elog_frame, read_elog_header, ElogHeader}; +use std::collections::{BTreeSet, VecDeque}; +use std::fs::File; +use std::io::BufReader; +use std::path::{Path, PathBuf}; +use warp_core::{make_node_id, make_warp_id, AttachmentValue, GraphStore}; + +// ----------------------------------------------------------------------------- +// Permutation-mode termination constants +// ----------------------------------------------------------------------------- + +/// Maximum consecutive no-progress steps before declaring a stall. +/// +/// In permutation mode, the engine may occasionally report no progress if rules +/// don't match in a given tick. This budget allows transient stalls (e.g., waiting +/// for physics to settle) without aborting prematurely. 64 is generous for typical +/// scenarios while still catching genuine infinite loops within reasonable time. +const PERMUTATION_STALL_BUDGET: u64 = 64; + +/// Multiplier for computing max ticks from initial pending count. +/// +/// Most scenarios should complete in exactly `pending_count` ticks (one intent per +/// tick). The 4× multiplier provides headroom for multi-phase rules, deferred intents, +/// and physics simulation steps that don't consume intents. +const PERMUTATION_TICK_MULTIPLIER: u64 = 4; + +/// Minimum tick ceiling for permutation mode. +/// +/// Even if a scenario has few pending intents, we allow at least this many ticks to +/// handle edge cases like physics warm-up or delayed rule matching. +const PERMUTATION_MIN_TICKS: u64 = 256; + +/// CLI interface for the DIND harness. +/// +/// Parse with `Cli::parse()` and dispatch via `entrypoint()`. +#[derive(Parser)] +#[command(name = "echo-dind")] +#[command(about = "Deterministic Ironclad Nightmare Drills Harness")] +pub struct Cli { + /// The subcommand to execute. + #[command(subcommand)] + pub command: Commands, +} + +/// Available DIND subcommands. +#[derive(Subcommand)] +pub enum Commands { + /// Run a scenario and optionally check against a golden file + Run { + /// Path to .eintlog file + scenario: PathBuf, + /// Optional path to golden hashes.json + #[arg(long)] + golden: Option, + /// Optional path to emit reproduction bundle on failure + #[arg(long)] + emit_repro: Option, + }, + /// Record a scenario and output a golden hashes.json + Record { + /// Path to .eintlog file + scenario: PathBuf, + /// Path to output hashes.json + #[arg(long)] + out: PathBuf, + }, + /// Run a scenario repeatedly to detect non-determinism + Torture { + /// Path to .eintlog file + scenario: PathBuf, + /// Number of runs + #[arg(long, default_value = "20")] + runs: u32, + /// Optional path to emit reproduction bundle on failure + #[arg(long)] + emit_repro: Option, + }, + /// Verify that multiple scenarios converge to the same final state hash + Converge { + /// Paths to .eintlog files + scenarios: Vec, + /// Override converge scope (DANGEROUS; use only for ad-hoc debugging) + #[arg(long)] + scope: Option, + /// Required when using --scope to acknowledge non-canonical behavior + #[arg(long)] + i_know_what_im_doing: bool, + }, +} + +/// Golden file format for DIND scenario verification. +/// +/// Contains metadata and the expected sequence of state hashes after each step. +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct Golden { + /// Event log format version (currently 1). + pub elog_version: u16, + /// Hex-encoded schema hash identifying the codec version. + pub schema_hash_hex: String, + /// Hash domain identifier (e.g., "DIND_STATE_HASH_V1"). + pub hash_domain: String, + /// Hash algorithm name (e.g., "BLAKE3"). + pub hash_alg: String, + /// Hex-encoded state hashes, one per step (including initial state at index 0). + pub hashes_hex: Vec, +} + +#[derive(serde::Deserialize)] +struct ManifestEntry { + path: String, + #[serde(default)] + tags: Vec, + #[serde(default)] + converge_scope: Option, +} + +/// Create a reproduction bundle for debugging determinism failures. +/// +/// Writes to `out_dir`: +/// - `scenario.eintlog`: Copy of the input scenario +/// - `actual.hashes.json`: The hashes produced by this run +/// - `expected.hashes.json`: The expected golden hashes (if provided) +/// - `diff.txt`: A description of the failure +pub fn create_repro_bundle( + out_dir: &Path, + scenario_path: &Path, + actual_hashes: &[String], + header: &ElogHeader, + expected_golden: Option<&Golden>, + failure_msg: &str, +) -> Result<()> { + std::fs::create_dir_all(out_dir).context("failed to create repro dir")?; + + // 1. Copy scenario + std::fs::copy(scenario_path, out_dir.join("scenario.eintlog")) + .context("failed to copy scenario")?; + + // 2. Write actual hashes + let actual_golden = Golden { + elog_version: 1, + schema_hash_hex: hex::encode(header.schema_hash), + hash_domain: "DIND_STATE_HASH_V1".to_string(), + hash_alg: "BLAKE3".to_string(), + hashes_hex: actual_hashes.to_vec(), + }; + let f_actual = File::create(out_dir.join("actual.hashes.json")) + .context("failed to create actual.hashes.json")?; + serde_json::to_writer_pretty(f_actual, &actual_golden) + .context("failed to serialize actual.hashes.json")?; + + // 3. Write expected hashes if available + if let Some(exp) = expected_golden { + let f_exp = File::create(out_dir.join("expected.hashes.json")) + .context("failed to create expected.hashes.json")?; + serde_json::to_writer_pretty(f_exp, exp) + .context("failed to serialize expected.hashes.json")?; + } + + // 4. Write diff.txt + std::fs::write(out_dir.join("diff.txt"), failure_msg).context("failed to write diff.txt")?; + + Ok(()) +} + +/// Main CLI entrypoint for the DIND harness. +/// +/// Parses command-line arguments and dispatches to the appropriate subcommand. +/// Returns an error if the scenario fails validation or encounters an I/O error. +pub fn entrypoint() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Run { + scenario, + golden, + emit_repro, + } => { + let (hashes, header) = run_scenario(&scenario)?; + + if let Some(golden_path) = golden { + let f = File::open(&golden_path).context("failed to open golden file")?; + let expected: Golden = + serde_json::from_reader(BufReader::new(f)).with_context(|| { + format!("failed to parse golden file: {}", golden_path.display()) + })?; + + // Validate metadata + if expected.schema_hash_hex != hex::encode(header.schema_hash) { + let msg = format!( + "Golden schema hash mismatch. Expected {}, found {}", + expected.schema_hash_hex, + hex::encode(header.schema_hash) + ); + if let Some(repro_path) = emit_repro { + create_repro_bundle( + &repro_path, + &scenario, + &hashes, + &header, + Some(&expected), + &msg, + )?; + bail!("{}\nRepro bundle emitted to {:?}", msg, repro_path); + } + bail!("{}", msg); + } + + // Check length first to avoid silent truncation from zip + if hashes.len() != expected.hashes_hex.len() { + let msg = format!( + "Length mismatch. Run has {} steps, golden has {}.", + hashes.len(), + expected.hashes_hex.len() + ); + if let Some(repro_path) = emit_repro { + create_repro_bundle( + &repro_path, + &scenario, + &hashes, + &header, + Some(&expected), + &msg, + )?; + bail!("{}\nRepro bundle emitted to {:?}", msg, repro_path); + } + bail!("{}", msg); + } + + // Compare hashes (length already validated above) + for (i, (actual, expect)) in + hashes.iter().zip(expected.hashes_hex.iter()).enumerate() + { + if actual != expect { + let msg = format!( + "Hash mismatch at step {}.\nActual: {}\nExpected: {}", + i, actual, expect + ); + if let Some(repro_path) = emit_repro { + create_repro_bundle( + &repro_path, + &scenario, + &hashes, + &header, + Some(&expected), + &msg, + )?; + bail!("{}\nRepro bundle emitted to {:?}", msg, repro_path); + } + bail!("{}", msg); + } + } + + println!("DIND: OK. {} steps verified.", hashes.len()); + } else { + println!("DIND: Run complete. {} steps executed.", hashes.len()); + } + } + Commands::Record { scenario, out } => { + let (hashes, header) = run_scenario(&scenario)?; + + let golden = Golden { + elog_version: 1, + schema_hash_hex: hex::encode(header.schema_hash), + hash_domain: "DIND_STATE_HASH_V1".to_string(), + hash_alg: "BLAKE3".to_string(), + hashes_hex: hashes, + }; + + let f = File::create(&out).context("failed to create output file")?; + serde_json::to_writer_pretty(f, &golden) + .context("failed to serialize golden output")?; + println!( + "DIND: Recorded {} steps to {:?}", + golden.hashes_hex.len(), + out + ); + } + Commands::Torture { + scenario, + runs, + emit_repro, + } => { + println!("DIND: Torture starting. {} runs on {:?}", runs, scenario); + let (baseline_hashes, header) = + run_scenario(&scenario).context("Run 1 (Baseline) failed")?; + + // Construct a synthetic "Golden" from baseline for reuse in repro + let baseline_golden = Golden { + elog_version: 1, + schema_hash_hex: hex::encode(header.schema_hash), + hash_domain: "DIND_STATE_HASH_V1".to_string(), + hash_alg: "BLAKE3".to_string(), + hashes_hex: baseline_hashes.clone(), + }; + + for i in 2..=runs { + let (hashes, _) = run_scenario(&scenario).context(format!("Run {} failed", i))?; + + if hashes != baseline_hashes { + let mut failure_msg = String::new(); + // Find first divergence + for (step, (base, current)) in + baseline_hashes.iter().zip(hashes.iter()).enumerate() + { + if base != current { + failure_msg = format!( + "DIND: DIVERGENCE DETECTED in Run {} at Step {}.\nBaseline: {} +Current: {}", + i, step, base, current + ); + break; + } + } + if failure_msg.is_empty() && hashes.len() != baseline_hashes.len() { + failure_msg = format!("DIND: DIVERGENCE DETECTED in Run {}. Length mismatch: Baseline {}, Current {}", i, baseline_hashes.len(), hashes.len()); + } + + if let Some(repro_path) = emit_repro { + create_repro_bundle( + &repro_path, + &scenario, + &hashes, + &header, + Some(&baseline_golden), + &failure_msg, + )?; + bail!("{}\nRepro bundle emitted to {:?}", failure_msg, repro_path); + } + bail!("{}", failure_msg); + } + // Optional: print progress every 10/100 runs + if i % 10 == 0 { + println!("DIND: {}/{} runs clean...", i, runs); + } + } + println!("DIND: Torture complete. {} runs identical.", runs); + } + Commands::Converge { + scenarios, + scope, + i_know_what_im_doing, + } => { + if scenarios.is_empty() { + bail!("No scenarios provided for convergence check."); + } + + let converge_scope = if let Some(scope) = scope { + if !i_know_what_im_doing { + bail!("--scope requires --i-know-what-im-doing"); + } + println!( + "WARNING: Overriding converge scope; results may not reflect canonical test expectations." + ); + Some(scope) + } else { + resolve_converge_scope(&scenarios)? + }; + + println!( + "DIND: Checking convergence across {} scenarios...", + scenarios.len() + ); + + let baseline = &scenarios[0]; + let (hashes, _, kernel) = + run_scenario_with_kernel(baseline).context("Failed to run baseline")?; + let baseline_full = hashes.last().cloned().unwrap_or_default(); + let baseline_proj = match &converge_scope { + Some(scope) => hex::encode(projected_state_hash(&kernel, scope)), + None => baseline_full.clone(), + }; + + println!( + "Baseline established from {:?}: {}", + baseline, baseline_full + ); + if let Some(scope) = &converge_scope { + println!("Convergence scope: {}", scope); + println!("Baseline projected hash: {}", baseline_proj); + } + + for path in scenarios.iter().skip(1) { + let (hashes, _, kernel) = + run_scenario_with_kernel(path).context(format!("Failed to run {:?}", path))?; + let full_hash = hashes.last().cloned().unwrap_or_default(); + let projected_hash = match &converge_scope { + Some(scope) => hex::encode(projected_state_hash(&kernel, scope)), + None => full_hash.clone(), + }; + if projected_hash != baseline_proj { + bail!( + "DIND: CONVERGENCE FAILURE.\nBaseline ({:?}): {}\nCurrent ({:?}): {}", + baseline, + baseline_proj, + path, + projected_hash + ); + } + if converge_scope.is_some() { + println!("Converged (projected): {:?} => {}", path, projected_hash); + if full_hash != baseline_full { + println!(" Note: full hash differs (expected for commutative scenarios)."); + } + } + } + println!("DIND: CONVERGENCE OK. Projected hashes identical across all scenarios."); + } + } + + Ok(()) +} + +fn probe_interop(kernel: &EchoKernel) { + let ball_id = make_node_id("ball"); + if let Ok(Some(AttachmentValue::Atom(atom))) = kernel.engine().node_attachment(&ball_id) { + // This exercises payload.rs, fixed_q32_32.rs, and scalar.rs via the canonical decoder + let _ = warp_core::decode_motion_atom_payload(atom); + } +} + +/// Run a DIND scenario and return the sequence of state hashes. +/// +/// # Arguments +/// * `path` - Path to the `.eintlog` scenario file +/// +/// # Returns +/// A tuple of (state hashes as hex strings, event log header). +pub fn run_scenario(path: &Path) -> Result<(Vec, ElogHeader)> { + let (hashes, header, _) = run_scenario_with_kernel(path)?; + Ok((hashes, header)) +} + +/// Run a DIND scenario and return both hashes and the kernel state. +/// +/// Like [`run_scenario`], but also returns the kernel for further inspection. +pub fn run_scenario_with_kernel(path: &Path) -> Result<(Vec, ElogHeader, EchoKernel)> { + let f = File::open(path).context("failed to open scenario file")?; + let mut r = BufReader::new(f); + + let header = read_elog_header(&mut r)?; + let kernel_schema_hash = hex::decode(SCHEMA_HASH).context("invalid kernel schema hash hex")?; + + if header.schema_hash.as_slice() != kernel_schema_hash.as_slice() { + bail!( + "Scenario schema hash mismatch.\nScenario: {} +Kernel: {}", + hex::encode(header.schema_hash), + SCHEMA_HASH + ); + } + + let mut kernel = EchoKernel::new(); + let mut hashes = Vec::new(); + + // Initial state hash (step 0) + hashes.push(hex::encode(kernel.state_hash())); + + let ingest_all_first = scenario_requires_ingest_all_first(path)?; + + if ingest_all_first { + // Permutation-invariance mode: ingest all frames first, then step until the pending set is empty. + // + // This enforces identical tick membership across permutations (same set → same full graph). + let mut frame_count: u64 = 0; + while let Some(frame) = read_elog_frame(&mut r)? { + kernel.dispatch_intent(&frame); + frame_count += 1; + } + + let mut ticks: u64 = 0; + let mut stall_budget: u64 = PERMUTATION_STALL_BUDGET; + let mut last_pending = kernel + .engine() + .pending_intent_count() + .context("pending_intent_count failed")? as u64; + let max_ticks: u64 = last_pending + .saturating_mul(PERMUTATION_TICK_MULTIPLIER) + .max(PERMUTATION_MIN_TICKS); + + while kernel + .engine() + .pending_intent_count() + .context("pending_intent_count failed")? + > 0 + { + if ticks >= max_ticks { + bail!( + "Permutation-mode hard stop: exceeded max_ticks={max_ticks} (frames={frame_count}, pending_initial={last_pending})" + ); + } + + let progressed = kernel.step(); + probe_interop(&kernel); + if !progressed { + stall_budget = stall_budget.saturating_sub(1); + if stall_budget == 0 { + bail!( + "Permutation-mode hard stop: no progress budget exhausted (ticks={ticks}, pending={last_pending})" + ); + } + continue; + } + + ticks += 1; + hashes.push(hex::encode(kernel.state_hash())); + + let pending_now = kernel + .engine() + .pending_intent_count() + .context("pending_intent_count failed")? as u64; + if pending_now < last_pending { + last_pending = pending_now; + stall_budget = PERMUTATION_STALL_BUDGET; + } else { + stall_budget = stall_budget.saturating_sub(1); + if stall_budget == 0 { + bail!( + "Permutation-mode hard stop: pending set not shrinking (ticks={ticks}, pending={pending_now})" + ); + } + } + } + } else { + while let Some(frame) = read_elog_frame(&mut r)? { + kernel.dispatch_intent(&frame); + kernel.step(); // Budget? Just run it. + probe_interop(&kernel); // Exercise boundary code + hashes.push(hex::encode(kernel.state_hash())); + } + } + + Ok((hashes, header, kernel)) +} + +fn scenario_requires_ingest_all_first(scenario: &Path) -> Result { + let filename = scenario.file_name().and_then(|s| s.to_str()).unwrap_or(""); + // Fast-path for the current permutation-invariance suite. + if filename.starts_with("050_") { + return Ok(true); + } + + let Some(manifest_path) = manifest_path_for_scenario(scenario) else { + return Ok(false); + }; + let f = File::open(&manifest_path).context("failed to open MANIFEST.json")?; + let entries: Vec = serde_json::from_reader(BufReader::new(f)) + .with_context(|| format!("failed to parse MANIFEST.json: {}", manifest_path.display()))?; + let Some(entry) = entries.into_iter().find(|e| e.path == filename) else { + return Ok(false); + }; + + Ok(entry + .tags + .iter() + .any(|t| t == "ingest-all-first" || t == "permute-invariant" || t == "order-sensitive")) +} + +fn resolve_converge_scope(scenarios: &[PathBuf]) -> Result> { + let mut scope: Option = None; + let mut missing = Vec::new(); + + for scenario in scenarios { + let Some(manifest_path) = manifest_path_for_scenario(scenario) else { + missing.push(scenario.clone()); + continue; + }; + let Some(entry_scope) = find_manifest_scope(&manifest_path, scenario)? else { + missing.push(scenario.clone()); + continue; + }; + + match &scope { + None => scope = Some(entry_scope), + Some(existing) => { + if existing != &entry_scope { + bail!( + "Converge scope mismatch: '{}' vs '{}'", + existing, + entry_scope + ); + } + } + } + } + + if scope.is_none() { + return Ok(None); + } + if !missing.is_empty() { + bail!("Converge scope missing for scenarios: {:?}", missing); + } + Ok(scope) +} + +fn manifest_path_for_scenario(scenario: &Path) -> Option { + let parent = scenario.parent()?; + let manifest = parent.join("MANIFEST.json"); + if manifest.exists() { + Some(manifest) + } else { + None + } +} + +fn find_manifest_scope(manifest_path: &Path, scenario: &Path) -> Result> { + let f = File::open(manifest_path).context("failed to open MANIFEST.json")?; + let entries: Vec = serde_json::from_reader(BufReader::new(f)) + .with_context(|| format!("failed to parse MANIFEST.json: {}", manifest_path.display()))?; + let filename = scenario.file_name().and_then(|s| s.to_str()).unwrap_or(""); + let entry = entries.into_iter().find(|e| e.path == filename); + Ok(entry.and_then(|e| e.converge_scope)) +} + +fn projected_state_hash(kernel: &EchoKernel, scope: &str) -> [u8; 32] { + let root_id = make_node_id(scope); + let Some(store) = kernel.engine().state().store(&make_warp_id("root")) else { + return [0u8; 32]; + }; + subgraph_hash(store, root_id) +} + +fn subgraph_hash(store: &GraphStore, root: warp_core::NodeId) -> [u8; 32] { + if store.node(&root).is_none() { + return GraphStore::new(store.warp_id()).canonical_state_hash(); + } + + let mut nodes = BTreeSet::new(); + let mut queue = VecDeque::new(); + let mut edges = Vec::new(); + + nodes.insert(root); + queue.push_back(root); + + while let Some(node_id) = queue.pop_front() { + for edge in store.edges_from(&node_id) { + edges.push(edge.clone()); + if nodes.insert(edge.to) { + queue.push_back(edge.to); + } + } + } + + let mut sub = GraphStore::new(store.warp_id()); + + for node_id in &nodes { + if let Some(record) = store.node(node_id) { + sub.insert_node(*node_id, record.clone()); + if let Some(att) = store.node_attachment(node_id) { + sub.set_node_attachment(*node_id, Some(att.clone())); + } + } + } + + for edge in edges { + if nodes.contains(&edge.from) && nodes.contains(&edge.to) { + sub.insert_edge(edge.from, edge.clone()); + if let Some(att) = store.edge_attachment(&edge.id) { + sub.set_edge_attachment(edge.id, Some(att.clone())); + } + } + } + + sub.canonical_state_hash() +} diff --git a/crates/echo-dind-harness/src/lib.rs b/crates/echo-dind-harness/src/lib.rs new file mode 100644 index 00000000..5f426171 --- /dev/null +++ b/crates/echo-dind-harness/src/lib.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Echo DIND (Deterministic Ironclad Nightmare Drills) harness. +//! +//! This crate provides tooling for running determinism verification scenarios +//! against the Echo kernel, ensuring bit-identical state evolution across runs. + +pub mod dind; diff --git a/crates/echo-dind-harness/tests/coverage.rs b/crates/echo-dind-harness/tests/coverage.rs new file mode 100644 index 00000000..2d9f1764 --- /dev/null +++ b/crates/echo-dind-harness/tests/coverage.rs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! DIND scenario coverage tests. + +use anyhow::Result; +use echo_dind_harness::dind::run_scenario; +use serde::Deserialize; +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; + +#[derive(Deserialize)] +struct ManifestEntry { + path: String, + // we might use tags later for filtering + #[allow(dead_code)] + tags: Vec, + #[allow(dead_code)] + desc: String, +} + +#[test] +fn test_dind_coverage() -> Result<()> { + // Locate manifest relative to the crate or workspace root + // We assume running from workspace root or crate root. + // Let's try to find testdata/dind/MANIFEST.json + + let manifest_path = PathBuf::from("../../testdata/dind/MANIFEST.json"); + if !manifest_path.exists() { + // Fallback if running from workspace root + if PathBuf::from("testdata/dind/MANIFEST.json").exists() { + return run_suite(PathBuf::from("testdata/dind/MANIFEST.json")); + } + panic!("Could not find MANIFEST.json at {:?}", manifest_path); + } + + run_suite(manifest_path) +} + +fn run_suite(manifest_path: PathBuf) -> Result<()> { + let f = File::open(&manifest_path)?; + let manifest: Vec = serde_json::from_reader(BufReader::new(f))?; + + let base_dir = manifest_path.parent().unwrap(); + + for entry in manifest { + let scenario_path = base_dir.join(&entry.path); + println!("Coverage running: {:?}", scenario_path); + + let (hashes, _) = run_scenario(&scenario_path)?; + + // Basic assertion that we got hashes + assert!(!hashes.is_empty(), "Scenario produced no hashes"); + + // Check if there is a golden file to verify against + let golden_path = base_dir.join(entry.path.replace(".eintlog", ".hashes.json")); + if golden_path.exists() { + let f_golden = File::open(&golden_path)?; + let expected: echo_dind_harness::dind::Golden = + serde_json::from_reader(BufReader::new(f_golden))?; + assert_eq!( + hashes, expected.hashes_hex, + "Hash mismatch for {}", + entry.path + ); + } + } + + Ok(()) +} diff --git a/crates/echo-dind-harness/tests/permutation_invariance.rs b/crates/echo-dind-harness/tests/permutation_invariance.rs new file mode 100644 index 00000000..fe6a5c1f --- /dev/null +++ b/crates/echo-dind-harness/tests/permutation_invariance.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Tests for permutation invariance of the deterministic kernel. + +use std::path::PathBuf; + +use anyhow::Result; +use echo_dind_harness::dind::run_scenario; + +#[test] +fn permutation_invariance_050_seeds_produce_identical_full_hash_chains() -> Result<()> { + // Anchor testdata path to CARGO_MANIFEST_DIR (crate root at compile time). + let base_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../testdata/dind"); + + let scenarios = [ + "050_randomized_order_small_seed0001.eintlog", + "050_randomized_order_small_seed0002.eintlog", + "050_randomized_order_small_seed0003.eintlog", + ]; + + let mut baseline: Option> = None; + for name in scenarios { + let path = base_dir.join(name); + let (hashes, _header) = run_scenario(&path)?; + match &baseline { + None => baseline = Some(hashes), + Some(base) => assert_eq!( + &hashes, base, + "expected permutation-invariant full hash chain for {:?}", + path + ), + } + } + + Ok(()) +} diff --git a/crates/echo-dind-tests/Cargo.toml b/crates/echo-dind-tests/Cargo.toml new file mode 100644 index 00000000..ea81f28e --- /dev/null +++ b/crates/echo-dind-tests/Cargo.toml @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +[package] +name = "echo-dind-tests" +version = "0.1.0" +edition = "2021" +rust-version = "1.90.0" +license = "Apache-2.0" +description = "Determinism test kernel for DIND scenarios" +repository = "https://github.com/flyingrobots/echo" +readme = "README.md" +keywords = ["echo", "dind", "determinism", "test"] +categories = ["development-tools"] + +[lib] +path = "src/lib.rs" + +[dependencies] +warp-core = { workspace = true } +echo-dry-tests = { workspace = true } +echo-wasm-abi = { workspace = true } +bytes = "1.0" + +[features] +# Enables test-only ops (e.g., put_kv) for DIND convergence tests. +dind_ops = [] diff --git a/crates/echo-dind-tests/README.md b/crates/echo-dind-tests/README.md new file mode 100644 index 00000000..5fafbfb5 --- /dev/null +++ b/crates/echo-dind-tests/README.md @@ -0,0 +1,8 @@ + + +# echo-dind-tests + +Determinism test kernel used by the DIND harness. + +This crate is **not** the flyingrobots.dev app. It is a small, stable Echo-powered test app +with a pinned schema/op set used to generate and validate DIND scenarios. diff --git a/crates/echo-dind-tests/src/generated/codecs.rs b/crates/echo-dind-tests/src/generated/codecs.rs new file mode 100644 index 00000000..0defb7f3 --- /dev/null +++ b/crates/echo-dind-tests/src/generated/codecs.rs @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +// AUTO-GENERATED. DO NOT EDIT. +// Generated by scripts/generate_binary_codecs.mjs +#![allow( + unused_mut, + unused_variables, + unused_assignments, + unused_imports, + clippy::let_and_return, + missing_docs +)] + +use crate::generated::type_ids::TYPEID_PAYLOAD_MOTION_V2; +use warp_core::AtomView; +use warp_core::TypeId; + +const MAX_STRING_BYTES: usize = 65536; +pub const SCHEMA_HASH: &str = "0427e9fd236e92dfc8c0765f7bd429fc233bfbfab3cb67c9d03b22a0f31e7f8a"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Theme { + LIGHT = 0, + DARK = 1, + SYSTEM = 2, +} + +impl Theme { + pub fn from_u16(val: u16) -> Option { + match val { + 0 => Some(Self::LIGHT), + 1 => Some(Self::DARK), + 2 => Some(Self::SYSTEM), + _ => None, + } + } +} + +pub struct MotionV2View<'a> { + raw: &'a [u8], +} +impl<'a> MotionV2View<'a> { + #[inline] + fn slot_i64_le(&self, slot: usize) -> i64 { + let start = slot * 8; + let chunk: [u8; 8] = self.raw[start..start + 8].try_into().unwrap(); + i64::from_le_bytes(chunk) + } + pub fn pos_raw(&self) -> [i64; 3] { + [ + self.slot_i64_le(0), + self.slot_i64_le(1), + self.slot_i64_le(2), + ] + } + pub fn vel_raw(&self) -> [i64; 3] { + [ + self.slot_i64_le(3), + self.slot_i64_le(4), + self.slot_i64_le(5), + ] + } +} +impl<'a> AtomView<'a> for MotionV2View<'a> { + const TYPE_ID: TypeId = TYPEID_PAYLOAD_MOTION_V2; + const BYTE_LEN: usize = 48; + #[inline] + fn parse(bytes: &'a [u8]) -> Option { + Some(Self { raw: bytes }) + } +} +pub struct MotionV2Builder { + buf: [u8; 48], +} +impl MotionV2Builder { + pub fn new(pos: [i64; 3], vel: [i64; 3]) -> Self { + let mut buf = [0u8; 48]; + for (i, raw) in pos.into_iter().chain(vel.into_iter()).enumerate() { + buf[i * 8..i * 8 + 8].copy_from_slice(&raw.to_le_bytes()); + } + Self { buf } + } + #[inline] + pub fn into_bytes(self) -> bytes::Bytes { + bytes::Bytes::copy_from_slice(&self.buf) + } +} + +pub mod ops { + use super::*; + pub mod app_state { + use super::*; + pub const OP_ID: u32 = 190543078; + pub struct Args {} + pub fn encode_vars(args: &Args) -> Vec { + let mut out = Vec::new(); + out + } + pub fn decode_vars(bytes: &[u8]) -> Option { + let mut offset = 0; + Some(Args {}) + } + } + pub mod drop_ball { + use super::*; + pub const OP_ID: u32 = 778504871; + pub struct Args {} + pub fn encode_vars(args: &Args) -> Vec { + let mut out = Vec::new(); + out + } + pub fn decode_vars(bytes: &[u8]) -> Option { + let mut offset = 0; + Some(Args {}) + } + } + pub mod set_theme { + use super::*; + pub const OP_ID: u32 = 1822649880; + pub struct Args { + pub mode: Theme, + } + pub fn encode_vars(args: &Args) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&(args.mode as u16).to_le_bytes()); + out + } + pub fn decode_vars(bytes: &[u8]) -> Option { + let mut offset = 0; + if bytes.len() < offset + 2 { + return None; + } + let val = u16::from_le_bytes(bytes[offset..offset + 2].try_into().unwrap()); + let mode = Theme::from_u16(val)?; + offset += 2; + Some(Args { mode }) + } + } + pub mod route_push { + use super::*; + pub const OP_ID: u32 = 2216217860; + pub struct Args { + pub path: String, + } + pub fn encode_vars(args: &Args) -> Vec { + let mut out = Vec::new(); + let b = args.path.as_bytes(); + out.extend_from_slice(&(b.len() as u32).to_le_bytes()); + out.extend_from_slice(b); + out + } + pub fn decode_vars(bytes: &[u8]) -> Option { + let mut offset = 0; + if bytes.len() < offset + 4 { + return None; + } + let s_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize; + offset += 4; + if s_len > super::super::MAX_STRING_BYTES || bytes.len() < offset + s_len { + return None; + } + let path = std::str::from_utf8(&bytes[offset..offset + s_len]) + .ok()? + .to_string(); + offset += s_len; + Some(Args { path }) + } + } + pub mod toggle_nav { + use super::*; + pub const OP_ID: u32 = 3272403183; + pub struct Args {} + pub fn encode_vars(args: &Args) -> Vec { + let mut out = Vec::new(); + out + } + pub fn decode_vars(bytes: &[u8]) -> Option { + let mut offset = 0; + Some(Args {}) + } + } + pub mod toast { + use super::*; + pub const OP_ID: u32 = 4255241313; + pub struct Args { + pub message: String, + } + pub fn encode_vars(args: &Args) -> Vec { + let mut out = Vec::new(); + let b = args.message.as_bytes(); + out.extend_from_slice(&(b.len() as u32).to_le_bytes()); + out.extend_from_slice(b); + out + } + pub fn decode_vars(bytes: &[u8]) -> Option { + let mut offset = 0; + if bytes.len() < offset + 4 { + return None; + } + let s_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize; + offset += 4; + if s_len > super::super::MAX_STRING_BYTES || bytes.len() < offset + s_len { + return None; + } + let message = std::str::from_utf8(&bytes[offset..offset + s_len]) + .ok()? + .to_string(); + offset += s_len; + Some(Args { message }) + } + } + #[cfg(feature = "dind_ops")] + pub mod put_kv { + use super::*; + pub const OP_ID: u32 = 3000000001; + pub struct Args { + pub key: String, + pub value: String, + } + pub fn encode_vars(args: &Args) -> Vec { + let mut out = Vec::new(); + let kb = args.key.as_bytes(); + out.extend_from_slice(&(kb.len() as u32).to_le_bytes()); + out.extend_from_slice(kb); + let vb = args.value.as_bytes(); + out.extend_from_slice(&(vb.len() as u32).to_le_bytes()); + out.extend_from_slice(vb); + out + } + pub fn decode_vars(bytes: &[u8]) -> Option { + let mut offset = 0; + if bytes.len() < offset + 4 { + return None; + } + let k_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize; + offset += 4; + if k_len > super::super::MAX_STRING_BYTES || bytes.len() < offset + k_len { + return None; + } + let key = std::str::from_utf8(&bytes[offset..offset + k_len]) + .ok()? + .to_string(); + offset += k_len; + if bytes.len() < offset + 4 { + return None; + } + let v_len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize; + offset += 4; + if v_len > super::super::MAX_STRING_BYTES || bytes.len() < offset + v_len { + return None; + } + let value = std::str::from_utf8(&bytes[offset..offset + v_len]) + .ok()? + .to_string(); + offset += v_len; + Some(Args { key, value }) + } + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + use echo_wasm_abi::pack_intent_v1; + + #[test] + fn test_golden_set_theme() { + // setTheme(DARK) + // OpID: 1822649880 (0x6ca33218) -> LE: 18 32 A3 6C + // Args: mode=Theme::DARK (1) -> u16 LE: 01 00 + // Vars Len: 2 -> 02 00 00 00 + // Envelope: EINT + OpID + Len + Vars + let args = ops::set_theme::Args { mode: Theme::DARK }; + let vars = ops::set_theme::encode_vars(&args); + let packed = pack_intent_v1(ops::set_theme::OP_ID, &vars).unwrap(); + + let expected_vars = vec![0x01, 0x00]; + assert_eq!(vars, expected_vars, "Vars mismatch"); + + let expected_header = b"EINT"; + let expected_op = 1822649880u32.to_le_bytes(); + let expected_len = 2u32.to_le_bytes(); + + let mut expected = Vec::new(); + expected.extend_from_slice(expected_header); + expected.extend_from_slice(&expected_op); + expected.extend_from_slice(&expected_len); + expected.extend_from_slice(&expected_vars); + + assert_eq!(packed, expected, "Envelope mismatch"); + } +} diff --git a/crates/warp-core/src/demo/mod.rs b/crates/echo-dind-tests/src/generated/mod.rs similarity index 52% rename from crates/warp-core/src/demo/mod.rs rename to crates/echo-dind-tests/src/generated/mod.rs index 65915bd8..ec0cc7cc 100644 --- a/crates/warp-core/src/demo/mod.rs +++ b/crates/echo-dind-tests/src/generated/mod.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Demo rules and helpers used by tests and examples. -pub mod motion; -pub mod ports; +//! Auto-generated codec and type definitions for DIND tests. + +pub mod codecs; +pub mod type_ids; diff --git a/crates/echo-dind-tests/src/generated/type_ids.rs b/crates/echo-dind-tests/src/generated/type_ids.rs new file mode 100644 index 00000000..bd6a022c --- /dev/null +++ b/crates/echo-dind-tests/src/generated/type_ids.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +// AUTO-GENERATED. DO NOT EDIT. +// Generated by scripts/generate_binary_codecs.mjs +#![allow(missing_docs)] + +use warp_core::TypeId; + +/// make_type_id("payload/motion/v2") +pub const TYPEID_PAYLOAD_MOTION_V2: TypeId = TypeId([ + 0x98, 0x64, 0x2a, 0xbd, 0x9b, 0x21, 0x50, 0xf2, 0x8b, 0xea, 0xac, 0xf9, 0xf3, 0x7a, 0x39, 0x44, + 0x9e, 0xda, 0x78, 0xf7, 0xf3, 0xac, 0xe3, 0x9b, 0x5a, 0x9a, 0xd8, 0x52, 0x32, 0x5d, 0xbc, 0x1f, +]); + +/// make_type_id("state/nav_open") +pub const TYPEID_STATE_NAV_OPEN: TypeId = TypeId([ + 0xfb, 0xce, 0xb0, 0xfc, 0xb7, 0x55, 0x52, 0xc7, 0x3f, 0x4f, 0xe7, 0x15, 0xa7, 0x11, 0x4b, 0x47, + 0x60, 0xe3, 0xb0, 0xa2, 0xfa, 0x7e, 0x87, 0xda, 0xbb, 0x7c, 0x26, 0x67, 0x99, 0x0b, 0xc6, 0x28, +]); + +/// make_type_id("state/route_path") +pub const TYPEID_STATE_ROUTE_PATH: TypeId = TypeId([ + 0xdc, 0x5b, 0x11, 0x8e, 0xe2, 0xcf, 0x5f, 0x58, 0x90, 0x33, 0x4b, 0xa5, 0xca, 0xa0, 0x02, 0xf3, + 0xf4, 0xde, 0x6e, 0x0d, 0x0d, 0x98, 0x66, 0xe1, 0x07, 0xb7, 0x97, 0xbc, 0x77, 0xc6, 0xac, 0xa2, +]); + +/// make_type_id("state/theme") +pub const TYPEID_STATE_THEME: TypeId = TypeId([ + 0xa3, 0xbe, 0xb4, 0x4f, 0x97, 0xb9, 0x62, 0xa4, 0x63, 0x5f, 0x27, 0xda, 0xde, 0x30, 0x8d, 0xdf, + 0xfd, 0xed, 0x92, 0xea, 0x7c, 0xc6, 0x75, 0x29, 0x13, 0x5c, 0x9f, 0x5f, 0x7c, 0x57, 0x5e, 0x1d, +]); + +/// make_type_id("state/kv") +pub const TYPEID_STATE_KV: TypeId = TypeId([ + 0x85, 0xf3, 0x48, 0xfb, 0x9b, 0x46, 0x24, 0x57, 0xee, 0x85, 0xfb, 0x9a, 0x6c, 0x36, 0xc5, 0x61, + 0x24, 0x9e, 0xd2, 0x0d, 0x2f, 0x05, 0x17, 0xdd, 0xea, 0x79, 0x3e, 0x55, 0xce, 0x9d, 0x83, 0x13, +]); + +/// make_type_id("view_op/MoveEntity") +pub const TYPEID_VIEW_OP_MOVEENTITY: TypeId = TypeId([ + 0x1f, 0x86, 0x36, 0xb3, 0xf7, 0xfc, 0xed, 0x9f, 0x7a, 0xf0, 0x74, 0x4b, 0x79, 0x04, 0x84, 0x8f, + 0x57, 0xe3, 0xfe, 0x13, 0xbe, 0x6c, 0x83, 0x13, 0x3b, 0x37, 0xf4, 0xc8, 0xb5, 0x95, 0x1d, 0xa1, +]); + +/// make_type_id("view_op/RoutePush") +pub const TYPEID_VIEW_OP_ROUTEPUSH: TypeId = TypeId([ + 0x99, 0xd7, 0x62, 0xbe, 0x63, 0x7e, 0xaf, 0xe6, 0x5a, 0xab, 0xc7, 0x89, 0x27, 0xc3, 0x58, 0xa6, + 0x50, 0xf0, 0x66, 0x98, 0x03, 0xd1, 0xb9, 0x0d, 0xdb, 0x78, 0x5c, 0x21, 0x08, 0x23, 0x07, 0x3f, +]); + +/// make_type_id("view_op/SetTheme") +pub const TYPEID_VIEW_OP_SETTHEME: TypeId = TypeId([ + 0xb8, 0xfc, 0x67, 0xc8, 0xd2, 0x86, 0xe8, 0xa5, 0x9a, 0x4a, 0xac, 0x5b, 0x91, 0x79, 0x0f, 0x20, + 0xb9, 0xde, 0x65, 0xaa, 0xc0, 0x8c, 0xfb, 0xc2, 0x7b, 0xd4, 0x87, 0x5d, 0x01, 0x90, 0xd3, 0xd9, +]); + +/// make_type_id("view_op/ShowToast") +pub const TYPEID_VIEW_OP_SHOWTOAST: TypeId = TypeId([ + 0xb4, 0xb4, 0x59, 0x5e, 0x6a, 0xcb, 0x7b, 0xb1, 0xfb, 0x3b, 0xb1, 0x9f, 0x01, 0xf1, 0xd2, 0x41, + 0x6a, 0x50, 0xc6, 0xe9, 0xc5, 0xbc, 0xa2, 0x00, 0x05, 0xdc, 0x1f, 0xb4, 0x02, 0x1e, 0x58, 0x2f, +]); + +/// make_type_id("view_op/ToggleNav") +pub const TYPEID_VIEW_OP_TOGGLENAV: TypeId = TypeId([ + 0xe1, 0x8a, 0x88, 0xa2, 0x3c, 0xe8, 0x2a, 0x01, 0x3d, 0xdb, 0xd7, 0xb9, 0xed, 0x67, 0xe1, 0x8c, + 0xcb, 0x41, 0x4c, 0xc8, 0xa2, 0x71, 0xd5, 0x4f, 0x34, 0x48, 0x7b, 0x81, 0xe5, 0x4c, 0xf1, 0x44, +]); diff --git a/crates/echo-dind-tests/src/lib.rs b/crates/echo-dind-tests/src/lib.rs new file mode 100644 index 00000000..eeb1abe9 --- /dev/null +++ b/crates/echo-dind-tests/src/lib.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Deterministic test kernel used by the DIND harness. + +use echo_dry_tests::build_motion_demo_engine; +use warp_core::{make_node_id, ApplyResult, DispatchDisposition, Engine}; + +/// Auto-generated codec and type definitions. +pub mod generated; +/// DIND test rules and state management. +pub mod rules; + +use rules::{ + ball_physics_rule, drop_ball_rule, route_push_rule, set_theme_rule, toast_rule, toggle_nav_rule, +}; + +#[cfg(feature = "dind_ops")] +use rules::put_kv_rule; + +/// The deterministic kernel used for DIND scenarios. +pub struct EchoKernel { + engine: Engine, +} + +impl Default for EchoKernel { + fn default() -> Self { + Self::new() + } +} + +impl EchoKernel { + /// Create a new kernel instance with all DIND rules registered. + pub fn new() -> Self { + let mut e = build_motion_demo_engine(); + e.register_rule(toast_rule()) + .expect("toast_rule registration failed"); + e.register_rule(route_push_rule()) + .expect("route_push_rule registration failed"); + e.register_rule(set_theme_rule()) + .expect("set_theme_rule registration failed"); + e.register_rule(toggle_nav_rule()) + .expect("toggle_nav_rule registration failed"); + e.register_rule(drop_ball_rule()) + .expect("drop_ball_rule registration failed"); + e.register_rule(ball_physics_rule()) + .expect("ball_physics_rule registration failed"); + #[cfg(feature = "dind_ops")] + e.register_rule(put_kv_rule()) + .expect("put_kv_rule registration failed"); + e.register_rule(warp_core::inbox::ack_pending_rule()) + .expect("ack_pending_rule registration failed"); + + Self { engine: e } + } + + /// Dispatch an intent (canonical bytes) with an auto-assigned sequence number. + pub fn dispatch_intent(&mut self, intent_bytes: &[u8]) { + // Canonical ingress: content-addressed, idempotent on `intent_id`. + // Bytes are opaque to the core engine; validation is the caller's responsibility. + let _ = self + .engine + .ingest_intent(intent_bytes) + .expect("ingest intent"); + } + + /// Run a deterministic step. + pub fn step(&mut self) -> bool { + let tx = self.engine.begin(); + let ball_id = make_node_id("ball"); + let mut dirty = false; + + // Consume exactly one pending intent per tick, using canonical `intent_id` order. + let dispatch = self + .engine + .dispatch_next_intent(tx) + .expect("dispatch_next_intent"); + if matches!(dispatch, DispatchDisposition::Consumed { .. }) { + dirty = true; + } + + if matches!( + self.engine + .apply(tx, rules::BALL_PHYSICS_RULE_NAME, &ball_id) + .expect("apply physics rule"), + ApplyResult::Applied + ) { + dirty = true; + } + + if dirty { + // Commit must succeed in test kernel; failure indicates corruption. + self.engine + .commit(tx) + .expect("commit failed - test kernel corruption"); + true + } else { + self.engine.abort(tx); + false + } + } + + /// Access the underlying engine (read-only). + pub fn engine(&self) -> &Engine { + &self.engine + } + + /// Access the underlying engine (mutable). + pub fn engine_mut(&mut self) -> &mut Engine { + &mut self.engine + } + + /// Canonical state hash of the root warp. + /// + /// # Panics + /// Panics if the root warp does not exist (indicates test kernel misconfiguration). + pub fn state_hash(&self) -> [u8; 32] { + self.engine + .state() + .store(&warp_core::make_warp_id("root")) + .expect("root warp must exist in test kernel") + .canonical_state_hash() + } +} diff --git a/crates/echo-dind-tests/src/rules.rs b/crates/echo-dind-tests/src/rules.rs new file mode 100644 index 00000000..2d0c7607 --- /dev/null +++ b/crates/echo-dind-tests/src/rules.rs @@ -0,0 +1,597 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Rewrite rules for the DIND test kernel. + +use crate::generated::codecs::{ops, MotionV2Builder, MotionV2View}; +use crate::generated::type_ids::*; +use echo_wasm_abi::unpack_intent_v1; +use warp_core::{ + make_edge_id, make_node_id, make_type_id, AtomPayload, AtomView, AttachmentKey, AttachmentSet, + AttachmentValue, ConflictPolicy, EdgeRecord, Footprint, GraphStore, Hash, IdSet, NodeId, + NodeKey, NodeRecord, PatternGraph, RewriteRule, TypeId, +}; + +const TYPE_VIEW_OP: &str = "sys/view/op"; + +// ----------------------------------------------------------------------------- +// Fixed-point physics constants (Q32.32 format) +// ----------------------------------------------------------------------------- + +/// Q32.32 scale factor: 1 unit = 2^32 fixed-point units. +const FIXED_POINT_SCALE: i64 = 1 << 32; + +/// Initial ball height in world units (400 units above ground). +const BALL_INITIAL_HEIGHT: i64 = 400; + +/// Initial downward velocity magnitude in world units per tick. +const BALL_INITIAL_VELOCITY: i64 = 5; + +/// Gravity acceleration in world units per tick (applied each physics step). +const GRAVITY_ACCEL: i64 = 1; + +/// Human-readable name for the route push command rule. +pub const ROUTE_PUSH_RULE_NAME: &str = "cmd/route_push"; +/// Human-readable name for the set theme command rule. +pub const SET_THEME_RULE_NAME: &str = "cmd/set_theme"; +/// Human-readable name for the toggle nav command rule. +pub const TOGGLE_NAV_RULE_NAME: &str = "cmd/toggle_nav"; +/// Human-readable name for the toast command rule. +pub const TOAST_RULE_NAME: &str = "cmd/toast"; +/// Human-readable name for the drop ball command rule. +pub const DROP_BALL_RULE_NAME: &str = "cmd/drop_ball"; +/// Human-readable name for the combined ball physics rule. +pub const BALL_PHYSICS_RULE_NAME: &str = "physics/ball"; +/// Human-readable name for the test-only KV rule. +#[cfg(feature = "dind_ops")] +pub const PUT_KV_RULE_NAME: &str = "cmd/put_kv"; + +/// Constructs the `cmd/route_push` rewrite rule. +#[must_use] +pub fn route_push_rule() -> RewriteRule { + let id: Hash = make_type_id("rule:cmd/route_push").0; + RewriteRule { + id, + name: ROUTE_PUSH_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: |s, scope| matcher_for_op(s, scope, ops::route_push::OP_ID), + executor: |s, scope| { + if let Some(args) = + decode_op_args::(s, scope, ops::route_push::decode_vars) + { + apply_route_push(s, args.path); + } + }, + compute_footprint: |s, scope| footprint_for_state_node(s, scope, "sim/state/routePath"), + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +/// Constructs the `cmd/set_theme` rewrite rule. +#[must_use] +pub fn set_theme_rule() -> RewriteRule { + let id: Hash = make_type_id("rule:cmd/set_theme").0; + RewriteRule { + id, + name: SET_THEME_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: |s, scope| matcher_for_op(s, scope, ops::set_theme::OP_ID), + executor: |s, scope| { + if let Some(args) = + decode_op_args::(s, scope, ops::set_theme::decode_vars) + { + apply_set_theme(s, args.mode); + } + }, + compute_footprint: |s, scope| footprint_for_state_node(s, scope, "sim/state/theme"), + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +/// Constructs the `cmd/toggle_nav` rewrite rule. +#[must_use] +pub fn toggle_nav_rule() -> RewriteRule { + let id: Hash = make_type_id("rule:cmd/toggle_nav").0; + RewriteRule { + id, + name: TOGGLE_NAV_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: |s, scope| matcher_for_op(s, scope, ops::toggle_nav::OP_ID), + executor: |s, _scope| { + apply_toggle_nav(s); + }, + compute_footprint: |s, scope| footprint_for_state_node(s, scope, "sim/state/navOpen"), + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +/// Constructs the `cmd/toast` rewrite rule. +#[must_use] +pub fn toast_rule() -> RewriteRule { + let id: Hash = make_type_id("rule:cmd/toast").0; + RewriteRule { + id, + name: TOAST_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: |s, scope| matcher_for_op(s, scope, ops::toast::OP_ID), + executor: |s, scope| { + if let Some(args) = + decode_op_args::(s, scope, ops::toast::decode_vars) + { + emit_view_op(s, TYPEID_VIEW_OP_SHOWTOAST, args.message.as_bytes()); + } + }, + compute_footprint: |s, scope| footprint_for_state_node(s, scope, "sim/view"), + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +/// Constructs the `cmd/drop_ball` rewrite rule. +#[must_use] +pub fn drop_ball_rule() -> RewriteRule { + let id: Hash = make_type_id("rule:cmd/drop_ball").0; + RewriteRule { + id, + name: DROP_BALL_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: |s, scope| matcher_for_op(s, scope, ops::drop_ball::OP_ID), + executor: |s, _scope| { + let ball_id = make_node_id("ball"); + // Q32.32 fixed-point: 1 unit = 1 << 32 + // Initial: y=400 units, downward velocity 5 units/tick + let pos = [0, BALL_INITIAL_HEIGHT * FIXED_POINT_SCALE, 0]; + let vel = [0, -BALL_INITIAL_VELOCITY * FIXED_POINT_SCALE, 0]; + let payload = MotionV2Builder::new(pos, vel).into_bytes(); + let atom = AtomPayload::new(TYPEID_PAYLOAD_MOTION_V2, payload); + s.insert_node( + ball_id, + NodeRecord { + ty: make_type_id("entity"), + }, + ); + s.set_node_attachment(ball_id, Some(AttachmentValue::Atom(atom))); + }, + compute_footprint: |s, scope| { + let mut fp = footprint_for_state_node(s, scope, "ball"); + fp.n_write.insert_node(&make_node_id("ball")); + fp + }, + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +/// Constructs the `physics/ball` rewrite rule (Semi-implicit Euler). +#[must_use] +pub fn ball_physics_rule() -> RewriteRule { + let id: Hash = make_type_id("rule:physics/ball").0; + RewriteRule { + id, + name: BALL_PHYSICS_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: |s, scope| { + if *scope != make_node_id("ball") { + return false; + } + if let Some(m) = MotionV2View::try_from_node(s, scope) { + let has_velocity = m.vel_raw().iter().any(|&v| v != 0); + let not_at_rest = m.pos_raw()[1] != 0; + return has_velocity || not_at_rest; + } + false + }, + executor: |s, scope| { + if let Some(m) = MotionV2View::try_from_node(s, scope) { + let mut pos = m.pos_raw(); + let mut vel = m.vel_raw(); + + // Apply gravity (semi-implicit Euler) while airborne + if pos[1] > 0 { + vel[1] -= GRAVITY_ACCEL * FIXED_POINT_SCALE; + } + pos[1] += vel[1]; + if pos[1] <= 0 { + pos[1] = 0; + vel = [0, 0, 0]; + } + + let out = MotionV2Builder::new(pos, vel).into_bytes(); + if let Some(AttachmentValue::Atom(atom)) = s.node_attachment_mut(scope) { + atom.bytes = out; + } + } + }, + compute_footprint: |s, scope| { + let mut a_write = AttachmentSet::default(); + a_write.insert(AttachmentKey::node_alpha(NodeKey { + warp_id: s.warp_id(), + local_id: *scope, + })); + Footprint { + a_write, + ..Default::default() + } + }, + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +/// Constructs the `cmd/put_kv` rewrite rule (Test only). +// TODO: Double-decoding desync risk - decode_op_args is called in both executor +// and compute_footprint. If the decoder has side effects or if the attachment +// changes between calls, this could lead to inconsistent behavior. +#[cfg(feature = "dind_ops")] +#[must_use] +pub fn put_kv_rule() -> RewriteRule { + let id: Hash = make_type_id("rule:cmd/put_kv").0; + RewriteRule { + id, + name: PUT_KV_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: |s, scope| matcher_for_op(s, scope, ops::put_kv::OP_ID), + executor: |s, scope| { + if let Some(args) = + decode_op_args::(s, scope, ops::put_kv::decode_vars) + { + apply_put_kv(s, args.key, args.value); + } + }, + compute_footprint: |s, scope| { + if let Some(args) = + decode_op_args::(s, scope, ops::put_kv::decode_vars) + { + footprint_for_state_node(s, scope, &format!("sim/state/kv/{}", args.key)) + } else { + Footprint::default() + } + }, + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +// --- Helpers --- + +fn matcher_for_op(store: &GraphStore, scope: &NodeId, expected_op_id: u32) -> bool { + let Some(AttachmentValue::Atom(a)) = store.node_attachment(scope) else { + return false; + }; + if let Ok((op_id, _)) = unpack_intent_v1(&a.bytes) { + return op_id == expected_op_id; + } + false +} + +fn decode_op_args( + store: &GraphStore, + scope: &NodeId, + decode_fn: fn(&[u8]) -> Option, +) -> Option { + let AttachmentValue::Atom(a) = store.node_attachment(scope)? else { + return None; + }; + let (_, vars) = unpack_intent_v1(&a.bytes).ok()?; + decode_fn(vars) +} + +impl<'a> MotionV2View<'a> { + /// Attempt to construct a motion v2 view from a node's attachment. + pub fn try_from_node(store: &'a GraphStore, node: &NodeId) -> Option { + let AttachmentValue::Atom(p) = store.node_attachment(node)? else { + return None; + }; + Self::try_from_payload(p) + } +} + +/// Compute the footprint for a state node operation. +pub fn footprint_for_state_node( + store: &GraphStore, + scope: &NodeId, + state_node_path: &str, +) -> Footprint { + let mut n_read = IdSet::default(); + let mut n_write = IdSet::default(); + let mut e_write = IdSet::default(); + let mut a_read = AttachmentSet::default(); + let mut a_write = AttachmentSet::default(); + + n_read.insert_node(scope); + a_read.insert(AttachmentKey::node_alpha(NodeKey { + warp_id: store.warp_id(), + local_id: *scope, + })); + + let sim_id = make_node_id("sim"); + let sim_state_id = make_node_id("sim/state"); + let target_id = make_node_id(state_node_path); + + n_write.insert_node(&sim_id); + n_write.insert_node(&sim_state_id); + n_write.insert_node(&target_id); + + e_write.insert_edge(&make_edge_id("edge:sim/state")); + e_write.insert_edge(&make_edge_id(&format!("edge:{state_node_path}"))); + + a_write.insert(AttachmentKey::node_alpha(NodeKey { + warp_id: store.warp_id(), + local_id: target_id, + })); + + Footprint { + n_read, + n_write, + e_write, + a_read, + a_write, + ..Default::default() + } +} + +/// Ensure the sim and sim/state base nodes exist, returning their IDs. +pub fn ensure_state_base(store: &mut GraphStore) -> (NodeId, NodeId) { + let sim_id = make_node_id("sim"); + let sim_state_id = make_node_id("sim/state"); + store.insert_node( + sim_id, + NodeRecord { + ty: make_type_id("sim"), + }, + ); + store.insert_node( + sim_state_id, + NodeRecord { + ty: make_type_id("sim/state"), + }, + ); + store.insert_edge( + sim_id, + EdgeRecord { + id: make_edge_id("edge:sim/state"), + from: sim_id, + to: sim_state_id, + ty: make_type_id("edge:state"), + }, + ); + (sim_id, sim_state_id) +} + +/// Apply a route push operation to update the current route path. +pub fn apply_route_push(store: &mut GraphStore, path: String) { + let (_, sim_state_id) = ensure_state_base(store); + let id = make_node_id("sim/state/routePath"); + store.insert_node( + id, + NodeRecord { + ty: make_type_id("sim/state/routePath"), + }, + ); + store.insert_edge( + sim_state_id, + EdgeRecord { + id: make_edge_id("edge:sim/state/routePath"), + from: sim_state_id, + to: id, + ty: make_type_id("edge:routePath"), + }, + ); + + let b = path.as_bytes(); + let mut out = Vec::with_capacity(4 + b.len()); + out.extend_from_slice(&(b.len() as u32).to_le_bytes()); + out.extend_from_slice(b); + store.set_node_attachment( + id, + Some(AttachmentValue::Atom(AtomPayload::new( + TYPEID_STATE_ROUTE_PATH, + out.into(), + ))), + ); +} + +/// Apply a set theme operation to update the current theme. +pub fn apply_set_theme(store: &mut GraphStore, mode: crate::generated::codecs::Theme) { + let (_, sim_state_id) = ensure_state_base(store); + let id = make_node_id("sim/state/theme"); + store.insert_node( + id, + NodeRecord { + ty: make_type_id("sim/state/theme"), + }, + ); + store.insert_edge( + sim_state_id, + EdgeRecord { + id: make_edge_id("edge:sim/state/theme"), + from: sim_state_id, + to: id, + ty: make_type_id("edge:theme"), + }, + ); + store.set_node_attachment( + id, + Some(AttachmentValue::Atom(AtomPayload::new( + TYPEID_STATE_THEME, + (mode as u16).to_le_bytes().to_vec().into(), + ))), + ); +} + +/// Apply a toggle nav operation to toggle the navigation state. +pub fn apply_toggle_nav(store: &mut GraphStore) { + let (_, sim_state_id) = ensure_state_base(store); + let id = make_node_id("sim/state/navOpen"); + store.insert_node( + id, + NodeRecord { + ty: make_type_id("sim/state/navOpen"), + }, + ); + store.insert_edge( + sim_state_id, + EdgeRecord { + id: make_edge_id("edge:sim/state/navOpen"), + from: sim_state_id, + to: id, + ty: make_type_id("edge:navOpen"), + }, + ); + + let current_val = match store.node_attachment(&id) { + Some(AttachmentValue::Atom(a)) if !a.bytes.is_empty() && a.bytes[0] == 1 => 1u8, + _ => 0u8, + }; + let next_val = if current_val == 1 { 0u8 } else { 1u8 }; + + store.set_node_attachment( + id, + Some(AttachmentValue::Atom(AtomPayload::new( + TYPEID_STATE_NAV_OPEN, + bytes::Bytes::copy_from_slice(&[next_val]), + ))), + ); +} + +/// Apply a put KV operation to store a key-value pair. +#[cfg(feature = "dind_ops")] +pub fn apply_put_kv(store: &mut GraphStore, key: String, value: String) { + let (_, sim_state_id) = ensure_state_base(store); + let node_label = format!("sim/state/kv/{}", key); + let id = make_node_id(&node_label); + + store.insert_node( + id, + NodeRecord { + ty: make_type_id("sim/state/kv"), + }, + ); + + let edge_label = format!("edge:sim/state/kv/{}", key); + store.insert_edge( + sim_state_id, + EdgeRecord { + id: make_edge_id(&edge_label), + from: sim_state_id, + to: id, + ty: make_type_id("edge:kv"), + }, + ); + + let b = value.as_bytes(); + let mut out = Vec::with_capacity(4 + b.len()); + out.extend_from_slice(&(b.len() as u32).to_le_bytes()); + out.extend_from_slice(b); + store.set_node_attachment( + id, + Some(AttachmentValue::Atom(AtomPayload::new( + TYPEID_STATE_KV, + out.into(), + ))), + ); +} + +/// Emit a view operation with the given type and payload. +pub fn emit_view_op(store: &mut GraphStore, type_id: TypeId, payload: &[u8]) { + let view_id = make_node_id("sim/view"); + store.insert_node( + view_id, + NodeRecord { + ty: make_type_id("sim/view"), + }, + ); + let seq_key = make_type_id("sim/view/seq"); + let seq = store + .node_attachment(&view_id) + .and_then(|v| match v { + AttachmentValue::Atom(a) if a.type_id == seq_key => { + if a.bytes.len() < 8 { + return None; + } + let mut b = [0u8; 8]; + b.copy_from_slice(&a.bytes[..8]); + Some(u64::from_le_bytes(b)) + } + _ => None, + }) + .unwrap_or(0); + let op_id = make_node_id(&format!("sim/view/op:{:016}", seq)); + store.insert_node( + op_id, + NodeRecord { + ty: make_type_id(TYPE_VIEW_OP), + }, + ); + store.insert_edge( + view_id, + EdgeRecord { + id: make_edge_id(&format!("edge:view/op:{:016}", seq)), + from: view_id, + to: op_id, + ty: make_type_id("edge:view/op"), + }, + ); + store.set_node_attachment( + op_id, + Some(AttachmentValue::Atom(AtomPayload::new( + type_id, + bytes::Bytes::copy_from_slice(payload), + ))), + ); + store.set_node_attachment( + view_id, + Some(AttachmentValue::Atom(AtomPayload::new( + seq_key, + bytes::Bytes::copy_from_slice(&(seq + 1).to_le_bytes()), + ))), + ); +} + +/// Helper to extract attachment bytes from a state node without cloning. +fn get_state_bytes(store: &GraphStore, state_path: &str) -> Option { + match store.node_attachment(&make_node_id(state_path))? { + AttachmentValue::Atom(a) => Some(a.bytes.clone()), + _ => None, + } +} + +/// Project the current state to view operations. +/// +/// This function reads the stored attachment bytes from state nodes and emits +/// them as view operations. The payload format is consistent between live +/// execution (e.g., `apply_route_push`, `apply_set_theme`) and replay: +/// +/// - Live execution stores processed values as attachments +/// - `project_state` reads those same bytes and emits them as view ops +/// +/// This ensures time-travel sync delivers the same payload shape as the UI expects. +/// +/// We gather all bytes first (borrowing store immutably), then emit view ops +/// (requiring mutable store). bytes::Bytes clone is O(1) ref-count increment. +pub fn project_state(store: &mut GraphStore) { + // Borrow bytes directly - gather all state before mutating + let theme_bytes = get_state_bytes(store, "sim/state/theme"); + let nav_bytes = get_state_bytes(store, "sim/state/navOpen"); + let route_bytes = get_state_bytes(store, "sim/state/routePath"); + + // Now emit view ops with mutable store access + if let Some(b) = theme_bytes { + emit_view_op(store, TYPEID_VIEW_OP_SETTHEME, &b); + } + if let Some(b) = nav_bytes { + emit_view_op(store, TYPEID_VIEW_OP_TOGGLENAV, &b); + } + if let Some(b) = route_bytes { + emit_view_op(store, TYPEID_VIEW_OP_ROUTEPUSH, &b); + } +} diff --git a/crates/echo-dry-tests/Cargo.toml b/crates/echo-dry-tests/Cargo.toml new file mode 100644 index 00000000..c4b2ff2f --- /dev/null +++ b/crates/echo-dry-tests/Cargo.toml @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS + +[package] +name = "echo-dry-tests" +version = "0.1.0" +edition = "2021" +license.workspace = true +repository.workspace = true +rust-version.workspace = true +description = "Shared test doubles and fixtures for Echo crates" +readme = "README.md" +keywords = ["echo", "testing", "fixtures"] +categories = ["development-tools::testing"] + +[dependencies] +# Core crates for test doubles +warp-core = { workspace = true } +echo-graph = { workspace = true } +echo-app-core = { workspace = true } + +# Hashing +blake3 = "1" + +# Serialization (for config store fake) +serde = { version = "1", features = ["derive"] } + +# Bytes for payload construction +bytes = "1" + +[dev-dependencies] +serde_json = "1" + +[features] +default = [] +# Scalar backend feature passthrough (matches warp-core) +det_float = ["warp-core/det_float"] +det_fixed = ["warp-core/det_fixed"] diff --git a/crates/echo-dry-tests/README.md b/crates/echo-dry-tests/README.md new file mode 100644 index 00000000..b22f46ce --- /dev/null +++ b/crates/echo-dry-tests/README.md @@ -0,0 +1,107 @@ + + + +# echo-dry-tests + +Shared test doubles and fixtures for Echo crates. + +## Purpose + +This crate provides reusable test utilities including: + +- In-memory configuration store for testing without filesystem +- Demo rules (motion, port) for integration tests +- Engine and GraphStore builder utilities +- WarpSnapshot and WarpDiff builders +- Hash ID generation helpers +- Motion payload encoding helpers +- Synthetic rule builders (noop matchers/executors) + +## Public API + +### Config Store + +- **`InMemoryConfigStore`** - An in-memory configuration store implementing the + config trait, useful for testing without touching the filesystem. + +### Demo Rules + +- **`MOTION_RULE_NAME`** - Constant string `"motion/update"` identifying the + built-in motion update rule. +- **`motion_rule()`** - Returns a `RewriteRule` that updates entity positions + based on velocity (position += velocity each tick). +- **`build_motion_demo_engine()`** - Constructs a demo `Engine` with a + world-root node and motion rule pre-registered. +- **`PORT_RULE_NAME`** - Constant string `"demo/port_nop"` identifying the port + demo rule. +- **`port_rule()`** - Returns a demo `RewriteRule` that reserves a boundary + input port. +- **`build_port_demo_engine()`** - Builds an engine with a world root for + port-rule tests. + +### Engine Builders + +- **`EngineTestBuilder`** - Fluent builder for constructing test engines with + custom configuration. +- **`build_engine_with_root(name)`** - Quickly build an engine with a named root + node. +- **`build_engine_with_typed_root(name, type_name)`** - Build an engine with a + named and typed root node. + +### Frame Builders + +- **`SnapshotBuilder`** - Builder for constructing `WarpSnapshot` test fixtures. +- **`DiffBuilder`** - Builder for constructing `WarpDiff` test fixtures. + +### Hash Helpers + +- **`make_rule_id(name)`** - Generate a deterministic rule ID hash from a name. +- **`make_intent_id(name)`** - Generate a deterministic intent ID hash from a + name. + +### Motion Helpers + +- **`MotionPayloadBuilder`** - Builder for constructing motion payloads. +- **`DEFAULT_MOTION_POSITION`** - Default position `[0.0, 0.0, 0.0]` for motion + payloads. +- **`DEFAULT_MOTION_VELOCITY`** - Default velocity `[0.0, 0.0, 0.0]` for motion + payloads. + +### Synthetic Rules + +- **`NoOpRule`** - A rule that matches everything but does nothing, useful for + testing rule registration and scheduling. +- **`SyntheticRuleBuilder`** - Builder for creating custom synthetic rules with + configurable matchers and executors. + +## Usage + +Add as a dev-dependency in your crate's `Cargo.toml`: + +```toml +[dev-dependencies] +echo-dry-tests = { workspace = true } +``` + +### Example: Using the Motion Rule + +```rust +use echo_dry_tests::{motion_rule, MOTION_RULE_NAME}; +use warp_core::Engine; + +// Create an engine and register the motion rule +let mut engine = Engine::default(); +engine.register_rule(motion_rule()).unwrap(); + +// The rule is now registered under MOTION_RULE_NAME ("motion/update") +// and will update position += velocity for matching nodes each tick. +``` + +### Example: Quick Engine Setup + +```rust +use echo_dry_tests::build_motion_demo_engine; + +// Get a fully configured engine with world-root and motion rule +let engine = build_motion_demo_engine(); +``` diff --git a/crates/echo-dry-tests/src/config.rs b/crates/echo-dry-tests/src/config.rs new file mode 100644 index 00000000..4fa9f261 --- /dev/null +++ b/crates/echo-dry-tests/src/config.rs @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! In-memory config store fake for testing without filesystem I/O. + +use echo_app_core::config::{ConfigError, ConfigStore}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +/// In-memory implementation of [`ConfigStore`] for testing. +/// +/// This fake allows tests to verify config save/load behavior without +/// touching the filesystem. It also tracks call counts for verification. +/// +/// # Example +/// +/// ``` +/// use echo_dry_tests::InMemoryConfigStore; +/// use echo_app_core::config::{ConfigService, ConfigStore}; +/// +/// let store = InMemoryConfigStore::new(); +/// let service = ConfigService::new(store.clone()); +/// +/// service.save("prefs", &serde_json::json!({"theme": "dark"})).unwrap(); +/// assert_eq!(store.load_count(), 0); +/// assert_eq!(store.save_count(), 1); +/// ``` +#[derive(Clone, Default)] +pub struct InMemoryConfigStore { + inner: Arc>, +} + +#[derive(Default)] +struct InMemoryConfigStoreInner { + data: HashMap>, + load_count: usize, + save_count: usize, + fail_on_load: bool, + fail_on_save: bool, +} + +impl InMemoryConfigStore { + /// Create a new empty in-memory config store. + pub fn new() -> Self { + Self::default() + } + + /// Create a store pre-populated with the given key-value pairs. + pub fn with_data(data: HashMap>) -> Self { + Self { + inner: Arc::new(Mutex::new(InMemoryConfigStoreInner { + data, + ..Default::default() + })), + } + } + + /// Configure the store to fail on load operations. + pub fn set_fail_on_load(&self, fail: bool) { + let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner()); + inner.fail_on_load = fail; + } + + /// Configure the store to fail on save operations. + pub fn set_fail_on_save(&self, fail: bool) { + let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner()); + inner.fail_on_save = fail; + } + + /// Get the number of times `load_raw` was called (attempted, not successful). + /// + /// This counter is incremented at the start of each `load_raw` call, + /// before any failure checks. It counts all attempts, including those + /// that fail due to `set_fail_on_load(true)` or missing keys. + pub fn load_count(&self) -> usize { + self.inner + .lock() + .unwrap_or_else(|e| e.into_inner()) + .load_count + } + + /// Get the number of times `save_raw` was called (attempted, not successful). + /// + /// This counter is incremented at the start of each `save_raw` call, + /// before any failure checks. It counts all attempts, including those + /// that fail due to `set_fail_on_save(true)`. + pub fn save_count(&self) -> usize { + self.inner + .lock() + .unwrap_or_else(|e| e.into_inner()) + .save_count + } + + /// Return all keys currently present in the store. + /// + /// This includes keys from any source: + /// - Keys added via `save_raw` calls + /// - Keys pre-populated via [`with_data()`](Self::with_data) + pub fn keys(&self) -> Vec { + self.inner + .lock() + .unwrap_or_else(|e| e.into_inner()) + .data + .keys() + .cloned() + .collect() + } + + /// Check if a key exists in the store. + pub fn contains_key(&self, key: &str) -> bool { + self.inner + .lock() + .unwrap_or_else(|e| e.into_inner()) + .data + .contains_key(key) + } + + /// Reset the store to its initial empty state. + /// + /// Clears all fields: + /// - `data`: All stored key-value pairs are removed + /// - `load_count`: Reset to 0 + /// - `save_count`: Reset to 0 + /// - `fail_on_load`: Reset to false + /// - `fail_on_save`: Reset to false + pub fn reset(&self) { + let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner()); + inner.data.clear(); + inner.load_count = 0; + inner.save_count = 0; + inner.fail_on_load = false; + inner.fail_on_save = false; + } +} + +impl ConfigStore for InMemoryConfigStore { + fn load_raw(&self, key: &str) -> Result, ConfigError> { + let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner()); + inner.load_count += 1; + + if inner.fail_on_load { + return Err(ConfigError::Other("simulated load failure".into())); + } + + inner.data.get(key).cloned().ok_or(ConfigError::NotFound) + } + + fn save_raw(&self, key: &str, data: &[u8]) -> Result<(), ConfigError> { + let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner()); + inner.save_count += 1; + + if inner.fail_on_save { + return Err(ConfigError::Other("simulated save failure".into())); + } + + inner.data.insert(key.to_string(), data.to_vec()); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip_save_load() { + let store = InMemoryConfigStore::new(); + store.save_raw("test", b"hello").unwrap(); + let loaded = store.load_raw("test").unwrap(); + assert_eq!(loaded, b"hello"); + assert_eq!(store.save_count(), 1); + assert_eq!(store.load_count(), 1); + } + + #[test] + fn load_missing_key_returns_not_found() { + let store = InMemoryConfigStore::new(); + let result = store.load_raw("missing"); + assert!(matches!(result, Err(ConfigError::NotFound))); + } + + #[test] + fn fail_on_load_returns_error() { + let store = InMemoryConfigStore::new(); + store.save_raw("test", b"data").unwrap(); + store.set_fail_on_load(true); + let result = store.load_raw("test"); + assert!(matches!(result, Err(ConfigError::Other(_)))); + } + + #[test] + fn fail_on_save_returns_error() { + let store = InMemoryConfigStore::new(); + store.set_fail_on_save(true); + let result = store.save_raw("test", b"data"); + assert!(matches!(result, Err(ConfigError::Other(_)))); + } + + #[test] + fn with_data_prepopulates_store() { + let mut initial_data = HashMap::new(); + initial_data.insert("key1".to_string(), b"value1".to_vec()); + initial_data.insert("key2".to_string(), b"value2".to_vec()); + + let store = InMemoryConfigStore::with_data(initial_data); + + assert_eq!(store.load_raw("key1").unwrap(), b"value1"); + assert_eq!(store.load_raw("key2").unwrap(), b"value2"); + // load_count should reflect the loads we just did + assert_eq!(store.load_count(), 2); + // save_count should be 0 since we only used with_data + assert_eq!(store.save_count(), 0); + } + + #[test] + fn with_data_empty_hashmap_creates_empty_store() { + let store = InMemoryConfigStore::with_data(HashMap::new()); + + assert!(store.keys().is_empty()); + let result = store.load_raw("any_key"); + assert!(matches!(result, Err(ConfigError::NotFound))); + } + + #[test] + fn keys_returns_all_stored_keys() { + let store = InMemoryConfigStore::new(); + store.save_raw("alpha", b"a").unwrap(); + store.save_raw("beta", b"b").unwrap(); + store.save_raw("gamma", b"c").unwrap(); + + let mut keys = store.keys(); + keys.sort(); + assert_eq!(keys, vec!["alpha", "beta", "gamma"]); + } + + #[test] + fn keys_returns_empty_vec_for_empty_store() { + let store = InMemoryConfigStore::new(); + assert!(store.keys().is_empty()); + } + + #[test] + fn contains_key_returns_true_for_existing_key() { + let store = InMemoryConfigStore::new(); + store.save_raw("exists", b"data").unwrap(); + + assert!(store.contains_key("exists")); + } + + #[test] + fn contains_key_returns_false_for_missing_key() { + let store = InMemoryConfigStore::new(); + store.save_raw("exists", b"data").unwrap(); + + assert!(!store.contains_key("does_not_exist")); + } + + #[test] + fn contains_key_returns_false_for_empty_store() { + let store = InMemoryConfigStore::new(); + assert!(!store.contains_key("any_key")); + } + + #[test] + fn reset_clears_all_data() { + let store = InMemoryConfigStore::new(); + store.save_raw("key1", b"value1").unwrap(); + store.save_raw("key2", b"value2").unwrap(); + + store.reset(); + + assert!(store.keys().is_empty()); + assert!(!store.contains_key("key1")); + assert!(!store.contains_key("key2")); + } + + #[test] + fn reset_clears_load_and_save_counts() { + let store = InMemoryConfigStore::new(); + store.save_raw("key", b"value").unwrap(); + let _ = store.load_raw("key"); + let _ = store.load_raw("key"); + + assert_eq!(store.save_count(), 1); + assert_eq!(store.load_count(), 2); + + store.reset(); + + assert_eq!(store.save_count(), 0); + assert_eq!(store.load_count(), 0); + } + + #[test] + fn reset_clears_fail_flags() { + let store = InMemoryConfigStore::new(); + store.set_fail_on_load(true); + store.set_fail_on_save(true); + + store.reset(); + + // After reset, operations should succeed + store.save_raw("key", b"value").unwrap(); + let result = store.load_raw("key"); + assert!(result.is_ok()); + } + + #[test] + fn clone_shares_state_between_instances() { + let store1 = InMemoryConfigStore::new(); + let store2 = store1.clone(); + + // Save through store1 + store1.save_raw("shared_key", b"shared_value").unwrap(); + + // Load through store2 - should see the same data + let loaded = store2.load_raw("shared_key").unwrap(); + assert_eq!(loaded, b"shared_value"); + + // Counts should be shared + assert_eq!(store1.save_count(), 1); + assert_eq!(store2.save_count(), 1); + assert_eq!(store1.load_count(), 1); + assert_eq!(store2.load_count(), 1); + } + + #[test] + fn clone_shares_fail_flags() { + let store1 = InMemoryConfigStore::new(); + let store2 = store1.clone(); + + // Set fail_on_save through store1 + store1.set_fail_on_save(true); + + // store2 should also fail on save + let result = store2.save_raw("key", b"value"); + assert!(matches!(result, Err(ConfigError::Other(_)))); + + // Set fail_on_load through store2 + store2.set_fail_on_load(true); + + // store1 should also fail on load + let result = store1.load_raw("any"); + assert!(matches!(result, Err(ConfigError::Other(_)))); + } + + #[test] + fn clone_shares_reset() { + let store1 = InMemoryConfigStore::new(); + let store2 = store1.clone(); + + store1.save_raw("key", b"value").unwrap(); + store1.set_fail_on_save(true); + + // Reset through store2 + store2.reset(); + + // store1 should see the reset state + assert!(store1.keys().is_empty()); + assert_eq!(store1.save_count(), 0); + // fail_on_save should be cleared, so this should succeed + store1.save_raw("new_key", b"new_value").unwrap(); + } + + #[test] + fn fail_on_save_still_increments_save_count() { + let store = InMemoryConfigStore::new(); + store.set_fail_on_save(true); + + let _ = store.save_raw("key", b"value"); + let _ = store.save_raw("key2", b"value2"); + + // Even failed saves should increment the count + assert_eq!(store.save_count(), 2); + } + + #[test] + fn fail_on_load_still_increments_load_count() { + let store = InMemoryConfigStore::new(); + store.set_fail_on_load(true); + + let _ = store.load_raw("key"); + let _ = store.load_raw("key2"); + + // Even failed loads should increment the count + assert_eq!(store.load_count(), 2); + } + + #[test] + fn fail_on_save_does_not_store_data() { + let store = InMemoryConfigStore::new(); + store.set_fail_on_save(true); + + let _ = store.save_raw("key", b"value"); + + // Data should not be stored when save fails + assert!(!store.contains_key("key")); + assert!(store.keys().is_empty()); + } + + #[test] + fn fail_on_save_can_be_toggled() { + let store = InMemoryConfigStore::new(); + + // Initially should work + store.save_raw("key1", b"value1").unwrap(); + + // Enable failure + store.set_fail_on_save(true); + let result = store.save_raw("key2", b"value2"); + assert!(result.is_err()); + + // Disable failure + store.set_fail_on_save(false); + store.save_raw("key3", b"value3").unwrap(); + + // Only key1 and key3 should exist + assert!(store.contains_key("key1")); + assert!(!store.contains_key("key2")); + assert!(store.contains_key("key3")); + } + + #[test] + fn fail_on_load_can_be_toggled() { + let store = InMemoryConfigStore::new(); + store.save_raw("key", b"value").unwrap(); + + // Initially should work + assert!(store.load_raw("key").is_ok()); + + // Enable failure + store.set_fail_on_load(true); + assert!(store.load_raw("key").is_err()); + + // Disable failure + store.set_fail_on_load(false); + assert!(store.load_raw("key").is_ok()); + } +} diff --git a/crates/echo-dry-tests/src/demo_rules.rs b/crates/echo-dry-tests/src/demo_rules.rs new file mode 100644 index 00000000..efd2a143 --- /dev/null +++ b/crates/echo-dry-tests/src/demo_rules.rs @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Demo rules for testing: motion update and port reservation. +//! +//! These rules were previously in warp-demo-kit but are now test-only utilities. + +use warp_core::{ + decode_motion_atom_payload_q32_32, decode_motion_payload, encode_motion_atom_payload, + encode_motion_payload, encode_motion_payload_q32_32, make_node_id, + make_type_id, motion_payload_type_id, pack_port_key, AttachmentKey, AttachmentSet, + AttachmentValue, ConflictPolicy, Engine, Footprint, GraphStore, Hash, IdSet, NodeId, NodeKey, + NodeRecord, PatternGraph, PortSet, RewriteRule, +}; + +// ============================================================================= +// Motion Rule +// ============================================================================= + +/// Rule name constant for the built-in motion update rule. +pub const MOTION_RULE_NAME: &str = "motion/update"; + +#[cfg(feature = "det_fixed")] +mod motion_scalar_backend { + use warp_core::math::scalar::DFix64; + + pub(super) type MotionScalar = DFix64; + + pub(super) fn scalar_from_raw(raw: i64) -> MotionScalar { + MotionScalar::from_raw(raw) + } + + pub(super) fn scalar_to_raw(value: MotionScalar) -> i64 { + value.raw() + } +} + +#[cfg(not(feature = "det_fixed"))] +mod motion_scalar_backend { + use warp_core::math::fixed_q32_32; + use warp_core::math::scalar::F32Scalar; + use warp_core::math::Scalar; + + pub(super) type MotionScalar = F32Scalar; + + pub(super) fn scalar_from_raw(raw: i64) -> MotionScalar { + MotionScalar::from_f32(fixed_q32_32::to_f32(raw)) + } + + pub(super) fn scalar_to_raw(value: MotionScalar) -> i64 { + fixed_q32_32::from_f32(value.to_f32()) + } +} + +use motion_scalar_backend::{scalar_from_raw, scalar_to_raw}; + +fn motion_executor(store: &mut GraphStore, scope: &NodeId) { + if store.node(scope).is_none() { + return; + } + let Some(AttachmentValue::Atom(payload)) = store.node_attachment_mut(scope) else { + return; + }; + + let Some((pos_raw, vel_raw)) = decode_motion_atom_payload_q32_32(payload) else { + return; + }; + + let mut pos = [ + scalar_from_raw(pos_raw[0]), + scalar_from_raw(pos_raw[1]), + scalar_from_raw(pos_raw[2]), + ]; + let vel = [ + scalar_from_raw(vel_raw[0]), + scalar_from_raw(vel_raw[1]), + scalar_from_raw(vel_raw[2]), + ]; + + for i in 0..3 { + pos[i] = pos[i] + vel[i]; + } + + let new_pos_raw = [ + scalar_to_raw(pos[0]), + scalar_to_raw(pos[1]), + scalar_to_raw(pos[2]), + ]; + let vel_out_raw = [ + scalar_to_raw(vel[0]), + scalar_to_raw(vel[1]), + scalar_to_raw(vel[2]), + ]; + + payload.type_id = motion_payload_type_id(); + payload.bytes = encode_motion_payload_q32_32(new_pos_raw, vel_out_raw); +} + +fn motion_matcher(store: &GraphStore, scope: &NodeId) -> bool { + matches!( + store.node_attachment(scope), + Some(AttachmentValue::Atom(payload)) if decode_motion_atom_payload_q32_32(payload).is_some() + ) +} + +fn motion_rule_id() -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"rule:"); + hasher.update(MOTION_RULE_NAME.as_bytes()); + hasher.finalize().into() +} + +fn compute_motion_footprint(store: &GraphStore, scope: &NodeId) -> Footprint { + let mut a_write = AttachmentSet::default(); + if store.node(scope).is_some() { + a_write.insert(AttachmentKey::node_alpha(NodeKey { + warp_id: store.warp_id(), + local_id: *scope, + })); + } + Footprint { + n_read: IdSet::default(), + n_write: IdSet::default(), + e_read: IdSet::default(), + e_write: IdSet::default(), + a_read: AttachmentSet::default(), + a_write, + b_in: PortSet::default(), + b_out: PortSet::default(), + factor_mask: 0, + } +} + +/// Returns a rewrite rule that updates entity positions based on velocity. +#[must_use] +pub fn motion_rule() -> RewriteRule { + RewriteRule { + id: motion_rule_id(), + name: MOTION_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: motion_matcher, + executor: motion_executor, + compute_footprint: compute_motion_footprint, + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +/// Constructs a demo Engine with a world-root node and motion rule pre-registered. +#[must_use] +#[allow(clippy::expect_used)] +pub fn build_motion_demo_engine() -> Engine { + let mut store = GraphStore::default(); + let root_id = make_node_id("world-root"); + let root_type = make_type_id("world"); + store.insert_node(root_id, NodeRecord { ty: root_type }); + + let mut engine = Engine::new(store, root_id); + engine + .register_rule(motion_rule()) + .expect("motion rule should register successfully in fresh engine"); + engine +} + +// ============================================================================= +// Port Rule +// ============================================================================= + +/// Public identifier for the port demo rule. +pub const PORT_RULE_NAME: &str = "demo/port_nop"; + +fn port_matcher(_: &GraphStore, _: &NodeId) -> bool { + true +} + +fn port_executor(store: &mut GraphStore, scope: &NodeId) { + if store.node(scope).is_none() { + return; + } + + let Some(attachment) = store.node_attachment_mut(scope) else { + let pos = [1.0, 0.0, 0.0]; + let vel = [0.0, 0.0, 0.0]; + store.set_node_attachment( + *scope, + Some(AttachmentValue::Atom(encode_motion_atom_payload(pos, vel))), + ); + return; + }; + + let AttachmentValue::Atom(payload) = attachment else { + return; + }; + if payload.type_id != motion_payload_type_id() { + return; + } + if let Some((mut pos, vel)) = decode_motion_payload(&payload.bytes) { + pos[0] += 1.0; + payload.bytes = encode_motion_payload(pos, vel); + } +} + +fn compute_port_footprint(store: &GraphStore, scope: &NodeId) -> Footprint { + let mut n_write = IdSet::default(); + let mut a_write = AttachmentSet::default(); + let mut b_in = PortSet::default(); + if store.node(scope).is_some() { + n_write.insert_node(scope); + a_write.insert(AttachmentKey::node_alpha(NodeKey { + warp_id: store.warp_id(), + local_id: *scope, + })); + b_in.insert(pack_port_key(scope, 0, true)); + } + Footprint { + n_read: IdSet::default(), + n_write, + e_read: IdSet::default(), + e_write: IdSet::default(), + a_read: AttachmentSet::default(), + a_write, + b_in, + b_out: PortSet::default(), + factor_mask: 0, + } +} + +/// Returns a demo rewrite rule that reserves a boundary input port. +#[must_use] +pub fn port_rule() -> RewriteRule { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"rule:"); + hasher.update(PORT_RULE_NAME.as_bytes()); + let id: Hash = hasher.finalize().into(); + RewriteRule { + id, + name: PORT_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: port_matcher, + executor: port_executor, + compute_footprint: compute_port_footprint, + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +/// Builds an engine with a world root for port-rule tests. +#[must_use] +#[allow(clippy::expect_used)] +pub fn build_port_demo_engine() -> Engine { + let mut store = GraphStore::default(); + let root_id = make_node_id("world-root-ports"); + let root_type = make_type_id("world"); + store.insert_node(root_id, NodeRecord { ty: root_type }); + let mut engine = Engine::new(store, root_id); + engine + .register_rule(port_rule()) + .expect("port rule should register successfully in fresh engine"); + engine +} diff --git a/crates/echo-dry-tests/src/engine.rs b/crates/echo-dry-tests/src/engine.rs new file mode 100644 index 00000000..9932b10f --- /dev/null +++ b/crates/echo-dry-tests/src/engine.rs @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Engine and GraphStore builder utilities for tests. + +use warp_core::{make_node_id, make_type_id, Engine, GraphStore, NodeId, NodeRecord, RewriteRule}; + +/// Builder for creating test engines with common configurations. +/// +/// # Example +/// +/// ``` +/// use echo_dry_tests::EngineTestBuilder; +/// +/// let engine = EngineTestBuilder::new() +/// .with_root("my-root") +/// .build(); +/// ``` +pub struct EngineTestBuilder { + root_label: String, + root_type: String, + rules: Vec, +} + +impl Default for EngineTestBuilder { + fn default() -> Self { + Self::new() + } +} + +impl EngineTestBuilder { + /// Create a new builder with default settings. + pub fn new() -> Self { + Self { + root_label: "root".to_string(), + root_type: "root".to_string(), + rules: Vec::new(), + } + } + + /// Set the root node label (used to generate the node ID). + pub fn with_root(mut self, label: &str) -> Self { + self.root_label = label.to_string(); + self + } + + /// Set the root node type ID label. + pub fn with_root_type(mut self, type_label: &str) -> Self { + self.root_type = type_label.to_string(); + self + } + + /// Add a rule to be registered after engine creation. + pub fn with_rule(mut self, rule: RewriteRule) -> Self { + self.rules.push(rule); + self + } + + /// Add the standard motion rule. + pub fn with_motion_rule(self) -> Self { + self.with_rule(crate::demo_rules::motion_rule()) + } + + /// Add the dispatch inbox rule. + pub fn with_dispatch_inbox_rule(self) -> Self { + self.with_rule(warp_core::inbox::dispatch_inbox_rule()) + } + + /// Add the ack pending rule. + pub fn with_ack_pending_rule(self) -> Self { + self.with_rule(warp_core::inbox::ack_pending_rule()) + } + + /// Add standard inbox rules (dispatch + ack). + pub fn with_inbox_rules(self) -> Self { + self.with_dispatch_inbox_rule().with_ack_pending_rule() + } + + /// Build the engine with configured settings. + pub fn build(self) -> Engine { + let root = make_node_id(&self.root_label); + let mut store = GraphStore::default(); + store.insert_node( + root, + NodeRecord { + ty: make_type_id(&self.root_type), + }, + ); + let mut engine = Engine::new(store, root); + + for rule in self.rules { + engine.register_rule(rule).expect("register rule"); + } + + engine + } + + /// Build the engine and return both the engine and the root node ID. + pub fn build_with_root(self) -> (Engine, NodeId) { + let root = make_node_id(&self.root_label); + let mut store = GraphStore::default(); + store.insert_node( + root, + NodeRecord { + ty: make_type_id(&self.root_type), + }, + ); + let mut engine = Engine::new(store, root); + + for rule in self.rules { + engine.register_rule(rule).expect("register rule"); + } + + (engine, root) + } +} + +/// Creates an Engine with a default GraphStore and a single root node. +/// +/// This is a shorthand for the common pattern: +/// ``` +/// use echo_dry_tests::engine::build_engine_with_root; +/// use warp_core::make_node_id; +/// +/// let root = make_node_id("root"); +/// let engine = build_engine_with_root(root); +/// ``` +pub fn build_engine_with_root(root: NodeId) -> Engine { + let mut store = GraphStore::default(); + store.insert_node( + root, + NodeRecord { + ty: make_type_id("root"), + }, + ); + Engine::new(store, root) +} + +/// Create an engine with a root node and custom type. +pub fn build_engine_with_typed_root(root: NodeId, type_label: &str) -> Engine { + let mut store = GraphStore::default(); + store.insert_node( + root, + NodeRecord { + ty: make_type_id(type_label), + }, + ); + Engine::new(store, root) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builder_creates_engine_with_root() { + let engine = EngineTestBuilder::new().with_root("test-root").build(); + let store = engine.store_clone(); + let root = make_node_id("test-root"); + assert!(store.node(&root).is_some()); + } + + #[test] + fn builder_with_motion_rule_registers_rule() { + let engine = EngineTestBuilder::new().with_motion_rule().build(); + // Engine should have motion rule registered (no direct way to check, + // but it shouldn't panic during creation) + let _ = engine; + } + + #[test] + fn build_with_root_returns_both() { + let (engine, root) = EngineTestBuilder::new() + .with_root("my-root") + .build_with_root(); + let store = engine.store_clone(); + assert!(store.node(&root).is_some()); + } +} diff --git a/crates/echo-dry-tests/src/frames.rs b/crates/echo-dry-tests/src/frames.rs new file mode 100644 index 00000000..f886993c --- /dev/null +++ b/crates/echo-dry-tests/src/frames.rs @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! WarpSnapshot and WarpDiff builders for tests. + +use echo_graph::{ + EdgeData, EdgeKind, EpochId, Hash32, NodeData, NodeKind, RenderEdge, RenderGraph, RenderNode, + WarpDiff, WarpFrame, WarpOp, WarpSnapshot, +}; + +/// Builder for creating [`WarpSnapshot`] instances in tests. +/// +/// # Example +/// +/// ``` +/// use echo_dry_tests::SnapshotBuilder; +/// +/// let snap = SnapshotBuilder::new() +/// .epoch(5) +/// .with_node(1, vec![1, 2, 3]) +/// .build(); +/// +/// assert_eq!(snap.epoch, 5); +/// assert_eq!(snap.graph.nodes.len(), 1); +/// ``` +#[derive(Default)] +pub struct SnapshotBuilder { + epoch: EpochId, + graph: RenderGraph, + state_hash: Option, +} + +impl SnapshotBuilder { + /// Create a new builder with defaults (epoch 0, empty graph). + pub fn new() -> Self { + Self::default() + } + + /// Set the epoch ID. + pub fn epoch(mut self, epoch: EpochId) -> Self { + self.epoch = epoch; + self + } + + /// Set the full graph. + pub fn graph(mut self, graph: RenderGraph) -> Self { + self.graph = graph; + self + } + + /// Set the state hash. + pub fn state_hash(mut self, hash: Hash32) -> Self { + self.state_hash = Some(hash); + self + } + + /// Add a node with the given ID and raw data. + pub fn with_node(mut self, id: u64, raw: Vec) -> Self { + self.graph.nodes.push(RenderNode { + id, + kind: NodeKind::Generic, + data: NodeData { raw }, + }); + self + } + + /// Add an edge between two nodes. + pub fn with_edge(mut self, id: u64, src: u64, dst: u64) -> Self { + self.graph.edges.push(RenderEdge { + id, + src, + dst, + kind: EdgeKind::Generic, + data: EdgeData { raw: Vec::new() }, + }); + self + } + + /// Build the snapshot. + pub fn build(self) -> WarpSnapshot { + WarpSnapshot { + epoch: self.epoch, + graph: self.graph, + state_hash: self.state_hash, + } + } + + /// Build and wrap in a WarpFrame::Snapshot. + pub fn build_frame(self) -> WarpFrame { + WarpFrame::Snapshot(self.build()) + } +} + +/// Builder for creating [`WarpDiff`] instances in tests. +/// +/// # Example +/// +/// ``` +/// use echo_dry_tests::DiffBuilder; +/// +/// let diff = DiffBuilder::new(0, 1) +/// .with_add_node(1, vec![1, 2, 3]) +/// .build(); +/// +/// assert_eq!(diff.from_epoch, 0); +/// assert_eq!(diff.to_epoch, 1); +/// assert_eq!(diff.ops.len(), 1); +/// ``` +pub struct DiffBuilder { + from_epoch: EpochId, + to_epoch: EpochId, + ops: Vec, + state_hash: Option, +} + +impl DiffBuilder { + /// Create a new builder for a diff from `from_epoch` to `to_epoch`. + pub fn new(from_epoch: EpochId, to_epoch: EpochId) -> Self { + Self { + from_epoch, + to_epoch, + ops: Vec::new(), + state_hash: None, + } + } + + /// Create a sequential diff (from_epoch to from_epoch + 1). + pub fn sequential(from_epoch: EpochId) -> Self { + Self::new(from_epoch, from_epoch + 1) + } + + /// Set the state hash. + pub fn state_hash(mut self, hash: Hash32) -> Self { + self.state_hash = Some(hash); + self + } + + /// Add an AddNode operation. + pub fn with_add_node(mut self, id: u64, raw: Vec) -> Self { + self.ops.push(WarpOp::AddNode { + id, + kind: NodeKind::Generic, + data: NodeData { raw }, + }); + self + } + + /// Add a RemoveNode operation. + pub fn with_remove_node(mut self, id: u64) -> Self { + self.ops.push(WarpOp::RemoveNode { id }); + self + } + + /// Add an UpdateNode operation. + pub fn with_update_node(mut self, id: u64, raw: Vec) -> Self { + self.ops.push(WarpOp::UpdateNode { + id, + data: echo_graph::NodeDataPatch::Replace(NodeData { raw }), + }); + self + } + + /// Add an AddEdge operation. + pub fn with_add_edge(mut self, id: u64, src: u64, dst: u64) -> Self { + self.ops.push(WarpOp::AddEdge { + id, + src, + dst, + kind: EdgeKind::Generic, + data: EdgeData { raw: Vec::new() }, + }); + self + } + + /// Add a RemoveEdge operation. + pub fn with_remove_edge(mut self, id: u64) -> Self { + self.ops.push(WarpOp::RemoveEdge { id }); + self + } + + /// Add a raw WarpOp. + pub fn with_op(mut self, op: WarpOp) -> Self { + self.ops.push(op); + self + } + + /// Build the diff. + pub fn build(self) -> WarpDiff { + WarpDiff { + from_epoch: self.from_epoch, + to_epoch: self.to_epoch, + ops: self.ops, + state_hash: self.state_hash, + } + } + + /// Build and wrap in a WarpFrame::Diff. + pub fn build_frame(self) -> WarpFrame { + WarpFrame::Diff(self.build()) + } +} + +/// Create an empty snapshot at epoch 0 (common test fixture). +pub fn empty_snapshot() -> WarpSnapshot { + SnapshotBuilder::new().build() +} + +/// Create an empty snapshot at the given epoch. +pub fn empty_snapshot_at(epoch: EpochId) -> WarpSnapshot { + SnapshotBuilder::new().epoch(epoch).build() +} + +/// Create an empty diff from one epoch to the next. +pub fn empty_diff(from: EpochId, to: EpochId) -> WarpDiff { + DiffBuilder::new(from, to).build() +} + +/// Create a sequence of empty sequential diffs. +pub fn sequential_empty_diffs(start: EpochId, count: usize) -> Vec { + (0..count) + .map(|i| empty_diff(start + i as u64, start + i as u64 + 1)) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn snapshot_builder_defaults() { + let snap = SnapshotBuilder::new().build(); + assert_eq!(snap.epoch, 0); + assert!(snap.graph.nodes.is_empty()); + assert!(snap.graph.edges.is_empty()); + assert!(snap.state_hash.is_none()); + } + + #[test] + fn snapshot_builder_with_nodes_and_edges() { + let snap = SnapshotBuilder::new() + .epoch(5) + .with_node(1, vec![1, 2, 3]) + .with_node(2, vec![4, 5, 6]) + .with_edge(100, 1, 2) + .build(); + + assert_eq!(snap.epoch, 5); + assert_eq!(snap.graph.nodes.len(), 2); + assert_eq!(snap.graph.edges.len(), 1); + } + + #[test] + fn diff_builder_sequential() { + let diff = DiffBuilder::sequential(5).build(); + assert_eq!(diff.from_epoch, 5); + assert_eq!(diff.to_epoch, 6); + } + + #[test] + fn diff_builder_with_ops() { + let diff = DiffBuilder::new(0, 1) + .with_add_node(1, vec![]) + .with_remove_node(2) + .build(); + + assert_eq!(diff.ops.len(), 2); + } + + #[test] + fn sequential_empty_diffs_creates_correct_sequence() { + let diffs = sequential_empty_diffs(5, 3); + assert_eq!(diffs.len(), 3); + assert_eq!(diffs[0].from_epoch, 5); + assert_eq!(diffs[0].to_epoch, 6); + assert_eq!(diffs[1].from_epoch, 6); + assert_eq!(diffs[1].to_epoch, 7); + assert_eq!(diffs[2].from_epoch, 7); + assert_eq!(diffs[2].to_epoch, 8); + } +} diff --git a/crates/echo-dry-tests/src/hashes.rs b/crates/echo-dry-tests/src/hashes.rs new file mode 100644 index 00000000..5cc15433 --- /dev/null +++ b/crates/echo-dry-tests/src/hashes.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Hash ID generation helpers for tests. +//! +//! These functions centralize the common pattern of creating deterministic +//! hash-based IDs for rules, intents, and other test artifacts. + +use warp_core::Hash; + +/// Generate a rule ID from a name. +/// +/// This uses the same pattern as production code: `blake3("rule:" + name)`. +/// +/// # Example +/// +/// ``` +/// use echo_dry_tests::make_rule_id; +/// +/// let id = make_rule_id("my-rule"); +/// assert_eq!(id.len(), 32); +/// ``` +pub fn make_rule_id(name: &str) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"rule:"); + hasher.update(name.as_bytes()); + hasher.finalize().into() +} + +/// Generate an intent ID from raw bytes. +/// +/// # Example +/// +/// ``` +/// use echo_dry_tests::make_intent_id; +/// +/// let id = make_intent_id(b"my-intent"); +/// assert_eq!(id.len(), 32); +/// ``` +pub fn make_intent_id(bytes: &[u8]) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"intent:"); + hasher.update(bytes); + hasher.finalize().into() +} + +/// Generate a generic test hash from a label. +/// +/// Useful for creating deterministic hashes in tests without coupling +/// to specific domain semantics. +pub fn make_test_hash(label: &str) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"test:"); + hasher.update(label.as_bytes()); + hasher.finalize().into() +} + +/// Generate a hash from a numeric seed (useful for loops). +pub fn make_hash_from_seed(seed: u64) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"seed:"); + hasher.update(&seed.to_le_bytes()); + hasher.finalize().into() +} + +/// Compute a plan digest from receipt entries (matching warp-core semantics). +/// +/// This is useful for verifying tick receipt digests in tests. +pub fn compute_plan_digest(entries: &[(Hash, Hash)]) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(&(entries.len() as u64).to_le_bytes()); + for (scope_hash, rule_id) in entries { + hasher.update(scope_hash); + hasher.update(rule_id); + } + hasher.finalize().into() +} + +/// Pre-defined test rule IDs for common scenarios. +pub mod presets { + use super::*; + + /// Rule ID for "rule-a" (useful in multi-rule tests). + pub fn rule_a() -> Hash { + make_rule_id("rule-a") + } + + /// Rule ID for "rule-b" (useful in multi-rule tests). + pub fn rule_b() -> Hash { + make_rule_id("rule-b") + } + + /// Rule ID for "rule-c" (useful in multi-rule tests). + pub fn rule_c() -> Hash { + make_rule_id("rule-c") + } + + /// Rule ID for motion rule. + pub fn motion_rule() -> Hash { + make_rule_id(crate::demo_rules::MOTION_RULE_NAME) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rule_id_is_deterministic() { + let id1 = make_rule_id("test"); + let id2 = make_rule_id("test"); + assert_eq!(id1, id2); + } + + #[test] + fn different_names_produce_different_ids() { + let id1 = make_rule_id("rule-a"); + let id2 = make_rule_id("rule-b"); + assert_ne!(id1, id2); + } + + #[test] + fn intent_id_is_deterministic() { + let id1 = make_intent_id(b"test"); + let id2 = make_intent_id(b"test"); + assert_eq!(id1, id2); + } + + #[test] + fn presets_are_stable() { + // Just verify they don't panic and return 32-byte hashes + assert_eq!(presets::rule_a().len(), 32); + assert_eq!(presets::rule_b().len(), 32); + assert_eq!(presets::rule_c().len(), 32); + } +} diff --git a/crates/echo-dry-tests/src/lib.rs b/crates/echo-dry-tests/src/lib.rs new file mode 100644 index 00000000..1e5301fe --- /dev/null +++ b/crates/echo-dry-tests/src/lib.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Shared test doubles and fixtures for Echo crates. +//! +//! This crate provides commonly used test utilities to reduce duplication +//! across the Echo test suite and improve test maintainability. +//! +//! # Modules +//! +//! - [`config`] - In-memory config store fake for testing without filesystem +//! - [`demo_rules`] - Demo rules (motion, port) for integration tests +//! - [`engine`] - Engine and GraphStore builder utilities +//! - [`frames`] - WarpSnapshot and WarpDiff builders +//! - [`hashes`] - Hash ID generation helpers (rule_id, intent_id, etc.) +//! - [`motion`] - Motion payload encoding helpers +//! - [`rules`] - Synthetic rule builders (noop matchers/executors) + +pub mod config; +pub mod demo_rules; +pub mod engine; +pub mod frames; +pub mod hashes; +pub mod motion; +pub mod rules; + +// Re-export commonly used items at crate root for convenience +pub use config::InMemoryConfigStore; +pub use demo_rules::{ + build_motion_demo_engine, build_port_demo_engine, motion_rule, port_rule, MOTION_RULE_NAME, + PORT_RULE_NAME, +}; +pub use engine::{build_engine_with_root, build_engine_with_typed_root, EngineTestBuilder}; +pub use frames::{DiffBuilder, SnapshotBuilder}; +pub use hashes::{make_intent_id, make_rule_id}; +pub use motion::{MotionPayloadBuilder, DEFAULT_MOTION_POSITION, DEFAULT_MOTION_VELOCITY}; +pub use rules::{NoOpRule, SyntheticRuleBuilder}; diff --git a/crates/echo-dry-tests/src/motion.rs b/crates/echo-dry-tests/src/motion.rs new file mode 100644 index 00000000..53cbeecc --- /dev/null +++ b/crates/echo-dry-tests/src/motion.rs @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Motion payload encoding helpers for tests. + +use warp_core::{encode_motion_atom_payload, AtomPayload}; + +/// Default position used in motion tests: [1.0, 2.0, 3.0] +pub const DEFAULT_MOTION_POSITION: [f32; 3] = [1.0, 2.0, 3.0]; + +/// Default velocity used in motion tests: [0.5, -1.0, 0.25] +pub const DEFAULT_MOTION_VELOCITY: [f32; 3] = [0.5, -1.0, 0.25]; + +/// Builder for creating motion payloads in tests. +/// +/// # Example +/// +/// ``` +/// use echo_dry_tests::MotionPayloadBuilder; +/// +/// let payload = MotionPayloadBuilder::new() +/// .position([10.0, 20.0, 30.0]) +/// .velocity([1.0, 0.0, 0.0]) +/// .build(); +/// ``` +pub struct MotionPayloadBuilder { + position: [f32; 3], + velocity: [f32; 3], +} + +impl Default for MotionPayloadBuilder { + fn default() -> Self { + Self::new() + } +} + +impl MotionPayloadBuilder { + /// Create a new builder with default position and velocity. + pub fn new() -> Self { + Self { + position: DEFAULT_MOTION_POSITION, + velocity: DEFAULT_MOTION_VELOCITY, + } + } + + /// Create a builder with zero position and velocity. + pub fn zero() -> Self { + Self { + position: [0.0, 0.0, 0.0], + velocity: [0.0, 0.0, 0.0], + } + } + + /// Set the position. + pub fn position(mut self, pos: [f32; 3]) -> Self { + self.position = pos; + self + } + + /// Set the velocity. + pub fn velocity(mut self, vel: [f32; 3]) -> Self { + self.velocity = vel; + self + } + + /// Set position X component. + pub fn px(mut self, x: f32) -> Self { + self.position[0] = x; + self + } + + /// Set position Y component. + pub fn py(mut self, y: f32) -> Self { + self.position[1] = y; + self + } + + /// Set position Z component. + pub fn pz(mut self, z: f32) -> Self { + self.position[2] = z; + self + } + + /// Set velocity X component. + pub fn vx(mut self, x: f32) -> Self { + self.velocity[0] = x; + self + } + + /// Set velocity Y component. + pub fn vy(mut self, y: f32) -> Self { + self.velocity[1] = y; + self + } + + /// Set velocity Z component. + pub fn vz(mut self, z: f32) -> Self { + self.velocity[2] = z; + self + } + + /// Build the atom payload. + pub fn build(self) -> AtomPayload { + encode_motion_atom_payload(self.position, self.velocity) + } + + /// Get the position. + pub fn get_position(&self) -> [f32; 3] { + self.position + } + + /// Get the velocity. + pub fn get_velocity(&self) -> [f32; 3] { + self.velocity + } +} + +/// Create a motion payload with default position and velocity. +pub fn default_motion_payload() -> AtomPayload { + MotionPayloadBuilder::new().build() +} + +/// Create a motion payload with zero position and velocity. +pub fn zero_motion_payload() -> AtomPayload { + MotionPayloadBuilder::zero().build() +} + +/// Create a motion payload with the given position and zero velocity. +pub fn stationary_at(position: [f32; 3]) -> AtomPayload { + MotionPayloadBuilder::zero().position(position).build() +} + +/// Create a motion payload with zero position and the given velocity. +pub fn moving_from_origin(velocity: [f32; 3]) -> AtomPayload { + MotionPayloadBuilder::zero().velocity(velocity).build() +} + +#[cfg(test)] +mod tests { + use super::*; + use warp_core::decode_motion_atom_payload; + + #[test] + fn builder_with_defaults() { + let payload = MotionPayloadBuilder::new().build(); + let (pos, vel) = decode_motion_atom_payload(&payload).expect("decode"); + assert_eq!(pos, DEFAULT_MOTION_POSITION); + assert_eq!(vel, DEFAULT_MOTION_VELOCITY); + } + + #[test] + fn builder_zero() { + let payload = MotionPayloadBuilder::zero().build(); + let (pos, vel) = decode_motion_atom_payload(&payload).expect("decode"); + assert_eq!(pos, [0.0, 0.0, 0.0]); + assert_eq!(vel, [0.0, 0.0, 0.0]); + } + + #[test] + fn builder_fluent_api() { + let payload = MotionPayloadBuilder::new() + .position([10.0, 20.0, 30.0]) + .velocity([1.0, 2.0, 3.0]) + .build(); + let (pos, vel) = decode_motion_atom_payload(&payload).expect("decode"); + assert_eq!(pos, [10.0, 20.0, 30.0]); + assert_eq!(vel, [1.0, 2.0, 3.0]); + } + + #[test] + fn builder_individual_components() { + let payload = MotionPayloadBuilder::zero() + .px(1.0) + .py(2.0) + .pz(3.0) + .vx(4.0) + .vy(5.0) + .vz(6.0) + .build(); + let (pos, vel) = decode_motion_atom_payload(&payload).expect("decode"); + assert_eq!(pos, [1.0, 2.0, 3.0]); + assert_eq!(vel, [4.0, 5.0, 6.0]); + } + + #[test] + fn stationary_at_helper() { + let payload = stationary_at([5.0, 10.0, 15.0]); + let (pos, vel) = decode_motion_atom_payload(&payload).expect("decode"); + assert_eq!(pos, [5.0, 10.0, 15.0]); + assert_eq!(vel, [0.0, 0.0, 0.0]); + } +} diff --git a/crates/echo-dry-tests/src/rules.rs b/crates/echo-dry-tests/src/rules.rs new file mode 100644 index 00000000..577c602b --- /dev/null +++ b/crates/echo-dry-tests/src/rules.rs @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Synthetic rule builders for tests. +//! +//! Provides pre-built rule components (matchers, executors, footprints) +//! and a builder for creating custom synthetic rules. + +use crate::hashes::make_rule_id; +use warp_core::{ConflictPolicy, Footprint, GraphStore, Hash, NodeId, PatternGraph, RewriteRule}; + +/// Type alias for join functions matching warp-core's `JoinFn`. +pub type JoinFn = fn(&NodeId, &NodeId) -> bool; + +// --- Matcher Functions --- + +/// Matcher that always returns true. +pub fn always_match(_: &GraphStore, _: &NodeId) -> bool { + true +} + +/// Matcher that always returns false. +pub fn never_match(_: &GraphStore, _: &NodeId) -> bool { + false +} + +/// Matcher that returns true if the scope node exists. +pub fn scope_exists(store: &GraphStore, scope: &NodeId) -> bool { + store.node(scope).is_some() +} + +// --- Executor Functions --- + +/// Executor that does nothing. +pub fn noop_exec(_: &mut GraphStore, _: &NodeId) {} + +// --- Footprint Functions --- + +/// Footprint that claims no reads or writes. +pub fn empty_footprint(_: &GraphStore, _: &NodeId) -> Footprint { + Footprint::default() +} + +/// Footprint that writes to the scope node. +pub fn write_scope_footprint(_: &GraphStore, scope: &NodeId) -> Footprint { + let mut fp = Footprint::default(); + fp.n_write.insert_node(scope); + fp.factor_mask = 1; + fp +} + +/// Footprint that reads from the scope node. +pub fn read_scope_footprint(_: &GraphStore, scope: &NodeId) -> Footprint { + let mut fp = Footprint::default(); + fp.n_read.insert_node(scope); + fp.factor_mask = 1; + fp +} + +/// Footprint that writes to scope and a derived "other" node. +pub fn write_scope_and_other_footprint(_: &GraphStore, scope: &NodeId) -> Footprint { + let mut fp = Footprint::default(); + fp.n_write.insert_node(scope); + fp.n_write.insert_node(&other_node_of(scope)); + fp.factor_mask = 1; + fp +} + +/// Derive an "other" node ID from a scope (useful for conflict tests). +/// +/// Uses domain-separated hashing (prefixed with `b"other-node:"`) for +/// consistency with other hash generation functions in this crate. +pub fn other_node_of(scope: &NodeId) -> NodeId { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"other-node:"); + hasher.update(&scope.0); + NodeId(hasher.finalize().into()) +} + +// --- Pre-built Rules --- + +/// A no-op rule that always matches and does nothing. +/// +/// Useful for testing rule registration and basic scheduling. +pub struct NoOpRule; + +impl NoOpRule { + /// Create a no-op rule with the given name. + /// + /// Note: The name must be a `&'static str` because `RewriteRule::name` + /// requires a static lifetime. + pub fn create(name: &'static str) -> RewriteRule { + SyntheticRuleBuilder::new(name) + .matcher(always_match) + .executor(noop_exec) + .footprint(empty_footprint) + .build() + } + + /// Create a no-op rule named "noop". + pub fn default_rule() -> RewriteRule { + Self::create("noop") + } +} + +/// Builder for creating synthetic rules in tests. +/// +/// # Example +/// +/// ``` +/// use echo_dry_tests::{SyntheticRuleBuilder, rules::always_match, rules::noop_exec}; +/// +/// let rule = SyntheticRuleBuilder::new("test-rule") +/// .matcher(always_match) +/// .executor(noop_exec) +/// .build(); +/// ``` +pub struct SyntheticRuleBuilder { + name: &'static str, + id: Option, + matcher: fn(&GraphStore, &NodeId) -> bool, + executor: fn(&mut GraphStore, &NodeId), + footprint: fn(&GraphStore, &NodeId) -> Footprint, + factor_mask: u64, + conflict_policy: ConflictPolicy, + join_fn: Option, +} + +impl SyntheticRuleBuilder { + /// Create a new builder with the given rule name. + /// + /// Note: The name must be a `&'static str` because `RewriteRule::name` + /// requires a static lifetime. + pub fn new(name: &'static str) -> Self { + Self { + name, + id: None, + matcher: always_match, + executor: noop_exec, + footprint: empty_footprint, + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } + } + + /// Set a custom rule ID (default is derived from name). + pub fn id(mut self, id: Hash) -> Self { + self.id = Some(id); + self + } + + /// Set the matcher function. + pub fn matcher(mut self, f: fn(&GraphStore, &NodeId) -> bool) -> Self { + self.matcher = f; + self + } + + /// Set the executor function. + pub fn executor(mut self, f: fn(&mut GraphStore, &NodeId)) -> Self { + self.executor = f; + self + } + + /// Set the footprint function. + pub fn footprint(mut self, f: fn(&GraphStore, &NodeId) -> Footprint) -> Self { + self.footprint = f; + self + } + + /// Set the factor mask. + pub fn factor_mask(mut self, mask: u64) -> Self { + self.factor_mask = mask; + self + } + + /// Set the conflict policy. + pub fn conflict_policy(mut self, policy: ConflictPolicy) -> Self { + self.conflict_policy = policy; + self + } + + /// Set the join function (required when `conflict_policy` is `ConflictPolicy::Join`). + pub fn join_fn(mut self, f: JoinFn) -> Self { + self.join_fn = Some(f); + self + } + + /// Use the "always match" matcher. + pub fn always_matches(self) -> Self { + self.matcher(always_match) + } + + /// Use the "never match" matcher. + pub fn never_matches(self) -> Self { + self.matcher(never_match) + } + + /// Use the "scope exists" matcher. + pub fn matches_if_scope_exists(self) -> Self { + self.matcher(scope_exists) + } + + /// Use the "write scope" footprint. + pub fn writes_scope(self) -> Self { + self.footprint(write_scope_footprint) + } + + /// Use the "read scope" footprint. + pub fn reads_scope(self) -> Self { + self.footprint(read_scope_footprint) + } + + /// Build the rule. + pub fn build(self) -> RewriteRule { + RewriteRule { + id: self.id.unwrap_or_else(|| make_rule_id(self.name)), + name: self.name, + left: PatternGraph { nodes: vec![] }, + matcher: self.matcher, + executor: self.executor, + compute_footprint: self.footprint, + factor_mask: self.factor_mask, + conflict_policy: self.conflict_policy, + join_fn: self.join_fn, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn noop_rule_creation() { + let rule = NoOpRule::create("test-noop"); + assert_eq!(rule.name, "test-noop"); + } + + #[test] + fn synthetic_builder_defaults() { + let rule = SyntheticRuleBuilder::new("my-rule").build(); + assert_eq!(rule.name, "my-rule"); + assert_eq!(rule.factor_mask, 0); + } + + #[test] + fn synthetic_builder_custom_id() { + let custom_id: Hash = [42u8; 32]; + let rule = SyntheticRuleBuilder::new("custom").id(custom_id).build(); + assert_eq!(rule.id, custom_id); + } + + #[test] + fn synthetic_builder_fluent_api() { + let rule = SyntheticRuleBuilder::new("fluent") + .always_matches() + .writes_scope() + .factor_mask(7) + .conflict_policy(ConflictPolicy::Abort) + .build(); + + assert_eq!(rule.name, "fluent"); + assert_eq!(rule.factor_mask, 7); + } + + #[test] + fn other_node_is_deterministic() { + let scope = NodeId([1u8; 32]); + let other1 = other_node_of(&scope); + let other2 = other_node_of(&scope); + assert_eq!(other1, other2); + assert_ne!(scope, other1); + } + + // --- Behavioral Tests --- + + /// Helper to check if an IdSet contains a specific node hash. + fn id_set_contains(set: &warp_core::IdSet, node: &NodeId) -> bool { + set.iter().any(|h| *h == node.0) + } + + #[test] + fn matcher_scope_exists_returns_true_when_node_present() { + use warp_core::NodeRecord; + + let mut store = GraphStore::default(); + let scope = other_node_of(&NodeId([0xAAu8; 32])); + let ty = warp_core::make_type_id("test-type"); + + // Node not yet present: matcher should return false + assert!(!scope_exists(&store, &scope)); + + // Insert node into store + store.insert_node(scope, NodeRecord { ty }); + + // Node now present: matcher should return true + assert!(scope_exists(&store, &scope)); + } + + #[test] + fn matcher_always_and_never_match_behavior() { + let store = GraphStore::default(); + let scope = NodeId([0xBBu8; 32]); + + assert!(always_match(&store, &scope)); + assert!(!never_match(&store, &scope)); + } + + #[test] + fn footprint_write_scope_produces_expected_footprint() { + let store = GraphStore::default(); + let scope = NodeId([0xCCu8; 32]); + + let fp = write_scope_footprint(&store, &scope); + + assert!(id_set_contains(&fp.n_write, &scope)); + assert!(!id_set_contains(&fp.n_read, &scope)); + assert_eq!(fp.factor_mask, 1); + } + + #[test] + fn footprint_read_scope_produces_expected_footprint() { + let store = GraphStore::default(); + let scope = NodeId([0xDDu8; 32]); + + let fp = read_scope_footprint(&store, &scope); + + assert!(id_set_contains(&fp.n_read, &scope)); + assert!(!id_set_contains(&fp.n_write, &scope)); + assert_eq!(fp.factor_mask, 1); + } + + #[test] + fn footprint_write_scope_and_other_includes_both_nodes() { + let store = GraphStore::default(); + let scope = NodeId([0xEEu8; 32]); + let other = other_node_of(&scope); + + let fp = write_scope_and_other_footprint(&store, &scope); + + assert!(id_set_contains(&fp.n_write, &scope)); + assert!(id_set_contains(&fp.n_write, &other)); + assert_eq!(fp.factor_mask, 1); + } + + #[test] + fn footprint_empty_produces_default_footprint() { + let store = GraphStore::default(); + let scope = NodeId([0xFFu8; 32]); + + let fp = empty_footprint(&store, &scope); + + assert!(!id_set_contains(&fp.n_read, &scope)); + assert!(!id_set_contains(&fp.n_write, &scope)); + assert_eq!(fp.factor_mask, 0); + } + + #[test] + fn builder_conflict_policy_propagates_abort() { + let rule = SyntheticRuleBuilder::new("abort-rule") + .conflict_policy(ConflictPolicy::Abort) + .build(); + + assert!(matches!(rule.conflict_policy, ConflictPolicy::Abort)); + } + + #[test] + fn builder_conflict_policy_propagates_retry() { + let rule = SyntheticRuleBuilder::new("retry-rule") + .conflict_policy(ConflictPolicy::Retry) + .build(); + + assert!(matches!(rule.conflict_policy, ConflictPolicy::Retry)); + } + + #[test] + fn builder_conflict_policy_propagates_join() { + fn dummy_join(_left: &NodeId, _right: &NodeId) -> bool { + true + } + + let rule = SyntheticRuleBuilder::new("join-rule") + .conflict_policy(ConflictPolicy::Join) + .join_fn(dummy_join) + .build(); + + assert!(matches!(rule.conflict_policy, ConflictPolicy::Join)); + assert!(rule.join_fn.is_some()); + } + + #[test] + fn builder_join_fn_propagates_to_rule() { + fn my_join(left: &NodeId, right: &NodeId) -> bool { + left.0[0] < right.0[0] + } + + let rule = SyntheticRuleBuilder::new("join-fn-rule") + .join_fn(my_join) + .build(); + + let join = rule.join_fn.expect("join_fn should be Some"); + let left = NodeId([0x10u8; 32]); + let right = NodeId([0x20u8; 32]); + assert!(join(&left, &right)); + assert!(!join(&right, &left)); + } + + #[test] + fn other_node_of_uses_domain_separation() { + // Verify that other_node_of produces a different result than a naive + // blake3::hash call (demonstrating domain separation is in effect). + let scope = NodeId([0x42u8; 32]); + let other = other_node_of(&scope); + + // Naive hash without domain prefix + let naive_hash: [u8; 32] = blake3::hash(&scope.0).into(); + + // Domain-separated hash should differ from naive hash + assert_ne!(other.0, naive_hash); + } +} diff --git a/crates/echo-registry-api/Cargo.toml b/crates/echo-registry-api/Cargo.toml new file mode 100644 index 00000000..91885b1e --- /dev/null +++ b/crates/echo-registry-api/Cargo.toml @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +[package] +name = "echo-registry-api" +version = "0.1.0" +edition = "2021" +rust-version = "1.90.0" +license = "Apache-2.0" +description = "Generic registry interface for Echo WASM helpers (schema hash, codec, ops catalog)" +repository = "https://github.com/flyingrobots/echo" +readme = "README.md" +keywords = ["echo", "wasm", "registry"] +categories = ["wasm", "development-tools"] + +[dependencies] diff --git a/crates/echo-registry-api/README.md b/crates/echo-registry-api/README.md new file mode 100644 index 00000000..2604fd8e --- /dev/null +++ b/crates/echo-registry-api/README.md @@ -0,0 +1,8 @@ + + +# echo-registry-api + +Generic registry interface for Echo WASM helpers. Provides the trait and data +types (`RegistryProvider`, `RegistryInfo`, `OpDef`) that an application-specific +registry crate implements. `warp-wasm` links only to this interface so Echo +stays generic; apps supply their own registry at build time. diff --git a/crates/echo-registry-api/src/lib.rs b/crates/echo-registry-api/src/lib.rs new file mode 100644 index 00000000..c29b425e --- /dev/null +++ b/crates/echo-registry-api/src/lib.rs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Minimal, generic registry interface for Echo WASM helpers. +//! +//! The registry provider is supplied by the application (generated from the +//! GraphQL/Wesley IR). Echo core and `warp-wasm` depend only on this crate and +//! **must not** embed app-specific registries. + +/// Codec identifier used by the registry. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RegistryInfo { + /// Canonical codec identifier (e.g., "cbor-canon-v1"). + pub codec_id: &'static str, + /// Registry schema version for breaking changes in layout. + pub registry_version: u32, + /// Hex-encoded schema hash (lowercase, 64 chars). + pub schema_sha256_hex: &'static str, +} + +/// Error codes for wasm helpers. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HelperError { + /// No registry provider installed. + NoRegistry, + /// Unknown operation ID. + UnknownOp, + /// Input did not match schema (unknown key, missing required, wrong type, enum mismatch). + InvalidInput, + /// Internal failure (encoding). + Internal, +} + +/// Operation kind (query or mutation/command). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OpKind { + /// Read-only operation. + Query, + /// State-mutating operation. + Mutation, +} + +/// Descriptor for a single operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OpDef { + /// Operation kind. + pub kind: OpKind, + /// Operation name (GraphQL name). + pub name: &'static str, + /// Persisted operation identifier. + pub op_id: u32, + /// Argument descriptors. + pub args: &'static [ArgDef], + /// Result type name (GraphQL return type). + pub result_ty: &'static str, +} + +/// Argument descriptor (flat; sufficient for strict object validation). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ArgDef { + /// Field name. + pub name: &'static str, + /// GraphQL base type name. + pub ty: &'static str, + /// Whether the field is required. + pub required: bool, + /// Whether the field is a list. + pub list: bool, +} + +/// Enum descriptor (for validating enum string values). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnumDef { + /// Enum name. + pub name: &'static str, + /// Allowed values (uppercase GraphQL names). + pub values: &'static [&'static str], +} + +/// Object descriptor for result validation (optional; fields may be empty for now). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ObjectDef { + /// Object name. + pub name: &'static str, + /// Fields on the object. + pub fields: &'static [ArgDef], +} + +/// Application-supplied registry provider. +/// +/// Implemented by a generated crate in the application build. `warp-wasm` +/// should link against that provider to validate op IDs, expose registry +/// metadata, and (eventually) drive schema-typed encoding/decoding. +pub trait RegistryProvider: Sync { + /// Return registry metadata (codec, version, schema hash). + fn info(&self) -> RegistryInfo; + + /// Look up an operation by ID. + fn op_by_id(&self, op_id: u32) -> Option<&'static OpDef>; + + /// Return all operations (sorted by op_id for deterministic iteration). + fn all_ops(&self) -> &'static [OpDef]; + + /// Return all enums (for validating enum values). + fn all_enums(&self) -> &'static [EnumDef]; + + /// Return all objects (for result validation). + fn all_objects(&self) -> &'static [ObjectDef]; +} diff --git a/crates/echo-wasm-abi/Cargo.toml b/crates/echo-wasm-abi/Cargo.toml index 676c6ff3..c326456c 100644 --- a/crates/echo-wasm-abi/Cargo.toml +++ b/crates/echo-wasm-abi/Cargo.toml @@ -14,4 +14,11 @@ categories = ["wasm", "encoding", "data-structures"] [dependencies] serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +ciborium = "0.2" +serde-value = "0.7" +half = "2.4" +thiserror = "1.0" + +[dev-dependencies] +proptest = "1.0" +serde_json = "1.0" \ No newline at end of file diff --git a/crates/echo-wasm-abi/README.md b/crates/echo-wasm-abi/README.md index b5f9930c..30c06d9d 100644 --- a/crates/echo-wasm-abi/README.md +++ b/crates/echo-wasm-abi/README.md @@ -9,7 +9,15 @@ Shared WASM-friendly DTOs for Echo/JITOS living specs. Mirrors the minimal graph - `Node`, `Edge`, `WarpGraph` - `Value` (Str/Num/Bool/Null) - `Rewrite` with `SemanticOp` (AddNode/Set/DeleteNode/Connect/Disconnect) +- Deterministic CBOR helpers: `encode_cbor` / `decode_cbor` (canonical subset, no tags/indefinite) ## Usage Add as a dependency and reuse the DTOs in WASM bindings and UI code to keep the schema consistent across kernel and specs. + +### Canonical encoding + +- `encode_cbor` / `decode_cbor` use the same canonical CBOR rules as `echo-session-proto` (definite lengths, sorted map keys, shortest ints/floats, no tags). +- Integers are limited to i64/u64 (CBOR major 0/1); float widths are minimized to round-trip. +- Host code should call into Rust/WASM helpers rather than hand-encoding bytes to avoid non-canonical payloads. +- JS→CBOR mapping rules for the ABI are frozen in `docs/js-cbor-mapping.md` (string map keys only; ban `undefined`/`BigInt`; shortest ints/floats; no tags or indefinite lengths). Host and kernel must both enforce these rules. diff --git a/crates/echo-wasm-abi/src/canonical.rs b/crates/echo-wasm-abi/src/canonical.rs new file mode 100644 index 00000000..2341cf8a --- /dev/null +++ b/crates/echo-wasm-abi/src/canonical.rs @@ -0,0 +1,411 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Deterministic CBOR encoder/decoder (subset, canonical) for WASM ABI payloads. +//! Copied verbatim in spirit from `echo-session-proto` canonical encoder to avoid +//! divergence; supports definite lengths only, no tags, sorted map keys, and the +//! smallest-width integers/floats that round-trip. Limits integers to i64/u64 as +//! supported by `ciborium::value::Integer`. + +use ciborium::value::{Integer, Value}; +use half::f16; + +/// Errors produced by the canonical CBOR encoder/decoder. +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum CanonError { + /// Input exhausted before value completed. + #[error("incomplete input")] + Incomplete, + /// Extra bytes present after decoding a single value. + #[error("trailing bytes after value")] + Trailing, + /// CBOR tags are disallowed in this canonical subset. + #[error("tags not allowed")] + Tag, + /// Indefinite lengths are disallowed. + #[error("indefinite length not allowed")] + Indefinite, + /// Integer encoded with non-minimal width. + #[error("non-canonical integer width")] + NonCanonicalInt, + /// Float encoded with non-minimal width. + #[error("non-canonical float width")] + NonCanonicalFloat, + /// Float used where an integer canonical form is required. + #[error("float encodes integral value; must be integer")] + FloatShouldBeInt, + /// Map keys not strictly increasing by bytewise order. + #[error("map keys not strictly increasing")] + MapKeyOrder, + /// Duplicate map key encountered. + #[error("duplicate map key")] + MapKeyDuplicate, + /// Generic decode error with detail. + #[error("decode error: {0}")] + Decode(String), + /// Generic encode error with detail. + #[error("encode error: {0}")] + Encode(String), +} + +type Result = std::result::Result; + +/// Encode a `ciborium::value::Value` to deterministic CBOR bytes. +pub fn encode_value(val: &Value) -> Result> { + let mut out = Vec::new(); + enc_value(val, &mut out)?; + Ok(out) +} + +/// Decode deterministic CBOR bytes into a `ciborium::value::Value`. +pub fn decode_value(bytes: &[u8]) -> Result { + let mut idx = 0usize; + let v = dec_value(bytes, &mut idx)?; + if idx != bytes.len() { + return Err(CanonError::Trailing); + } + Ok(v) +} + +fn enc_value(v: &Value, out: &mut Vec) -> Result<()> { + match v { + Value::Bool(b) => { + out.push(if *b { 0xf5 } else { 0xf4 }); + } + Value::Null => out.push(0xf6), + Value::Integer(n) => enc_int(i128::from(*n), out), + Value::Float(f) => enc_float(*f, out), + Value::Text(s) => enc_text(s, out)?, + Value::Bytes(b) => enc_bytes(b, out)?, + Value::Array(items) => { + enc_len(4, items.len() as u64, out); + for it in items { + enc_value(it, out)?; + } + } + Value::Map(entries) => { + let mut buf: Vec<(Value, Value, Vec)> = Vec::with_capacity(entries.len()); + for (k, v) in entries { + let mut kb = Vec::new(); + enc_value(k, &mut kb)?; + buf.push((k.clone(), v.clone(), kb)); + } + + buf.sort_by(|a, b| a.2.cmp(&b.2)); + + for win in buf.windows(2) { + if win[0].2 == win[1].2 { + return Err(CanonError::MapKeyDuplicate); + } + } + + enc_len(5, buf.len() as u64, out); + for (_k, v, kb) in buf { + out.extend_from_slice(&kb); + enc_value(&v, out)?; + } + } + Value::Tag(_, _) => return Err(CanonError::Tag), + _ => return Err(CanonError::Decode("unsupported simple value".into())), + } + Ok(()) +} + +fn enc_len(major: u8, len: u64, out: &mut Vec) { + write_major(major, len as u128, out); +} + +fn enc_int(n: i128, out: &mut Vec) { + if n >= 0 { + write_major(0, n as u128, out); + } else { + let m = (-1 - n) as u128; + write_major(1, m, out); + } +} + +fn enc_float(f: f64, out: &mut Vec) { + if f.is_nan() { + let h = f16::NAN; + write_half(h, out); + return; + } + if f.is_infinite() { + let h = if f.is_sign_positive() { + f16::INFINITY + } else { + f16::NEG_INFINITY + }; + write_half(h, out); + return; + } + if f.fract() == 0.0 { + // i128 range: approximately ±1.7e38; f64 can represent up to ±1.8e308 + // Check range before casting to avoid overflow/UB + const I128_MAX_F: f64 = i128::MAX as f64; + const I128_MIN_F: f64 = i128::MIN as f64; + if (I128_MIN_F..=I128_MAX_F).contains(&f) { + let i = f as i128; + if i as f64 == f { + enc_int(i, out); + return; + } + } + } + let h = f16::from_f64(f); + if h.to_f64() == f { + write_half(h, out); + return; + } + let f32v = f as f32; + if f32v as f64 == f { + write_f32(f32v, out); + } else { + write_f64(f, out); + } +} + +fn write_half(h: f16, out: &mut Vec) { + out.push(0xf9); + out.extend_from_slice(&h.to_bits().to_be_bytes()); +} + +fn write_f32(fv: f32, out: &mut Vec) { + out.push(0xfa); + out.extend_from_slice(&fv.to_be_bytes()); +} + +fn write_f64(fv: f64, out: &mut Vec) { + out.push(0xfb); + out.extend_from_slice(&fv.to_be_bytes()); +} + +fn enc_bytes(b: &[u8], out: &mut Vec) -> Result<()> { + enc_len(2, b.len() as u64, out); + out.extend_from_slice(b); + Ok(()) +} + +fn enc_text(s: &str, out: &mut Vec) -> Result<()> { + enc_len(3, s.len() as u64, out); + out.extend_from_slice(s.as_bytes()); + Ok(()) +} + +fn write_major(major: u8, n: u128, out: &mut Vec) { + debug_assert!(major <= 7); + match n { + 0..=23 => out.push((major << 5) | n as u8), + 24..=0xff => { + out.push((major << 5) | 24); + out.push(n as u8); + } + 0x100..=0xffff => { + out.push((major << 5) | 25); + out.extend_from_slice(&(n as u16).to_be_bytes()); + } + 0x1_0000..=0xffff_ffff => { + out.push((major << 5) | 26); + out.extend_from_slice(&(n as u32).to_be_bytes()); + } + _ => { + out.push((major << 5) | 27); + out.extend_from_slice(&(n as u64).to_be_bytes()); + } + } +} + +fn dec_value(bytes: &[u8], idx: &mut usize) -> Result { + fn need(bytes: &[u8], idx: usize, n: usize) -> Result<()> { + if bytes.len().saturating_sub(idx) < n { + Err(CanonError::Incomplete) + } else { + Ok(()) + } + } + + need(bytes, *idx, 1)?; + let b0 = bytes[*idx]; + *idx += 1; + let major = b0 >> 5; + let info = b0 & 0x1f; + + fn read_uint(bytes: &[u8], idx: &mut usize, nbytes: usize) -> Result { + need(bytes, *idx, nbytes)?; + let mut val = 0u64; + for _ in 0..nbytes { + val = (val << 8) | (bytes[*idx] as u64); + *idx += 1; + } + Ok(val) + } + + fn read_f(bytes: &[u8], idx: &mut usize, nbytes: usize) -> Result { + need(bytes, *idx, nbytes)?; + let slice = &bytes[*idx..*idx + nbytes]; + *idx += nbytes; + Ok(match nbytes { + 2 => f16::from_bits(u16::from_be_bytes( + slice + .try_into() + .map_err(|_| CanonError::Decode("invalid f16 bytes".into()))?, + )) + .to_f64(), + 4 => f32::from_be_bytes( + slice + .try_into() + .map_err(|_| CanonError::Decode("invalid f32 bytes".into()))?, + ) as f64, + 8 => f64::from_be_bytes( + slice + .try_into() + .map_err(|_| CanonError::Decode("invalid f64 bytes".into()))?, + ), + _ => unreachable!(), + }) + } + + fn read_len(bytes: &[u8], idx: &mut usize, info: u8) -> Result { + let val = match info { + 0..=23 => info as u64, + 24 => read_uint(bytes, idx, 1)?, + 25 => read_uint(bytes, idx, 2)?, + 26 => read_uint(bytes, idx, 4)?, + 27 => read_uint(bytes, idx, 8)?, + 31 => return Err(CanonError::Indefinite), + _ => return Err(CanonError::Decode("invalid length info".into())), + }; + // Reject non-canonical (over-wide) length encodings + match info { + 24 if val <= 23 => return Err(CanonError::NonCanonicalInt), + 25 if val <= 0xff => return Err(CanonError::NonCanonicalInt), + 26 if val <= 0xffff => return Err(CanonError::NonCanonicalInt), + 27 if val <= 0xffff_ffff => return Err(CanonError::NonCanonicalInt), + _ => {} + } + Ok(val) + } + + match major { + 0 | 1 => { + let n = read_len(bytes, idx, info)?; + let i = if major == 0 { + Integer::from(n) + } else { + let neg = -(1i128 + n as i128); + let signed = i64::try_from(neg) + .map_err(|_| CanonError::Decode("integer out of range".into()))?; + Integer::from(signed) + }; + Ok(Value::Integer(i)) + } + 2 | 3 => { + let len = read_len(bytes, idx, info)?; + let len = len as usize; + need(bytes, *idx, len)?; + let data = &bytes[*idx..*idx + len]; + *idx += len; + if major == 2 { + Ok(Value::Bytes(data.to_vec())) + } else { + let s = std::str::from_utf8(data) + .map_err(|e| CanonError::Decode(format!("utf8: {}", e)))?; + Ok(Value::Text(s.to_string())) + } + } + 4 => { + let len = read_len(bytes, idx, info)? as usize; + let mut items = Vec::with_capacity(len); + for _ in 0..len { + items.push(dec_value(bytes, idx)?); + } + Ok(Value::Array(items)) + } + 5 => { + let len = read_len(bytes, idx, info)? as usize; + let mut entries = Vec::with_capacity(len); + let mut last_key: Option> = None; + for _ in 0..len { + let key_start = *idx; + let k = dec_value(bytes, idx)?; + let key_end = *idx; + let kb = &bytes[key_start..key_end]; + if let Some(prev) = &last_key { + if kb == prev.as_slice() { + return Err(CanonError::MapKeyDuplicate); + } else if kb < prev.as_slice() { + return Err(CanonError::MapKeyOrder); + } + } + last_key = Some(kb.to_vec()); + let v = dec_value(bytes, idx)?; + entries.push((k, v)); + } + Ok(Value::Map(entries)) + } + 7 => match info { + 20 => Ok(Value::Bool(false)), + 21 => Ok(Value::Bool(true)), + 22 => Ok(Value::Null), + 25 => { + let f = read_f(bytes, idx, 2)?; + if is_exact_int(f) { + return Err(CanonError::FloatShouldBeInt); + } + Ok(Value::Float(f)) + } + 26 => { + let f = read_f(bytes, idx, 4)?; + if is_exact_int(f) { + return Err(CanonError::FloatShouldBeInt); + } + if can_fit_f16(f) { + return Err(CanonError::NonCanonicalFloat); + } + Ok(Value::Float(f)) + } + 27 => { + let f = read_f(bytes, idx, 8)?; + if is_exact_int(f) { + return Err(CanonError::FloatShouldBeInt); + } + if can_fit_f16(f) { + return Err(CanonError::NonCanonicalFloat); + } + if can_fit_f32(f) { + return Err(CanonError::NonCanonicalFloat); + } + Ok(Value::Float(f)) + } + 31 => Err(CanonError::Indefinite), + _ => Err(CanonError::Decode("simple value not supported".into())), + }, + 6 => Err(CanonError::Tag), + _ => Err(CanonError::Decode("unknown major type".into())), + } +} + +fn is_exact_int(f: f64) -> bool { + if f.is_infinite() || f.is_nan() { + return false; + } + if f.fract() != 0.0 { + return false; + } + let i = f as i128; + i as f64 == f +} + +fn can_fit_f16(f: f64) -> bool { + if f.is_nan() { + return true; + } + let h = f16::from_f64(f); + h.to_f64() == f +} + +fn can_fit_f32(f: f64) -> bool { + if f.is_nan() { + return true; + } + (f as f32) as f64 == f +} diff --git a/crates/echo-wasm-abi/src/eintlog.rs b/crates/echo-wasm-abi/src/eintlog.rs new file mode 100644 index 00000000..7480a586 --- /dev/null +++ b/crates/echo-wasm-abi/src/eintlog.rs @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Binary logging format for Echo intent logs (.eintlog). +//! +//! Format Spec (v1): +//! Header (48 bytes): +//! - Magic: "ELOG" (4 bytes) +//! - Version: u16 LE (2 bytes) = 1 +//! - Flags: u16 LE (2 bytes) = 0 +//! - Schema Hash: [u8; 32] (32 bytes) +//! - Reserved: [u8; 8] (8 bytes) = 0 +//! +//! Frames (Repeated): +//! - Length: u32 LE (4 bytes) +//! - Payload: [u8; Length] (Must be valid EINT envelope) + +use std::io::{self, Read, Write}; + +/// Magic bytes identifying an ELOG file: "ELOG". +pub const ELOG_MAGIC: [u8; 4] = *b"ELOG"; +/// Current ELOG format version. +pub const ELOG_VERSION: u16 = 1; +/// Maximum allowed frame length (10 MiB). +pub const MAX_FRAME_LEN: usize = 10 * 1024 * 1024; + +/// Header for an ELOG binary log file. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ElogHeader { + /// BLAKE3 hash of the schema used for this log. + pub schema_hash: [u8; 32], + /// Reserved flags (currently unused, should be 0). + pub flags: u16, +} + +fn read_exact_arr(r: &mut R) -> io::Result<[u8; N]> { + let mut buf = [0u8; N]; + r.read_exact(&mut buf)?; + Ok(buf) +} + +/// Reads and validates an ELOG header from a reader. +/// +/// # Errors +/// Returns an error if the magic bytes are invalid or the version is unsupported. +pub fn read_elog_header(r: &mut R) -> io::Result { + let magic = read_exact_arr::<4, _>(r)?; + if magic != ELOG_MAGIC { + return Err(io::Error::new(io::ErrorKind::InvalidData, "bad ELOG magic")); + } + + let version_bytes = read_exact_arr::<2, _>(r)?; + let version = u16::from_le_bytes(version_bytes); + if version != ELOG_VERSION { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("unsupported ELOG version: {}", version), + )); + } + + let flags_bytes = read_exact_arr::<2, _>(r)?; + let flags = u16::from_le_bytes(flags_bytes); + if flags != 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "ELOG flags must be zero", + )); + } + + let schema_hash = read_exact_arr::<32, _>(r)?; + let reserved = read_exact_arr::<8, _>(r)?; + if reserved != [0u8; 8] { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "ELOG reserved bytes must be zero", + )); + } + + Ok(ElogHeader { schema_hash, flags }) +} + +/// Writes an ELOG header to a writer. +/// +/// # Errors +/// Returns an error if writing fails. +pub fn write_elog_header(w: &mut W, hdr: &ElogHeader) -> io::Result<()> { + w.write_all(&ELOG_MAGIC)?; + w.write_all(&ELOG_VERSION.to_le_bytes())?; + w.write_all(&hdr.flags.to_le_bytes())?; + w.write_all(&hdr.schema_hash)?; + w.write_all(&[0u8; 8])?; // reserved + Ok(()) +} + +/// Reads a single frame from an ELOG file. +/// +/// Returns `Ok(None)` only if EOF occurs while reading the 4-byte length prefix. +/// If EOF happens mid-payload, this returns `Err(UnexpectedEof)`. +/// +/// Returns `Ok(Some(data))` for a valid frame. +/// +/// # Errors +/// Returns an error if the frame is too large, reading fails, or the payload is truncated. +pub fn read_elog_frame(r: &mut R) -> io::Result>> { + let mut len_bytes = [0u8; 4]; + match r.read_exact(&mut len_bytes) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None), + Err(e) => return Err(e), + } + let len = u32::from_le_bytes(len_bytes) as usize; + if len > MAX_FRAME_LEN { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "frame too large", + )); + } + + let mut buf = vec![0u8; len]; + r.read_exact(&mut buf)?; + Ok(Some(buf)) +} + +/// Writes a single frame to an ELOG file. +/// +/// # Errors +/// Returns an error if the frame exceeds [`MAX_FRAME_LEN`] or writing fails. +pub fn write_elog_frame(w: &mut W, frame: &[u8]) -> io::Result<()> { + if frame.len() > MAX_FRAME_LEN { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "frame too large", + )); + } + let len = frame.len() as u32; + w.write_all(&len.to_le_bytes())?; + w.write_all(frame)?; + Ok(()) +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + + #[test] + fn test_elog_round_trip() { + let hdr = ElogHeader { + schema_hash: [0xAA; 32], + flags: 0, // Must be zero per spec + }; + let frame1 = b"frame1".to_vec(); + let frame2 = vec![0u8; 1000]; + + let mut buf = Vec::new(); + write_elog_header(&mut buf, &hdr).unwrap(); + write_elog_frame(&mut buf, &frame1).unwrap(); + write_elog_frame(&mut buf, &frame2).unwrap(); + + let mut cursor = std::io::Cursor::new(buf); + let read_hdr = read_elog_header(&mut cursor).unwrap(); + assert_eq!(read_hdr, hdr); + + let f1 = read_elog_frame(&mut cursor).unwrap().unwrap(); + assert_eq!(f1, frame1); + + let f2 = read_elog_frame(&mut cursor).unwrap().unwrap(); + assert_eq!(f2, frame2); + + assert!(read_elog_frame(&mut cursor).unwrap().is_none()); + } + + #[test] + fn test_elog_rejects_large_frame() { + let mut buf = Vec::new(); + let huge_len = (MAX_FRAME_LEN + 1) as u32; + buf.extend_from_slice(&huge_len.to_le_bytes()); + + let mut cursor = std::io::Cursor::new(buf); + let res = read_elog_frame(&mut cursor); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().to_string(), "frame too large"); + } +} diff --git a/crates/echo-wasm-abi/src/lib.rs b/crates/echo-wasm-abi/src/lib.rs index 99743c7d..ccde9894 100644 --- a/crates/echo-wasm-abi/src/lib.rs +++ b/crates/echo-wasm-abi/src/lib.rs @@ -1,191 +1,321 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Shared WASM-friendly DTOs for Echo/JITOS living specs. -//! -//! This crate is intentionally small and **WASM-friendly**: -//! -//! - The types are designed to cross the JS boundary (via `serde` + `wasm-bindgen` wrappers). -//! - The shapes are used by Spec-000 (and future interactive specs) to render and mutate a tiny -//! “teaching graph” in the browser. -//! -//! Determinism note: -//! -//! - These DTOs are *not* the canonical deterministic wire format for Echo networking. -//! - In particular, maps are stored as `HashMap` for ergonomic interop; ordering is not stable. -//! - For canonical/deterministic transport and hashing, prefer `echo-session-proto` / `echo-graph`. +//! Shared WASM-friendly DTOs and Protocol Utilities for Echo. + +#![deny(clippy::unwrap_used)] +#![deny(clippy::expect_used)] +#![deny(clippy::panic)] use serde::{Deserialize, Serialize}; use std::collections::HashMap; -/// Node identifier used in the living-spec demos. +pub mod canonical; +pub use canonical::{CanonError, decode_value, encode_value}; + +pub mod eintlog; +pub use eintlog::*; + +/// Errors produced by the Intent Envelope parser. +#[derive(Debug, PartialEq, Eq)] +pub enum EnvelopeError { + /// The 4-byte magic header "EINT" was missing or incorrect. + InvalidMagic, + /// The buffer was too short to contain the header or the declared payload. + TooShort, + /// The buffer length did not match the length declared in the header. + LengthMismatch, + /// Internal structure of the envelope was malformed (e.g. invalid integer encoding). + Malformed, + /// Payload length exceeds u32::MAX. + PayloadTooLarge, +} + +/// Packs an application-blind intent envelope v1. +/// Layout: "EINT" (4 bytes) + op_id (u32 LE) + vars_len (u32 LE) + vars /// -/// Uses a `String` rather than an integer to keep JS/WASM interop simple and ergonomic. -pub type NodeId = String; +/// # Errors +/// Returns `PayloadTooLarge` if `vars.len()` exceeds `u32::MAX`. +pub fn pack_intent_v1(op_id: u32, vars: &[u8]) -> Result, EnvelopeError> { + let vars_len: u32 = vars + .len() + .try_into() + .map_err(|_| EnvelopeError::PayloadTooLarge)?; + let mut out = Vec::with_capacity(12 + vars.len()); + out.extend_from_slice(b"EINT"); + out.extend_from_slice(&op_id.to_le_bytes()); + out.extend_from_slice(&vars_len.to_le_bytes()); + out.extend_from_slice(vars); + Ok(out) +} + +/// Unpacks an application-blind intent envelope v1. +/// Returns (op_id, vars_slice). +pub fn unpack_intent_v1(bytes: &[u8]) -> Result<(u32, &[u8]), EnvelopeError> { + if bytes.len() < 12 { + return Err(EnvelopeError::TooShort); + } + if &bytes[0..4] != b"EINT" { + return Err(EnvelopeError::InvalidMagic); + } + + let op_id_bytes: [u8; 4] = bytes[4..8] + .try_into() + .map_err(|_| EnvelopeError::Malformed)?; + let op_id = u32::from_le_bytes(op_id_bytes); + + let vars_len_bytes: [u8; 4] = bytes[8..12] + .try_into() + .map_err(|_| EnvelopeError::Malformed)?; + let vars_len = u32::from_le_bytes(vars_len_bytes) as usize; + + // Prevent integer overflow on 32-bit systems (though vars_len is u32, usize might be u32) + let required_len = 12usize + .checked_add(vars_len) + .ok_or(EnvelopeError::TooShort)?; + + if bytes.len() < required_len { + return Err(EnvelopeError::TooShort); + } + if bytes.len() > required_len { + return Err(EnvelopeError::LengthMismatch); + } + + Ok((op_id, &bytes[12..])) +} + +// ----------------------------------------------------------------------------- +// Legacy DTOs (Retained for cross-repo compatibility, to be purged later) +// ----------------------------------------------------------------------------- + +/// Encode any serde value into deterministic CBOR bytes. +pub fn encode_cbor(value: &T) -> Result, CanonError> { + let val = serde_value::to_value(value).map_err(|e| CanonError::Encode(e.to_string()))?; + let canon = sv_to_cv(val)?; + encode_value(&canon) +} + +/// Decode deterministic CBOR bytes into a serde value. +pub fn decode_cbor Deserialize<'de>>(bytes: &[u8]) -> Result { + let val = decode_value(bytes)?; + let sv = cv_to_sv(val)?; + T::deserialize(sv).map_err(|e| CanonError::Decode(e.to_string())) +} + +fn sv_to_cv(val: serde_value::Value) -> Result { + use ciborium::value::Value as CV; + use serde_value::Value::*; + Ok(match val { + Bool(b) => CV::Bool(b), + I8(n) => CV::Integer((n as i64).into()), + I16(n) => CV::Integer((n as i64).into()), + I32(n) => CV::Integer((n as i64).into()), + I64(n) => CV::Integer(n.into()), + U8(n) => CV::Integer((n as u64).into()), + U16(n) => CV::Integer((n as u64).into()), + U32(n) => CV::Integer((n as u64).into()), + U64(n) => CV::Integer(n.into()), + F32(f) => CV::Float(f as f64), + F64(f) => CV::Float(f), + Char(c) => CV::Text(c.to_string()), + String(s) => CV::Text(s), + Bytes(b) => CV::Bytes(b), + Unit => CV::Null, + Option(None) => CV::Null, + Option(Some(v)) => sv_to_cv(*v)?, + Newtype(v) => sv_to_cv(*v)?, + Seq(vs) => { + let mut out = Vec::with_capacity(vs.len()); + for v in vs { + out.push(sv_to_cv(v)?); + } + CV::Array(out) + } + Map(m) => { + let mut out = Vec::with_capacity(m.len()); + for (k, v) in m { + out.push((sv_to_cv(k)?, sv_to_cv(v)?)); + } + CV::Map(out) + } + }) +} + +fn cv_to_sv(val: ciborium::value::Value) -> Result { + use ciborium::value::Value as CV; + use serde_value::Value as SV; + Ok(match val { + CV::Bool(b) => SV::Bool(b), + CV::Null => SV::Unit, + CV::Integer(i) => { + let n: i128 = i.into(); + // Convert non-negative i128 to u64 if it fits + if n >= 0 + && let Ok(v) = u64::try_from(n) + { + return Ok(SV::U64(v)); + } + if let Ok(v) = i64::try_from(n) { + SV::I64(v) + } else { + return Err(CanonError::Decode("integer out of range".into())); + } + } + CV::Float(f) => SV::F64(f), + CV::Text(s) => SV::String(s), + CV::Bytes(b) => SV::Bytes(b), + CV::Array(vs) => { + let mut out = Vec::with_capacity(vs.len()); + for v in vs { + out.push(cv_to_sv(v)?); + } + SV::Seq(out) + } + CV::Map(entries) => { + let mut map = std::collections::BTreeMap::new(); + for (k, v) in entries { + map.insert(cv_to_sv(k)?, cv_to_sv(v)?); + } + SV::Map(map) + } + CV::Tag(_, _) => return Err(CanonError::Decode("tags not supported".into())), + _ => return Err(CanonError::Decode("unsupported value".into())), + }) +} -/// Field name used in the living-spec demos. +/// Unique identifier for a graph node. +pub type NodeId = String; +/// Name of a field within a node. pub type FieldName = String; -/// Simple tagged value for demo/spec transfer. -/// -/// Serialized as `{ "kind": "...", "value": ... }` to make the JS-side shape explicit. +/// A typed value that can be stored in a node field. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind", content = "value")] pub enum Value { - /// UTF-8 string value. + /// String value. Str(String), - /// 64-bit integer. + /// Numeric value (64-bit signed integer). Num(i64), /// Boolean value. Bool(bool), - /// Explicit null. + /// Null/absent value. Null, } -/// Graph node with arbitrary fields. -/// -/// Invariants: -/// -/// - `id` should be unique within a [`WarpGraph`] (not enforced by the type). -/// - `fields` is an unordered bag of per-node values intended for UI/demo state. +/// A node in the warp graph with an ID and field map. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Node { - /// Stable node identifier. + /// Unique identifier for this node. pub id: NodeId, - /// Field map (unordered). + /// Map of field names to their values. pub fields: HashMap, } -/// Graph edge (directed). -/// -/// In the demo, edges are not required to be unique and are not validated against the node set. +/// A directed edge between two nodes. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Edge { - /// Source node id. + /// Source node ID. pub from: NodeId, - /// Target node id. + /// Target node ID. pub to: NodeId, } -/// Minimal WARP graph view for the WASM demo. -/// -/// This is the “teaching graph” representation used by Spec-000 and friends, not the canonical -/// engine graph. +/// A graph structure containing nodes and edges. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct WarpGraph { - /// Node map keyed by id (unordered). + /// Map of node IDs to nodes. pub nodes: HashMap, - /// Edges (directed). + /// List of directed edges. pub edges: Vec, } -/// Semantic operation kinds for rewrites. -/// -/// These are high-level demo operations, used to label [`Rewrite`] records. +/// The type of semantic operation in a rewrite. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum SemanticOp { - /// Set/overwrite a field value on a node. + /// Set a field value on a node. Set, - /// Add a new node. + /// Add a new node to the graph. AddNode, - /// Delete/tombstone a node. + /// Delete an existing node from the graph. DeleteNode, - /// Add a directed edge. + /// Create an edge between two nodes. Connect, - /// Remove a directed edge. + /// Remove an edge between two nodes. Disconnect, } -/// Rewrite record (append-only). -/// -/// This is the minimal “history entry” the living specs append when mutating the demo graph. -/// -/// Invariants and conventions: -/// -/// - `id` is expected to be monotonic within a single history (the demo kernel uses `0..n`). -/// - `target` is the primary node id the operation is about. -/// - `subject` is an optional secondary identifier (e.g., field name for `Set`). -/// - `old_value` / `new_value` are intentionally generic to keep the DTO small; their meaning is -/// operation-dependent (see below). -/// -/// Operation field semantics (Spec-000 demo conventions): -/// -/// - [`SemanticOp::AddNode`]: `target = node_id`, values are `None`. -/// - [`SemanticOp::DeleteNode`]: `target = node_id`, values are `None`. -/// - [`SemanticOp::Set`]: `target = node_id`, `subject = Some(field_name)`, -/// `old_value = Some(prior_value)` (or `None`), and `new_value = Some(new_value)`. -/// - [`SemanticOp::Connect`]: `target = from_id`, `new_value = Some(Value::Str(to_id))`. -/// - [`SemanticOp::Disconnect`]: same encoding as `Connect`, but interpreted as removal. +/// A single rewrite operation describing a graph mutation. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Rewrite { - /// Monotonic rewrite id within history. + /// Unique identifier for this rewrite operation. pub id: u64, - /// Operation kind. + /// The type of operation. pub op: SemanticOp, - /// Target node id. + /// The target node ID. pub target: NodeId, - /// Optional secondary identifier for the operation. - /// - /// For [`SemanticOp::Set`] this is the field name. + /// Optional subject (e.g., field name or connected node). pub subject: Option, - /// Prior value (if any). + /// Previous value before the operation (for Set operations). pub old_value: Option, - /// New value (if any). + /// New value after the operation (for Set operations). pub new_value: Option, } #[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { use super::*; #[test] - fn serialize_rewrite_round_trips_across_ops() { - let cases = [ - Rewrite { - id: 1, - op: SemanticOp::AddNode, - target: "A".into(), - subject: None, - old_value: None, - new_value: None, - }, - Rewrite { - id: 2, - op: SemanticOp::Set, - target: "A".into(), - subject: Some("name".into()), - old_value: Some(Value::Str("Prior".into())), - new_value: Some(Value::Str("Server".into())), - }, - Rewrite { - id: 3, - op: SemanticOp::Connect, - target: "A".into(), - subject: None, - old_value: None, - new_value: Some(Value::Str("B".into())), - }, - Rewrite { - id: 4, - op: SemanticOp::Disconnect, - target: "A".into(), - subject: None, - old_value: None, - new_value: Some(Value::Str("B".into())), - }, - Rewrite { - id: 5, - op: SemanticOp::DeleteNode, - target: "A".into(), - subject: None, - old_value: None, - new_value: None, - }, - ]; - - for rw in cases { - let json = serde_json::to_string(&rw).expect("serialize"); - let back: Rewrite = serde_json::from_str(&json).expect("deserialize"); - assert_eq!(rw, back); - } + fn test_pack_unpack_round_trip() { + let op_id = 12345; + let vars = b"test payload"; + let packed = pack_intent_v1(op_id, vars).expect("pack failed"); + + // Verify structure: "EINT" + op_id(4) + len(4) + payload + assert_eq!(&packed[0..4], b"EINT"); + assert_eq!(&packed[4..8], &op_id.to_le_bytes()); + assert_eq!(&packed[8..12], &(vars.len() as u32).to_le_bytes()); + assert_eq!(&packed[12..], vars); + + // Round trip + let (out_op, out_vars) = unpack_intent_v1(&packed).expect("unpack failed"); + assert_eq!(out_op, op_id); + assert_eq!(out_vars, vars); + } + + #[test] + fn test_unpack_errors() { + // Too short for header + assert_eq!(unpack_intent_v1(b"EINT"), Err(EnvelopeError::TooShort)); + + // Invalid magic + assert_eq!( + unpack_intent_v1(b"XXXX\x00\x00\x00\x00\x00\x00\x00\x00"), + Err(EnvelopeError::InvalidMagic) + ); + + // Payload shorter than declared length + let mut short = Vec::new(); + short.extend_from_slice(b"EINT"); + short.extend_from_slice(&1u32.to_le_bytes()); // op_id + short.extend_from_slice(&10u32.to_le_bytes()); // declared len 10 + short.extend_from_slice(b"123"); // actual len 3 + assert_eq!(unpack_intent_v1(&short), Err(EnvelopeError::TooShort)); + + // Payload longer than declared length + let mut long = Vec::new(); + long.extend_from_slice(b"EINT"); + long.extend_from_slice(&1u32.to_le_bytes()); // op_id + long.extend_from_slice(&3u32.to_le_bytes()); // declared len 3 + long.extend_from_slice(b"12345"); // actual len 5 + assert_eq!(unpack_intent_v1(&long), Err(EnvelopeError::LengthMismatch)); + } + + #[test] + fn test_empty_vars() { + let packed = pack_intent_v1(99, &[]).unwrap(); + let (op, vars) = unpack_intent_v1(&packed).unwrap(); + assert_eq!(op, 99); + assert_eq!(vars, &[]); } } diff --git a/crates/echo-wasm-abi/tests/canonical_vectors.rs b/crates/echo-wasm-abi/tests/canonical_vectors.rs new file mode 100644 index 00000000..cbf49d5f --- /dev/null +++ b/crates/echo-wasm-abi/tests/canonical_vectors.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Golden vectors and rejection cases for canonical CBOR encoding/decoding. + +use echo_wasm_abi::{decode_cbor, encode_cbor}; +use serde::{Deserialize, Serialize}; +use serde_value::Value; + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +/// Minimal test fixture used to validate canonical CBOR round-trips. +struct Sample { + a: u8, + b: bool, +} + +#[test] +fn golden_sample_map() { + let sample = Sample { a: 1, b: true }; + let bytes = encode_cbor(&sample).expect("encode"); + // Expected canonical CBOR: { "a": 1, "b": true } + // a2 61 61 01 61 62 f5 + let expected: &[u8] = &[0xa2, 0x61, 0x61, 0x01, 0x61, 0x62, 0xf5]; + assert_eq!(bytes, expected); + let back: Sample = decode_cbor(&bytes).expect("decode"); + assert_eq!(sample, back); +} + +#[test] +fn reject_non_canonical_int_width() { + // Map { "x": 1 } but 1 is encoded as 0x18 0x01 (non-minimal) + let bytes: &[u8] = &[0xa1, 0x61, 0x78, 0x18, 0x01]; + let err = decode_cbor::(bytes).unwrap_err(); + assert!( + format!("{err:?}").contains("NonCanonicalInt"), + "expected NonCanonicalInt, got {err:?}" + ); +} + +#[test] +fn reject_map_key_order() { + // Map { "b":1, "a":2 } (keys not sorted) + let bytes: &[u8] = &[0xa2, 0x61, 0x62, 0x01, 0x61, 0x61, 0x02]; + let err = decode_cbor::(bytes).unwrap_err(); + assert!( + format!("{err:?}").contains("MapKeyOrder"), + "expected MapKeyOrder, got {err:?}" + ); +} diff --git a/crates/echo-wasm-abi/tests/fuzz_wire.rs b/crates/echo-wasm-abi/tests/fuzz_wire.rs new file mode 100644 index 00000000..f8321e18 --- /dev/null +++ b/crates/echo-wasm-abi/tests/fuzz_wire.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Fuzz tests for wire protocol parsing robustness. + +use echo_wasm_abi::unpack_intent_v1; +use proptest::prelude::*; + +proptest! { + #[test] + fn fuzz_unpack_intent_v1_no_panics(bytes in prop::collection::vec(any::(), 0..1024)) { + // The goal is simply to ensure this does not panic. + let _ = unpack_intent_v1(&bytes); + } + + #[test] + fn fuzz_unpack_valid_structure_garbage_payload( + op_id in any::(), + len in 0u32..1000, + payload in prop::collection::vec(any::(), 0..1000) + ) { + let mut data = Vec::new(); + data.extend_from_slice(b"EINT"); + data.extend_from_slice(&op_id.to_le_bytes()); + data.extend_from_slice(&len.to_le_bytes()); + data.extend_from_slice(&payload); + + let res = unpack_intent_v1(&data); + + if payload.len() == len as usize { + // Should succeed + prop_assert!(res.is_ok()); + let (out_op, out_vars) = res.unwrap(); + prop_assert_eq!(out_op, op_id); + prop_assert_eq!(out_vars, &payload[..]); + } else { + // Should fail cleanly + prop_assert!(res.is_err()); + } + } +} diff --git a/crates/echo-wasm-abi/tests/non_canonical_floats.rs b/crates/echo-wasm-abi/tests/non_canonical_floats.rs new file mode 100644 index 00000000..2676393c --- /dev/null +++ b/crates/echo-wasm-abi/tests/non_canonical_floats.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Tests for canonical floating-point decoding. +//! +//! Verifies that the decoder rejects non-canonical floating-point representations +//! (e.g. floats that should be integers, or f64s that fit in f32/f16). + +use echo_wasm_abi::{CanonError, decode_value}; + +#[test] +fn test_rejects_non_canonical_floats() { + // 1.0 encoded as f32 (0xfa3f800000) + // Should be integer 1 (0x01) + let one_f32 = vec![0xfa, 0x3f, 0x80, 0x00, 0x00]; + let res = decode_value(&one_f32); + assert_eq!( + res.unwrap_err(), + CanonError::FloatShouldBeInt, + "1.0 as f32 should be rejected" + ); + + // 1.5 encoded as f64 (0xfb3ff8000000000000) + // Should be f16 (0xf93e00) or f32 + // 1.5 is 0x3fc00000 in f32, 0x3e00 in f16 + let one_point_five_f64 = vec![0xfb, 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + let res = decode_value(&one_point_five_f64); + assert_eq!( + res.unwrap_err(), + CanonError::NonCanonicalFloat, + "1.5 as f64 should be rejected (fits in f16/f32)" + ); + + // 1.5 encoded as f32 (0xfa3fc00000) + // Should be f16 (0xf93e00) + let one_point_five_f32 = vec![0xfa, 0x3f, 0xc0, 0x00, 0x00]; + let res = decode_value(&one_point_five_f32); + assert_eq!( + res.unwrap_err(), + CanonError::NonCanonicalFloat, + "1.5 as f32 should be rejected (fits in f16)" + ); +} + +#[test] +fn test_rejects_int_as_float() { + // 42.0 encoded as f16 (0xf95140) + // Should be integer 42 (0x18 0x2a) + let forty_two_f16 = vec![0xf9, 0x51, 0x40]; + let res = decode_value(&forty_two_f16); + assert_eq!( + res.unwrap_err(), + CanonError::FloatShouldBeInt, + "42.0 as f16 should be rejected" + ); +} diff --git a/crates/echo-wasm-bindings/Cargo.toml b/crates/echo-wasm-bindings/Cargo.toml index 6fd66a2c..5879397d 100644 --- a/crates/echo-wasm-bindings/Cargo.toml +++ b/crates/echo-wasm-bindings/Cargo.toml @@ -15,8 +15,11 @@ categories = ["wasm", "encoding", "api-bindings"] [dependencies] echo-wasm-abi.workspace = true serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +serde-wasm-bindgen = "0.6" wasm-bindgen = { version = "0.2", optional = true } +[dev-dependencies] +serde_json = "1.0" + [features] wasm = ["wasm-bindgen"] diff --git a/crates/echo-wasm-bindings/src/lib.rs b/crates/echo-wasm-bindings/src/lib.rs index 66f7ef9c..7d872892 100644 --- a/crates/echo-wasm-bindings/src/lib.rs +++ b/crates/echo-wasm-bindings/src/lib.rs @@ -17,7 +17,7 @@ use wasm_bindgen::prelude::*; /// /// - It owns an in-memory [`WarpGraph`] and an append-only [`Rewrite`] history. /// - It is designed for JS/WASM interop: when built with `--features wasm`, the type is exposed -/// via `wasm-bindgen` and provides JSON serializers (see `serializeGraph` / `serializeHistory`). +/// via `wasm-bindgen` and provides JS object accessors. /// - It is not the production Echo kernel, does not validate invariants, and does not implement /// canonical hashing / deterministic encoding. /// @@ -76,29 +76,6 @@ impl DemoKernel { }); } - /// Set a field value on a node. - /// - /// Records a [`Rewrite`] with: - /// - /// - `target = node_id` - /// - `subject = Some(field_name)` - /// - `old_value = prior field value` (or `None` if the field was missing) - /// - `new_value = new field value` - pub fn set_field(&mut self, target: String, field: String, value: Value) { - if let Some(node) = self.graph.nodes.get_mut(&target) { - let prior_value = node.fields.get(&field).cloned(); - node.fields.insert(field.clone(), value.clone()); - self.history.push(Rewrite { - id: self.history.len() as u64, - op: SemanticOp::Set, - target, - subject: Some(field), - old_value: prior_value, - new_value: Some(value), - }); - } - } - /// Add a directed edge between two nodes. pub fn connect(&mut self, from: String, to: String) { if !self.graph.nodes.contains_key(&from) || !self.graph.nodes.contains_key(&to) { @@ -137,42 +114,56 @@ impl DemoKernel { }); } } +} - /// Get a clone of the current graph (host use). - pub fn graph(&self) -> WarpGraph { - self.graph.clone() - } - - /// Get a clone of the rewrite history. - pub fn history(&self) -> Vec { - self.history.clone() +// Separate impl for set_field to handle WASM vs Host types for 'value'. +#[cfg(feature = "wasm")] +#[wasm_bindgen] +impl DemoKernel { + /// Set a field value on a node (WASM version). + #[wasm_bindgen(js_name = setField)] + pub fn set_field_wasm(&mut self, target: String, field: String, value: JsValue) { + let val: Value = serde_wasm_bindgen::from_value(value).unwrap_or(Value::Null); + self.set_field(target, field, val); } - /// Serialize graph to JSON (host use). - pub fn graph_json(&self) -> String { - serde_json::to_string(&self.graph) - .unwrap_or_else(|_| "{\"nodes\":{},\"edges\":[]}".to_string()) + /// Export the current graph state as a JS object. + #[wasm_bindgen(js_name = getGraph)] + pub fn get_graph_wasm(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.graph).unwrap_or(JsValue::NULL) } - /// Serialize history to JSON (host use). - pub fn history_json(&self) -> String { - serde_json::to_string(&self.history).unwrap_or_else(|_| "[]".to_string()) + /// Export the rewrite history as a JS array. + #[wasm_bindgen(js_name = getHistory)] + pub fn get_history_wasm(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.history).unwrap_or(JsValue::NULL) } } -// Expose JSON helpers to WASM when feature enabled. -#[cfg(feature = "wasm")] -#[wasm_bindgen] impl DemoKernel { - /// Get graph as JSON string (JS/WASM use). - #[wasm_bindgen(js_name = serializeGraph)] - pub fn serialize_graph(&self) -> String { - self.graph_json() + /// Set a field value on a node (Host version). + pub fn set_field(&mut self, target: String, field: String, value: Value) { + if let Some(node) = self.graph.nodes.get_mut(&target) { + let prior_value = node.fields.get(&field).cloned(); + node.fields.insert(field.clone(), value.clone()); + self.history.push(Rewrite { + id: self.history.len() as u64, + op: SemanticOp::Set, + target, + subject: Some(field), + old_value: prior_value, + new_value: Some(value), + }); + } + } + + /// Get a reference to the current graph. + pub fn graph(&self) -> &WarpGraph { + &self.graph } - /// Get history as JSON string (JS/WASM use). - #[wasm_bindgen(js_name = serializeHistory)] - pub fn serialize_history(&self) -> String { - self.history_json() + /// Get a reference to the rewrite history. + pub fn history(&self) -> &[Rewrite] { + &self.history } } diff --git a/crates/echo-wesley-gen/Cargo.toml b/crates/echo-wesley-gen/Cargo.toml new file mode 100644 index 00000000..55f9a549 --- /dev/null +++ b/crates/echo-wesley-gen/Cargo.toml @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +[package] +name = "echo-wesley-gen" +version = "0.1.0" +edition = "2021" +rust-version = "1.90.0" +description = "Code generator for Echo: consumes Wesley IR (JSON) and emits Rust artifacts via syn/quote." +license = "Apache-2.0" +repository = "https://github.com/flyingrobots/echo" +readme = "README.md" +keywords = ["warp", "echo", "codegen", "wesley", "wasm"] +categories = ["development-tools", "command-line-utilities", "encoding", "wasm"] + +[dependencies] +anyhow = "1.0" +clap = { version = "4.4", features = ["derive"] } +proc-macro2 = "1.0" +quote = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } +prettyplease = "0.2" diff --git a/crates/echo-wesley-gen/README.md b/crates/echo-wesley-gen/README.md new file mode 100644 index 00000000..e9c84c6e --- /dev/null +++ b/crates/echo-wesley-gen/README.md @@ -0,0 +1,24 @@ + + + +# echo-wesley-gen + +CLI tool that reads Wesley IR (JSON) from stdin and emits Rust structs/enums +for Echo. Intended to be driven by the JavaScript generator (packages/wesley-generator-echo) +which now outputs `ir.json` instead of handwritten Rust. + +## Usage + +```bash +# Generate Rust code to stdout +cat ir.json | cargo run -p echo-wesley-gen -- + +# Write to a file +cat ir.json | cargo run -p echo-wesley-gen -- --out generated.rs +``` + +## Notes + +- Supports ENUM and OBJECT kinds from Wesley IR. +- Optional fields become `Option`; lists become `Vec` (wrapped in Option when not required). +- Unknown scalar names are emitted as identifiers as-is (so ensure upstream IR types are valid Rust idents). diff --git a/crates/echo-wesley-gen/src/ir.rs b/crates/echo-wesley-gen/src/ir.rs new file mode 100644 index 00000000..bd686881 --- /dev/null +++ b/crates/echo-wesley-gen/src/ir.rs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Minimal Wesley IR structs used by echo-wesley-gen. + +use serde::Deserialize; + +/// Root Wesley IR payload consumed by `echo-wesley-gen`. +/// +/// This is a minimal, serde-friendly representation of the IR JSON emitted by +/// the upstream generator. Unknown fields are ignored by serde; missing fields +/// are defaulted where sensible so the CLI can be tolerant of additive schema +/// changes. +#[derive(Debug, Deserialize)] +pub struct WesleyIR { + /// IR schema version tag (e.g. `"echo-ir/v1"`). + #[serde(default)] + pub ir_version: Option, + /// Provenance metadata describing the toolchain that produced this IR. + #[serde(default)] + #[allow(dead_code)] // Part of IR spec, present for deserialization + pub generated_by: Option, + /// Optional schema hash of the source GraphQL schema (hex). + #[serde(default)] + pub schema_sha256: Option, + /// Type catalog (enums, objects, etc.) referenced by operations. + #[serde(default)] + pub types: Vec, + /// Operation catalog (query/mutation) with stable op ids and argument info. + #[serde(default)] + pub ops: Vec, + /// Canonical codec identifier for encoding/decoding op argument payloads. + #[serde(default)] + pub codec_id: Option, + /// Registry layout version (bumped for breaking changes in the generated output). + #[serde(default)] + pub registry_version: Option, +} + +/// Generator provenance metadata embedded in the IR. +#[derive(Debug, Deserialize)] +#[allow(dead_code)] // Part of IR spec, present for deserialization +pub struct GeneratedBy { + /// Tool name (package/binary) that produced this IR. + pub tool: String, + #[serde(default)] + /// Optional tool version. + pub version: Option, +} + +/// Type definition in the IR type catalog. +#[derive(Debug, Deserialize)] +pub struct TypeDefinition { + /// GraphQL type name. + pub name: String, + /// Kind tag (object vs enum, etc.). + pub kind: TypeKind, + /// Object fields (empty for non-object kinds). + #[serde(default)] + pub fields: Vec, + /// Enum values (empty for non-enum kinds). + #[serde(default)] + pub values: Vec, // For enums +} + +/// Kind tag for IR type definitions. +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TypeKind { + /// GraphQL object type. + Object, + /// GraphQL enum type. + Enum, + /// GraphQL scalar type. + Scalar, + /// GraphQL interface type. + Interface, + /// GraphQL union type. + Union, + /// GraphQL input object type. + InputObject, +} + +/// Operation kind (query or mutation). +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OpKind { + /// Read-only operation. + Query, + /// State-mutating operation. + Mutation, +} + +/// Operation definition in the IR operation catalog. +#[derive(Debug, Deserialize)] +pub struct OpDefinition { + /// Operation kind. + pub kind: OpKind, + /// GraphQL operation name. + pub name: String, + /// Persisted operation identifier. + pub op_id: u32, + /// Argument catalog for strict input validation. + #[serde(default)] + pub args: Vec, + /// GraphQL result type name. + pub result_type: String, +} + +/// Argument definition (used for both operation args and object fields). +/// +/// The Wesley IR currently represents operation args and object fields with +/// the same shape (name + base type + required + list). We keep distinct Rust +/// wrapper types (`ArgDefinition` and `FieldDefinition`) so call sites can +/// remain semantically explicit even if the JSON schema evolves. +#[derive(Debug, Deserialize)] +pub struct ArgDefinition { + /// Field/argument name. + pub name: String, + #[serde(rename = "type")] + /// GraphQL base type name (e.g. `"String"`, `"Theme"`, `"AppState"`). + pub type_name: String, + /// Whether the argument is required. + pub required: bool, + /// Whether the argument is a list. + #[serde(default)] + pub list: bool, +} + +/// Object field definition (same schema as [`ArgDefinition`]; kept for semantic clarity). +#[derive(Debug, Deserialize)] +pub struct FieldDefinition { + /// Field name. + pub name: String, + #[serde(rename = "type")] + /// GraphQL base type name. + pub type_name: String, + /// Whether the field is required. + pub required: bool, + /// Whether the field is a list. + #[serde(default)] + pub list: bool, +} diff --git a/crates/echo-wesley-gen/src/main.rs b/crates/echo-wesley-gen/src/main.rs new file mode 100644 index 00000000..63480c7d --- /dev/null +++ b/crates/echo-wesley-gen/src/main.rs @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! CLI that reads Wesley IR JSON from stdin and emits Rust structs/enums for Echo. + +use anyhow::Result; +use clap::Parser; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use std::io::{self, Read}; + +/// Create an identifier safely, falling back to a raw identifier for Rust keywords. +fn safe_ident(name: &str) -> proc_macro2::Ident { + syn::parse_str::(name) + .unwrap_or_else(|_| proc_macro2::Ident::new_raw(name, proc_macro2::Span::call_site())) +} + +mod ir; +use ir::{OpKind, TypeKind, WesleyIR}; + +#[derive(Parser)] +#[command( + author, + version, + about = "Generates Echo Rust artifacts from Wesley IR" +)] +struct Args { + /// Optional output path (defaults to stdout) + #[arg(short, long)] + out: Option, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + + let ir: WesleyIR = serde_json::from_str(&buffer)?; + validate_version(&ir)?; + let code = generate_rust(&ir)?; + + if let Some(path) = args.out { + std::fs::write(path, code)?; + } else { + println!("{}", code); + } + + Ok(()) +} + +fn generate_rust(ir: &WesleyIR) -> Result { + let mut tokens = quote! { + // Generated by echo-wesley-gen. Do not edit. + use serde::{Serialize, Deserialize}; + }; + + // Metadata constants + let schema_sha = ir.schema_sha256.as_deref().unwrap_or(""); + let codec_id = ir.codec_id.as_deref().unwrap_or("cbor-canon-v1"); + let registry_version = ir.registry_version.unwrap_or(1); + + tokens.extend(quote! { + pub const SCHEMA_SHA256: &str = #schema_sha; + pub const CODEC_ID: &str = #codec_id; + pub const REGISTRY_VERSION: u32 = #registry_version; + }); + + for type_def in &ir.types { + let name = safe_ident(&type_def.name); + + match type_def.kind { + TypeKind::Enum => { + let variants = type_def.values.iter().map(|v| safe_ident(v)); + tokens.extend(quote! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] + pub enum #name { + #(#variants),* + } + }); + } + TypeKind::Object => { + let fields = type_def.fields.iter().map(|f| { + let field_name = safe_ident(&f.name); + let base_ty = map_type(&f.type_name); + let list_ty: TokenStream = if f.list { + quote! { Vec<#base_ty> } + } else { + quote! { #base_ty } + }; + + if f.required { + quote! { pub #field_name: #list_ty } + } else { + quote! { pub #field_name: Option<#list_ty> } + } + }); + + tokens.extend(quote! { + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct #name { + #(#fields),* + } + }); + } + _ => {} // Ignore scalars/interfaces for now + } + } + + if !ir.ops.is_empty() { + tokens.extend(quote! { + // Registry provider types (Echo runtime loads an app-supplied implementation). + use echo_registry_api::{ArgDef, EnumDef, ObjectDef, OpDef, OpKind, RegistryInfo, RegistryProvider}; + }); + + let mut enum_defs: Vec<_> = ir + .types + .iter() + .filter(|t| t.kind == TypeKind::Enum) + .collect(); + enum_defs.sort_unstable_by(|a, b| a.name.cmp(&b.name)); + + for en in &enum_defs { + let values_ident = format_ident!("ENUM_{}_VALUES", en.name.to_ascii_uppercase()); + let values = en.values.iter(); + tokens.extend(quote! { + pub const #values_ident: &[&str] = &[ + #(#values),* + ]; + }); + } + + let enum_entries = enum_defs.iter().map(|en| { + let name = &en.name; + let values_ident = format_ident!("ENUM_{}_VALUES", en.name.to_ascii_uppercase()); + quote! { EnumDef { name: #name, values: #values_ident } } + }); + + tokens.extend(quote! { + pub const ENUMS: &[EnumDef] = &[ + #(#enum_entries),* + ]; + }); + + let mut obj_defs: Vec<_> = ir + .types + .iter() + .filter(|t| t.kind == TypeKind::Object) + .collect(); + obj_defs.sort_unstable_by(|a, b| a.name.cmp(&b.name)); + + for obj in &obj_defs { + let fields_ident = format_ident!("OBJ_{}_FIELDS", obj.name.to_ascii_uppercase()); + let fields = obj.fields.iter().map(|f| { + let name = &f.name; + let ty = &f.type_name; + let required = f.required; + let list = f.list; + quote! { ArgDef { name: #name, ty: #ty, required: #required, list: #list } } + }); + tokens.extend(quote! { + pub const #fields_ident: &[ArgDef] = &[ + #(#fields),* + ]; + }); + } + + let obj_entries = obj_defs.iter().map(|obj| { + let name = &obj.name; + let fields_ident = format_ident!("OBJ_{}_FIELDS", obj.name.to_ascii_uppercase()); + quote! { ObjectDef { name: #name, fields: #fields_ident } } + }); + + tokens.extend(quote! { + pub const OBJECTS: &[ObjectDef] = &[ + #(#obj_entries),* + ]; + }); + + let mut ops_sorted: Vec<_> = ir.ops.iter().collect(); + ops_sorted.sort_unstable_by_key(|op| op.op_id); + + // Op ID constants + arg descriptors (sorted by op_id for deterministic iteration). + for op in &ops_sorted { + let const_name = op_const_ident(&op.name, op.op_id); + let args_name = format_ident!("{}_ARGS", const_name); + let op_id = op.op_id; + let args = op.args.iter().map(|a| { + let name = &a.name; + let ty = &a.type_name; + let required = a.required; + let list = a.list; + quote! { ArgDef { name: #name, ty: #ty, required: #required, list: #list } } + }); + tokens.extend(quote! { + pub const #const_name: u32 = #op_id; + pub const #args_name: &[ArgDef] = &[ + #(#args),* + ]; + }); + } + + // OPS table (sorted by op_id). + let ops_entries = ops_sorted.iter().map(|op| { + let kind = match op.kind { + OpKind::Query => quote! { OpKind::Query }, + OpKind::Mutation => quote! { OpKind::Mutation }, + }; + let name = &op.name; + let op_id = op.op_id; + let args_name = format_ident!("{}_ARGS", op_const_ident(&op.name, op.op_id)); + let result_ty = &op.result_type; + quote! { OpDef { kind: #kind, name: #name, op_id: #op_id, args: #args_name, result_ty: #result_ty } } + }); + + tokens.extend(quote! { + pub const OPS: &[OpDef] = &[ + #(#ops_entries),* + ]; + + /// Lookup an op by ID. + pub fn op_by_id(op_id: u32) -> Option<&'static OpDef> { + OPS.iter().find(|op| op.op_id == op_id) + } + + /// Lookup an op by kind + name (useful for dev tooling, not for runtime intent routing). + pub fn op_by_name(kind: OpKind, name: &str) -> Option<&'static OpDef> { + OPS.iter().find(|op| op.kind == kind && op.name == name) + } + + /// Application-supplied registry provider implementation (generated from Wesley IR). + pub struct GeneratedRegistry; + + impl RegistryProvider for GeneratedRegistry { + fn info(&self) -> RegistryInfo { + RegistryInfo { + codec_id: CODEC_ID, + registry_version: REGISTRY_VERSION, + schema_sha256_hex: SCHEMA_SHA256, + } + } + + fn op_by_id(&self, op_id: u32) -> Option<&'static OpDef> { + op_by_id(op_id) + } + + fn all_ops(&self) -> &'static [OpDef] { + OPS + } + + fn all_enums(&self) -> &'static [EnumDef] { + ENUMS + } + + fn all_objects(&self) -> &'static [ObjectDef] { + OBJECTS + } + } + + pub static REGISTRY: GeneratedRegistry = GeneratedRegistry; + }); + } + + let syntax_tree = syn::parse2(tokens)?; + Ok(prettyplease::unparse(&syntax_tree)) +} + +fn op_const_ident(name: &str, op_id: u32) -> proc_macro2::Ident { + let mut out = String::new(); + for (i, c) in name.chars().enumerate() { + if c.is_alphanumeric() { + if c.is_uppercase() && i > 0 { + out.push('_'); + } + out.push(c.to_ascii_uppercase()); + } else { + out.push('_'); + } + } + if out.is_empty() { + return format_ident!("OP_ID_{}", op_id); + } + format_ident!("OP_{}", out) +} + +fn validate_version(ir: &WesleyIR) -> Result<()> { + const SUPPORTED: &str = "echo-ir/v1"; + match ir.ir_version.as_deref() { + Some(SUPPORTED) => Ok(()), + Some(other) => anyhow::bail!( + "Unsupported ir_version '{}'; expected '{}'. Please regenerate IR with a compatible generator.", + other, + SUPPORTED + ), + None => anyhow::bail!( + "Missing ir_version; expected '{}'. Regenerate IR with a current @wesley/generator-echo.", + SUPPORTED + ), + } +} + +/// Map a GraphQL base type name to a Rust type used in generated DTOs. +/// +/// GraphQL `Float` intentionally maps to `f32` (not `f64`) so generated types +/// integrate cleanly with Echo’s deterministic scalar foundation. +fn map_type(gql_type: &str) -> TokenStream { + match gql_type { + "Boolean" => quote! { bool }, + "String" => quote! { String }, + "Int" => quote! { i32 }, + "Float" => quote! { f32 }, + "ID" => quote! { String }, + other => { + let ident = safe_ident(other); + quote! { #ident } + } + } +} diff --git a/crates/echo-wesley-gen/tests/generation.rs b/crates/echo-wesley-gen/tests/generation.rs new file mode 100644 index 00000000..548b8425 --- /dev/null +++ b/crates/echo-wesley-gen/tests/generation.rs @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Integration test for the echo-wesley-gen CLI (Wesley IR -> Rust code). + +use std::io::Write; +use std::process::{Command, Output, Stdio}; + +/// Spawns `cargo run -p echo-wesley-gen --`, pipes `ir` to stdin, and returns the output. +fn run_wesley_gen(ir: &str) -> Output { + let mut child = Command::new("cargo") + .args(["run", "-p", "echo-wesley-gen", "--"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn cargo run"); + + let mut stdin = child.stdin.take().expect("failed to get stdin"); + stdin + .write_all(ir.as_bytes()) + .expect("failed to write to stdin"); + drop(stdin); + + child.wait_with_output().expect("failed to wait on child") +} + +#[test] +fn test_generate_from_json() { + let ir = r#"{ + "ir_version": "echo-ir/v1", + "schema_sha256": "abc123", + "codec_id": "cbor-canon-v1", + "registry_version": 7, + "types": [ + { + "name": "AppState", + "kind": "OBJECT", + "fields": [ + { "name": "theme", "type": "Theme", "required": true }, + { "name": "tags", "type": "String", "required": false, "list": true } + ] + }, + { + "name": "Theme", + "kind": "ENUM", + "values": ["LIGHT", "DARK"] + }, + { + "name": "Mutation", + "kind": "OBJECT", + "fields": [ + { "name": "setTheme", "type": "AppState", "required": true } + ] + }, + { + "name": "Query", + "kind": "OBJECT", + "fields": [ + { "name": "appState", "type": "AppState", "required": true } + ] + } + ], + "ops": [ + { "kind": "MUTATION", "name": "setTheme", "op_id": 111, "args": [], "result_type": "AppState" }, + { "kind": "QUERY", "name": "appState", "op_id": 222, "args": [], "result_type": "AppState" } + ] + }"#; + + let output = run_wesley_gen(ir); + assert!( + output.status.success(), + "CLI failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(stdout.contains("pub struct AppState")); + assert!(stdout.contains("pub enum Theme")); + assert!(stdout.contains("pub theme: Theme")); + assert!(stdout.contains("pub tags: Option>")); + assert!(stdout.contains("pub const SCHEMA_SHA256: &str = \"abc123\"")); + assert!(stdout.contains("pub const CODEC_ID: &str = \"cbor-canon-v1\"")); + assert!(stdout.contains("pub const REGISTRY_VERSION: u32 = 7")); + assert!(stdout.contains("pub const OP_SET_THEME: u32 = 111")); + assert!(stdout.contains("pub const OP_APP_STATE: u32 = 222")); + assert!(stdout.contains("use echo_registry_api::{")); + assert!(stdout.contains("pub const OPS: &[OpDef]")); + assert!(stdout.contains("pub static REGISTRY: GeneratedRegistry")); +} + +#[test] +fn test_ops_catalog_present() { + let ir = r#"{ + "ir_version": "echo-ir/v1", + "types": [], + "ops": [ + { "kind": "MUTATION", "name": "setTheme", "op_id": 123, "args": [], "result_type": "AppState" }, + { "kind": "QUERY", "name": "appState", "op_id": 456, "args": [], "result_type": "AppState" } + ] + }"#; + + let output = run_wesley_gen(ir); + assert!( + output.status.success(), + "CLI failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("pub const OP_SET_THEME: u32 = 123")); + assert!(stdout.contains("pub const OP_APP_STATE: u32 = 456")); + assert!( + stdout.contains("pub const OPS: &[OpDef]"), + "ops catalog not found in output" + ); +} + +#[test] +fn test_rejects_unknown_version() { + let ir = r#"{ + "ir_version": "echo-ir/v2", + "types": [] + }"#; + + let output = run_wesley_gen(ir); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Unsupported ir_version")); +} + +#[test] +fn test_rejects_missing_version() { + let ir = r#"{ + "types": [] + }"#; + + let output = run_wesley_gen(ir); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Missing ir_version")); +} diff --git a/crates/warp-benches/Cargo.toml b/crates/warp-benches/Cargo.toml index 38370d4a..56a44ece 100644 --- a/crates/warp-benches/Cargo.toml +++ b/crates/warp-benches/Cargo.toml @@ -14,6 +14,7 @@ readme = "README.md" criterion = { version = "0.5", default-features = false, features = ["html_reports"] } # Pin version alongside path to satisfy cargo-deny wildcard bans warp-core = { version = "0.1.0", path = "../warp-core" } +echo-dry-tests = { version = "0.1.0", path = "../echo-dry-tests" } # Patch-level pin for reproducibility while allowing security fixes; keep defaults off to avoid rayon/parallelism. blake3 = { version = "~1.8.2", default-features = false, features = ["std"] } rustc-hash = "2.1.1" diff --git a/crates/warp-benches/benches/motion_throughput.rs b/crates/warp-benches/benches/motion_throughput.rs index cf5856f7..f548d48d 100644 --- a/crates/warp-benches/benches/motion_throughput.rs +++ b/crates/warp-benches/benches/motion_throughput.rs @@ -2,10 +2,11 @@ // © James Ross Ω FLYING•ROBOTS #![allow(missing_docs)] use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput}; +use echo_dry_tests::{build_motion_demo_engine, MOTION_RULE_NAME}; use std::{hint::black_box, time::Duration}; use warp_core::{ decode_motion_atom_payload, encode_motion_atom_payload, make_node_id, make_type_id, - ApplyResult, AttachmentValue, Engine, NodeId, NodeRecord, MOTION_RULE_NAME, + ApplyResult, AttachmentValue, Engine, NodeId, NodeRecord, }; fn extract_motion_payload(engine: &Engine, id: &NodeId) -> ([f32; 3], [f32; 3]) { @@ -28,7 +29,7 @@ fn extract_motion_payload(engine: &Engine, id: &NodeId) -> ([f32; 3], [f32; 3]) fn build_engine_with_n_entities(n: usize) -> (Engine, Vec) { // Start from the demo engine (root + motion rule registered). - let mut engine = warp_core::build_motion_demo_engine(); + let mut engine = build_motion_demo_engine(); let ty = make_type_id("entity"); let mut ids = Vec::with_capacity(n); // Insert N entities with a simple payload. diff --git a/crates/warp-benches/benches/scheduler_drain.rs b/crates/warp-benches/benches/scheduler_drain.rs index dea2b16b..d403877c 100644 --- a/crates/warp-benches/benches/scheduler_drain.rs +++ b/crates/warp-benches/benches/scheduler_drain.rs @@ -13,6 +13,7 @@ //! TODO(PR-14/15): Persist JSON artifacts and add a regression gate. use blake3::Hasher; use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput}; +use echo_dry_tests::build_motion_demo_engine; use std::time::Duration; use warp_core::{ make_node_id, make_type_id, ApplyResult, ConflictPolicy, Engine, Footprint, Hash, NodeId, @@ -54,7 +55,7 @@ fn bench_noop_rule() -> RewriteRule { } fn build_engine_with_entities(n: usize) -> (Engine, Vec) { - let mut engine = warp_core::build_motion_demo_engine(); + let mut engine = build_motion_demo_engine(); // Register a no-op rule to isolate scheduler overhead from executor work. engine .register_rule(bench_noop_rule()) diff --git a/crates/warp-core/.clippy.toml b/crates/warp-core/.clippy.toml new file mode 100644 index 00000000..12462089 --- /dev/null +++ b/crates/warp-core/.clippy.toml @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +# Disallow non-deterministic serialization in warp-core. +# DETERMINISM IS PARAMOUNT: Always use echo_wasm_abi::{encode_cbor, decode_cbor} for deterministic canonical encoding. +# serde_json has non-deterministic map key ordering and is completely banned from warp-core. +disallowed-methods = [ + # Disallow direct ciborium usage (use echo_wasm_abi canonical encoder instead) + { path = "ciborium::from_reader", reason = "Use echo_wasm_abi::decode_cbor for deterministic CBOR decoding" }, + { path = "ciborium::into_writer", reason = "Use echo_wasm_abi::encode_cbor for deterministic CBOR encoding" }, + { path = "ciborium::de::from_reader", reason = "Use echo_wasm_abi::decode_cbor for deterministic CBOR decoding" }, + { path = "ciborium::ser::into_writer", reason = "Use echo_wasm_abi::encode_cbor for deterministic CBOR encoding" }, + { path = "ciborium::de::from_reader_with_buffer", reason = "Use echo_wasm_abi::decode_cbor for deterministic CBOR decoding" }, + + # TOTAL BAN: serde_json is forbidden in warp-core (non-deterministic map key ordering) + # Serialization + { path = "serde_json::to_writer", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::to_writer_pretty", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::to_string", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::to_string_pretty", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::to_vec", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::to_vec_pretty", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::to_value", reason = "serde_json banned in warp-core - use CBOR" }, + # Deserialization + { path = "serde_json::from_reader", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::from_str", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::from_slice", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::from_value", reason = "serde_json banned in warp-core - use CBOR" }, + # Streaming + { path = "serde_json::Deserializer::from_reader", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::Deserializer::from_slice", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::Deserializer::from_str", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::Deserializer::into_iter", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::Serializer::new", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::Serializer::pretty", reason = "serde_json banned in warp-core - use CBOR" }, + { path = "serde_json::Serializer::with_formatter", reason = "serde_json banned in warp-core - use CBOR" }, + # Value construction/manipulation + { path = "serde_json::Map::new", reason = "serde_json banned in warp-core - use CBOR" }, +] + +# Ban serde_json macros +disallowed-macros = [ + { path = "serde_json::json", reason = "serde_json banned in warp-core - use CBOR" }, +] + +# Ban serde_json types entirely +disallowed-types = [ + { path = "serde_json::Value", reason = "serde_json types banned in warp-core - use ciborium::Value via echo_wasm_abi" }, + { path = "serde_json::Map", reason = "serde_json types banned in warp-core - use BTreeMap" }, + { path = "serde_json::Number", reason = "serde_json types banned in warp-core" }, + { path = "serde_json::Serializer", reason = "serde_json types banned in warp-core - use CBOR" }, + { path = "serde_json::Deserializer", reason = "serde_json types banned in warp-core - use CBOR" }, + { path = "serde_json::Error", reason = "serde_json types banned in warp-core - use CBOR" }, +] diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index a4ae9cc4..2f3f2f2e 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -17,23 +17,26 @@ build = "build.rs" blake3 = "1.0" bytes = "1.0" thiserror = "1.0" -hex = { version = "0.4", optional = true } -serde = { version = "1.0", features = ["derive"], optional = true } -serde_json = { version = "1.0", optional = true } +hex = "0.4" +ciborium = "0.2" rustc-hash = "2.1.1" +serde = { version = "1.0", features = ["derive"], optional = true } [dev-dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" proptest = { version = "1.5" } libm = "0.2" +echo-dry-tests = { workspace = true } [features] default = [] # Optional regression check for PRNG sequences; off by default to avoid # freezing algorithm choices. Used only in tests guarded with `cfg(feature)`. golden_prng = [] -telemetry = ["serde", "serde_json", "hex"] +# Serde support for serializable wrappers (use ONLY with deterministic CBOR encoder). +# NEVER use with serde_json - JSON is non-deterministic and banned in warp-core. +serde = ["dep:serde", "bytes/serde"] # Scalar backend lanes (CI/test orchestration). # - `det_float`: default float32-backed lane using `F32Scalar`. diff --git a/crates/warp-core/README.md b/crates/warp-core/README.md index 482f7091..4beae0c2 100644 --- a/crates/warp-core/README.md +++ b/crates/warp-core/README.md @@ -16,6 +16,21 @@ This crate is the Rust core. See the repository root `README.md` for the full pr - Provides the foundational APIs that `warp-ffi`, `warp-wasm`, and higher-level tools build on. +## Website kernel spike (WARP graphs) + +The `warp-core` crate also contains a small “website kernel spike” used by the +`flyingrobots.dev` app: + +- `Engine::ingest_intent(intent_bytes)` ingests canonical intent envelopes into `sim/inbox`: + - `intent_id = H(intent_bytes)` is computed immediately. + - event node IDs are content-addressed by `intent_id` (arrival order is non-semantic). + - pending vs applied is tracked via `edge:pending` edges; ledger/event nodes are append-only. +- `Engine::ingest_inbox_event(seq, payload)` is a legacy compatibility wrapper: + - `seq` is ignored for identity (content addressing is by `intent_id`). + - callers should prefer `ingest_intent(intent_bytes)` for causality-first semantics. +- `sys/dispatch_inbox` drains the inbox by deleting `edge:pending` edges only (queue maintenance). +- `sys/ack_pending` consumes exactly one pending edge for an event scope (used by canonical dispatch). + ## Documentation - Core engine specs live in `docs/`: diff --git a/crates/warp-core/src/attachment.rs b/crates/warp-core/src/attachment.rs index 217158a1..ed8ffe2e 100644 --- a/crates/warp-core/src/attachment.rs +++ b/crates/warp-core/src/attachment.rs @@ -34,6 +34,7 @@ use crate::ident::{EdgeKey, NodeKey, TypeId, WarpId}; /// /// In Paper I notation, vertex attachments are `α` and edge attachments are `β`. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum AttachmentPlane { /// Vertex/node attachment plane (`α`). Alpha, @@ -52,6 +53,7 @@ impl AttachmentPlane { /// Owner identity for an attachment slot. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum AttachmentOwner { /// Attachment owned by a node. Node(NodeKey), @@ -74,6 +76,7 @@ impl AttachmentOwner { /// changes to an attachment slot (especially `Descend`) must invalidate matches /// inside descendant instances deterministically. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AttachmentKey { /// Owner of the slot. pub owner: AttachmentOwner, @@ -111,6 +114,7 @@ impl AttachmentKey { /// Stage B1 introduces [`AttachmentValue::Descend`] to model recursive WARPs as /// flattened indirection. #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum AttachmentValue { /// Depth-0 atom payload. Atom(AtomPayload), @@ -131,6 +135,7 @@ pub enum AttachmentValue { /// slicing, or rewrite applicability must be expressed as explicit skeleton /// nodes/edges/ports. #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AtomPayload { /// Type identifier describing how to interpret `bytes`. pub type_id: TypeId, diff --git a/crates/warp-core/src/cmd.rs b/crates/warp-core/src/cmd.rs new file mode 100644 index 00000000..2906a298 --- /dev/null +++ b/crates/warp-core/src/cmd.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Command rewrite rules for warp-core. +//! +//! Generic engine-level commands (e.g. system management or GC triggers) +//! belong in this module. Application-specific commands should be defined +//! in application crates and registered with the engine at runtime. diff --git a/crates/warp-core/src/demo/motion.rs b/crates/warp-core/src/demo/motion.rs deleted file mode 100644 index 537e4786..00000000 --- a/crates/warp-core/src/demo/motion.rs +++ /dev/null @@ -1,305 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// © James Ross Ω FLYING•ROBOTS -//! Demo motion rule: advances position by velocity stored in payload. - -use crate::attachment::AttachmentValue; -use crate::engine_impl::Engine; -use crate::footprint::{AttachmentSet, Footprint, IdSet, PortSet}; -use crate::graph::GraphStore; -use crate::ident::{make_node_id, make_type_id, Hash, NodeId}; -use crate::payload::{ - decode_motion_atom_payload, decode_motion_atom_payload_q32_32, encode_motion_payload_q32_32, - motion_payload_type_id, -}; -use crate::record::NodeRecord; -use crate::rule::{ConflictPolicy, PatternGraph, RewriteRule}; -// Build-time generated canonical ids (domain-separated). -include!(concat!(env!("OUT_DIR"), "/rule_ids.rs")); - -/// Rule name constant for the built-in motion update rule. -/// -/// Pass this name to [`Engine::apply`] to execute the motion update rule, -/// which advances an entity's position by its velocity. Operates on nodes -/// whose payload is a valid motion encoding. -/// -/// Canonical payload encoding is v2: -/// - 6 × `i64` Q32.32 little-endian (48 bytes). -/// - Legacy v0 decoding is supported for compatibility: -/// 6 × `f32` little-endian (24 bytes). -/// -/// Example usage (in tests): -/// ```ignore -/// let mut engine = build_motion_demo_engine(); -/// let entity_id = make_node_id("entity"); -/// // ... insert entity and payload ... -/// let tx = engine.begin(); -/// engine.apply(tx, MOTION_RULE_NAME, &entity_id)?; -/// ``` -pub const MOTION_RULE_NAME: &str = "motion/update"; - -#[cfg(feature = "det_fixed")] -/// Scalar backend using deterministic fixed-point (`DFix64`) directly. -mod motion_scalar_backend { - use crate::math::scalar::DFix64; - - pub(super) type MotionScalar = DFix64; - - /// Converts a raw Q32.32 integer directly to `DFix64`. - pub(super) fn scalar_from_raw(raw: i64) -> MotionScalar { - MotionScalar::from_raw(raw) - } - - /// Extracts the raw Q32.32 integer representation from `DFix64`. - pub(super) fn scalar_to_raw(value: MotionScalar) -> i64 { - value.raw() - } -} - -#[cfg(not(feature = "det_fixed"))] -/// Scalar backend using `F32Scalar` with Q32.32 ↔ f32 conversion. -mod motion_scalar_backend { - use crate::math::fixed_q32_32; - use crate::math::scalar::F32Scalar; - use crate::math::Scalar; - - pub(super) type MotionScalar = F32Scalar; - - /// Converts raw Q32.32 to `f32`, then wraps it in `F32Scalar`. - pub(super) fn scalar_from_raw(raw: i64) -> MotionScalar { - MotionScalar::from_f32(fixed_q32_32::to_f32(raw)) - } - - /// Unwraps `F32Scalar` to `f32`, then deterministically quantizes to Q32.32. - pub(super) fn scalar_to_raw(value: MotionScalar) -> i64 { - fixed_q32_32::from_f32(value.to_f32()) - } -} - -use motion_scalar_backend::{scalar_from_raw, scalar_to_raw}; - -fn motion_executor(store: &mut GraphStore, scope: &NodeId) { - if store.node(scope).is_none() { - return; - } - let Some(AttachmentValue::Atom(payload)) = store.node_attachment_mut(scope) else { - return; - }; - - // Supports both canonical v2 and legacy v0 payloads: - // - v2 decodes directly to Q32.32. - // - v0 deterministically quantizes f32 values to Q32.32 at the boundary. - let Some((pos_raw, vel_raw)) = decode_motion_atom_payload_q32_32(payload) else { - return; - }; - - let mut pos = [ - scalar_from_raw(pos_raw[0]), - scalar_from_raw(pos_raw[1]), - scalar_from_raw(pos_raw[2]), - ]; - let vel = [ - scalar_from_raw(vel_raw[0]), - scalar_from_raw(vel_raw[1]), - scalar_from_raw(vel_raw[2]), - ]; - - for i in 0..3 { - pos[i] = pos[i] + vel[i]; - } - - let new_pos_raw = [ - scalar_to_raw(pos[0]), - scalar_to_raw(pos[1]), - scalar_to_raw(pos[2]), - ]; - let vel_out_raw = [ - scalar_to_raw(vel[0]), - scalar_to_raw(vel[1]), - scalar_to_raw(vel[2]), - ]; - - // Always upgrade to the canonical v2 payload encoding on write. - payload.type_id = motion_payload_type_id(); - payload.bytes = encode_motion_payload_q32_32(new_pos_raw, vel_out_raw); -} - -fn motion_matcher(store: &GraphStore, scope: &NodeId) -> bool { - matches!( - store.node_attachment(scope), - Some(AttachmentValue::Atom(payload)) if decode_motion_atom_payload(payload).is_some() - ) -} - -/// Deterministic rule id bytes for `rule:motion/update`. -const MOTION_RULE_ID: Hash = MOTION_UPDATE_FAMILY_ID; - -/// Returns a rewrite rule that updates entity positions based on velocity. -/// -/// This rule matches any node containing a valid motion payload and updates -/// the position by adding the velocity component-wise under deterministic -/// scalar semantics. -/// -/// Register this rule with [`Engine::register_rule`], then apply it with -/// [`Engine::apply`] using [`MOTION_RULE_NAME`]. -/// -/// Returns a [`RewriteRule`] with deterministic id, empty pattern (relies on -/// the matcher), and the motion update executor. -#[must_use] -pub fn motion_rule() -> RewriteRule { - RewriteRule { - id: MOTION_RULE_ID, - name: MOTION_RULE_NAME, - left: PatternGraph { nodes: vec![] }, - matcher: motion_matcher, - executor: motion_executor, - compute_footprint: compute_motion_footprint, - factor_mask: 0, - conflict_policy: ConflictPolicy::Abort, - join_fn: None, - } -} - -fn compute_motion_footprint(store: &GraphStore, scope: &NodeId) -> Footprint { - // Motion updates only the node's attachment payload (α plane). - let mut a_write = AttachmentSet::default(); - if store.node(scope).is_some() { - a_write.insert(crate::AttachmentKey::node_alpha(crate::NodeKey { - warp_id: store.warp_id(), - local_id: *scope, - })); - } - Footprint { - n_read: IdSet::default(), - n_write: IdSet::default(), - e_read: IdSet::default(), - e_write: IdSet::default(), - a_read: AttachmentSet::default(), - a_write, - b_in: PortSet::default(), - b_out: PortSet::default(), - factor_mask: 0, - } -} - -/// Constructs a demo [`Engine`] with a world-root node and motion rule pre-registered. -/// -/// Creates a [`GraphStore`] with a single root node (id: "world-root", type: -/// "world"), initializes an [`Engine`] with that root, and registers the -/// [`motion_rule`]. Ready for immediate use in tests and demos. -/// -/// Returns an [`Engine`] with the motion rule registered and an empty -/// world‑root node. -/// -/// # Panics -/// Panics if rule registration fails (should not happen in a fresh engine). -#[must_use] -#[allow(clippy::expect_used)] -pub fn build_motion_demo_engine() -> Engine { - let mut store = GraphStore::default(); - let root_id = make_node_id("world-root"); - let root_type = make_type_id("world"); - store.insert_node(root_id, NodeRecord { ty: root_type }); - - let mut engine = Engine::new(store, root_id); - engine - .register_rule(motion_rule()) - .expect("motion rule should register successfully in fresh engine"); - engine -} - -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - - #[test] - fn motion_rule_id_matches_domain_separated_name() { - // Our build.rs generates the family id using a domain separator: - // blake3("rule:" ++ MOTION_RULE_NAME) - let mut hasher = blake3::Hasher::new(); - hasher.update(b"rule:"); - hasher.update(MOTION_RULE_NAME.as_bytes()); - let expected: Hash = hasher.finalize().into(); - assert_eq!( - MOTION_RULE_ID, expected, - "MOTION_RULE_ID must equal blake3(\"rule:\" ++ MOTION_RULE_NAME)" - ); - } - - #[test] - fn motion_executor_updates_position_and_bytes() { - let mut store = GraphStore::default(); - let ent = make_node_id("entity-motion-bytes"); - let ty = make_type_id("entity"); - let pos = [10.0, -2.0, 3.5]; - let vel = [0.125, 2.0, -1.5]; - let payload = crate::encode_motion_atom_payload(pos, vel); - store.insert_node(ent, NodeRecord { ty }); - store.set_node_attachment(ent, Some(AttachmentValue::Atom(payload))); - - // Run executor directly and validate position math and encoded bytes. - motion_executor(&mut store, &ent); - let Some(_rec) = store.node(&ent) else { - unreachable!("entity present"); - }; - let Some(AttachmentValue::Atom(bytes)) = store.node_attachment(&ent) else { - unreachable!("payload present"); - }; - let Some((new_pos, new_vel)) = decode_motion_atom_payload(bytes) else { - unreachable!("payload decode"); - }; - // Compare component-wise using exact bit equality for deterministic values. - for i in 0..3 { - assert_eq!(new_vel[i].to_bits(), vel[i].to_bits()); - let expected = (pos[i] + vel[i]).to_bits(); - assert_eq!(new_pos[i].to_bits(), expected); - } - // Encoding round-trip should match re-encoding of updated values exactly. - assert_eq!(bytes.type_id, motion_payload_type_id()); - let expected_bytes = crate::encode_motion_payload(new_pos, new_vel); - let Some(AttachmentValue::Atom(bytes)) = store.node_attachment(&ent) else { - unreachable!("payload present after executor"); - }; - assert_eq!(bytes.bytes, expected_bytes); - } - - fn encode_motion_payload_v0_bytes(position: [f32; 3], velocity: [f32; 3]) -> Bytes { - crate::payload::encode_motion_payload_v0(position, velocity) - } - - #[test] - fn motion_executor_accepts_v0_and_upgrades_to_v2() { - let mut store = GraphStore::default(); - let ent = make_node_id("entity-motion-v0-upgrade"); - let ty = make_type_id("entity"); - - let pos = [1.0, 2.0, 3.0]; - let vel = [0.5, -1.0, 0.25]; - let payload = crate::attachment::AtomPayload::new( - crate::payload::motion_payload_type_id_v0(), - encode_motion_payload_v0_bytes(pos, vel), - ); - - store.insert_node(ent, NodeRecord { ty }); - store.set_node_attachment(ent, Some(AttachmentValue::Atom(payload))); - - motion_executor(&mut store, &ent); - - let Some(AttachmentValue::Atom(payload)) = store.node_attachment(&ent) else { - unreachable!("payload present after executor"); - }; - assert_eq!( - payload.type_id, - motion_payload_type_id(), - "executor should upgrade to v2" - ); - let Some((new_pos, new_vel)) = decode_motion_atom_payload(payload) else { - unreachable!("payload decode"); - }; - for i in 0..3 { - assert_eq!(new_vel[i].to_bits(), vel[i].to_bits()); - let expected = (pos[i] + vel[i]).to_bits(); - assert_eq!(new_pos[i].to_bits(), expected); - } - } -} diff --git a/crates/warp-core/src/demo/ports.rs b/crates/warp-core/src/demo/ports.rs deleted file mode 100644 index 3f366b25..00000000 --- a/crates/warp-core/src/demo/ports.rs +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// © James Ross Ω FLYING•ROBOTS -//! Demo rule that reserves a boundary input port, used to exercise the -//! reservation gate and independence checks. -use crate::attachment::AttachmentValue; -use crate::engine_impl::Engine; -use crate::footprint::{pack_port_key, AttachmentSet, Footprint, IdSet, PortSet}; -use crate::graph::GraphStore; -use crate::ident::{make_node_id, make_type_id, Hash, NodeId}; -use crate::payload::{ - decode_motion_payload, encode_motion_atom_payload, encode_motion_payload, - motion_payload_type_id, -}; -use crate::record::NodeRecord; -use crate::rule::{ConflictPolicy, PatternGraph, RewriteRule}; - -/// Public identifier for the port demo rule. -pub const PORT_RULE_NAME: &str = "demo/port_nop"; - -fn port_matcher(_: &GraphStore, _: &NodeId) -> bool { - true -} - -fn port_executor(store: &mut GraphStore, scope: &NodeId) { - if store.node(scope).is_none() { - return; - } - - // State machine: - // - missing attachment => initialize the motion payload (pos.x starts at 1.0) - // - existing Atom motion payload => increment pos.x by 1.0 - // - any other attachment kind/type => no-op (rule does not apply) - // Use motion payload layout; increment pos.x by 1.0 - let Some(attachment) = store.node_attachment_mut(scope) else { - let pos = [1.0, 0.0, 0.0]; - let vel = [0.0, 0.0, 0.0]; - store.set_node_attachment( - *scope, - Some(AttachmentValue::Atom(encode_motion_atom_payload(pos, vel))), - ); - return; - }; - - let AttachmentValue::Atom(payload) = attachment else { - return; - }; - if payload.type_id != motion_payload_type_id() { - return; - } - if let Some((mut pos, vel)) = decode_motion_payload(&payload.bytes) { - pos[0] += 1.0; - payload.bytes = encode_motion_payload(pos, vel); - } -} - -fn compute_port_footprint(store: &GraphStore, scope: &NodeId) -> Footprint { - let mut n_write = IdSet::default(); - let mut a_write = AttachmentSet::default(); - let mut b_in = PortSet::default(); - if store.node(scope).is_some() { - n_write.insert_node(scope); - a_write.insert(crate::AttachmentKey::node_alpha(crate::NodeKey { - warp_id: store.warp_id(), - local_id: *scope, - })); - b_in.insert(pack_port_key(scope, 0, true)); - } - Footprint { - n_read: IdSet::default(), - n_write, - e_read: IdSet::default(), - e_write: IdSet::default(), - a_read: AttachmentSet::default(), - a_write, - b_in, - b_out: PortSet::default(), - factor_mask: 0, - } -} - -/// Returns a demo rewrite rule that reserves a boundary input port. -/// -/// This rule always matches and increments the x component of the scoped -/// node's motion payload by 1.0 (or initializes to `[1.0, 0.0, 0.0]` if -/// absent). Its footprint reserves a single boundary input port (port 0, -/// direction=in) on the scoped node, used to test port-based independence -/// checks. -/// -/// Register with [`Engine::register_rule`], then apply with [`Engine::apply`] -/// using [`PORT_RULE_NAME`]. Returns a [`RewriteRule`] with a runtime-computed -/// id (BLAKE3 of the name for the spike), empty pattern, and -/// [`ConflictPolicy::Abort`]. -#[must_use] -pub fn port_rule() -> RewriteRule { - // Family id will be generated later via build.rs when promoted to a stable demo. - // For the spike, derive from a domain-separated name at runtime. - let mut hasher = blake3::Hasher::new(); - hasher.update(b"rule:"); - hasher.update(PORT_RULE_NAME.as_bytes()); - let id: Hash = hasher.finalize().into(); - RewriteRule { - id, - name: PORT_RULE_NAME, - left: PatternGraph { nodes: vec![] }, - matcher: port_matcher, - executor: port_executor, - compute_footprint: compute_port_footprint, - factor_mask: 0, - conflict_policy: ConflictPolicy::Abort, - join_fn: None, - } -} - -/// Builds an engine with a world root for port-rule tests. -/// -/// # Panics -/// Panics if registering the port rule fails (should not occur in a fresh engine). -#[must_use] -#[allow(clippy::expect_used)] -pub fn build_port_demo_engine() -> Engine { - let mut store = GraphStore::default(); - let root_id = make_node_id("world-root-ports"); - let root_type = make_type_id("world"); - store.insert_node(root_id, NodeRecord { ty: root_type }); - let mut engine = Engine::new(store, root_id); - engine - .register_rule(port_rule()) - .expect("port rule should register successfully in fresh engine"); - engine -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn port_rule_id_is_domain_separated() { - let rule = port_rule(); - let mut hasher = blake3::Hasher::new(); - hasher.update(b"rule:"); - hasher.update(PORT_RULE_NAME.as_bytes()); - let expected: Hash = hasher.finalize().into(); - assert_eq!(rule.id, expected); - } -} diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index 158dc9fd..f69f007d 100644 --- a/crates/warp-core/src/engine_impl.rs +++ b/crates/warp-core/src/engine_impl.rs @@ -8,15 +8,20 @@ use thiserror::Error; use crate::attachment::{AttachmentKey, AttachmentValue}; use crate::graph::GraphStore; -use crate::ident::{CompactRuleId, EdgeId, Hash, NodeId, NodeKey, WarpId}; +use crate::ident::{ + make_edge_id, make_node_id, make_type_id, CompactRuleId, EdgeId, Hash, NodeId, NodeKey, WarpId, +}; +use crate::inbox::{INBOX_EVENT_TYPE, INBOX_PATH, INTENT_ATTACHMENT_TYPE, PENDING_EDGE_TYPE}; use crate::receipt::{TickReceipt, TickReceiptDisposition, TickReceiptEntry, TickReceiptRejection}; use crate::record::NodeRecord; use crate::rule::{ConflictPolicy, RewriteRule}; use crate::scheduler::{DeterministicScheduler, PendingRewrite, RewritePhase, SchedulerKind}; use crate::snapshot::{compute_commit_hash_v2, compute_state_root, Snapshot}; +use crate::telemetry::{NullTelemetrySink, TelemetrySink}; use crate::tick_patch::{diff_state, SlotId, TickCommitStatus, WarpTickPatchV1}; use crate::tx::TxId; use crate::warp_state::{WarpInstance, WarpState}; +use std::sync::Arc; /// Result of calling [`Engine::apply`]. #[derive(Debug)] @@ -27,6 +32,37 @@ pub enum ApplyResult { NoMatch, } +/// Result of calling [`Engine::ingest_intent`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IngestDisposition { + /// The intent was already present in the ledger (idempotent retry). + Duplicate { + /// Content hash of the canonical intent bytes (`intent_id = H(intent_bytes)`). + intent_id: Hash, + }, + /// The intent was accepted and added to the pending inbox set. + Accepted { + /// Content hash of the canonical intent bytes (`intent_id = H(intent_bytes)`). + intent_id: Hash, + }, +} + +/// Result of calling [`Engine::dispatch_next_intent`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DispatchDisposition { + /// No pending intent was present in the inbox. + NoPending, + /// A pending intent was consumed (pending edge removed). + /// + /// `handler_matched` indicates whether a `cmd/*` rule matched the intent in this tick. + Consumed { + /// Content hash of the canonical intent bytes (`intent_id = H(intent_bytes)`). + intent_id: Hash, + /// Whether a command handler (`cmd/*`) matched and was enqueued. + handler_matched: bool, + }, +} + /// Errors emitted by the engine. #[derive(Debug, Error)] pub enum EngineError { @@ -51,8 +87,144 @@ pub enum EngineError { /// Attempted to access a warp instance that does not exist. #[error("unknown warp instance: {0:?}")] UnknownWarp(WarpId), + /// Tick index is out of bounds (exceeds ledger length). + #[error("tick index {0} exceeds ledger length {1}")] + InvalidTickIndex(usize, usize), +} + +// ============================================================================ +// Engine Builder +// ============================================================================ + +/// Source for building an engine from a fresh [`GraphStore`]. +pub struct FreshStore { + store: GraphStore, + root: NodeId, +} + +/// Source for building an engine from an existing [`WarpState`]. +pub struct ExistingState { + state: WarpState, + root: NodeKey, } +/// Fluent builder for constructing [`Engine`] instances. +/// +/// Use [`EngineBuilder::new`] to start from a fresh [`GraphStore`], or +/// [`EngineBuilder::from_state`] to start from an existing [`WarpState`]. +/// +/// # Example +/// +/// ```ignore +/// let engine = EngineBuilder::new(store, root) +/// .scheduler(SchedulerKind::Radix) +/// .policy_id(42) +/// .build(); +/// ``` +pub struct EngineBuilder { + source: Source, + scheduler: SchedulerKind, + policy_id: u32, + telemetry: Option>, +} + +impl EngineBuilder { + /// Creates a builder for a new engine with the given store and root node. + /// + /// Defaults: + /// - Scheduler: [`SchedulerKind::Radix`] + /// - Policy ID: [`crate::POLICY_ID_NO_POLICY_V0`] + /// - Telemetry: [`NullTelemetrySink`] + pub fn new(store: GraphStore, root: NodeId) -> Self { + Self { + source: FreshStore { store, root }, + scheduler: SchedulerKind::Radix, + policy_id: crate::POLICY_ID_NO_POLICY_V0, + telemetry: None, + } + } + + /// Builds the engine. This operation is infallible for fresh stores. + #[must_use] + pub fn build(self) -> Engine { + let telemetry = self + .telemetry + .unwrap_or_else(|| Arc::new(NullTelemetrySink)); + Engine::with_telemetry( + self.source.store, + self.source.root, + self.scheduler, + self.policy_id, + telemetry, + ) + } +} + +impl EngineBuilder { + /// Creates a builder for an engine from an existing [`WarpState`]. + /// + /// Defaults: + /// - Scheduler: [`SchedulerKind::Radix`] + /// - Policy ID: [`crate::POLICY_ID_NO_POLICY_V0`] + /// - Telemetry: [`NullTelemetrySink`] + pub fn from_state(state: WarpState, root: NodeKey) -> Self { + Self { + source: ExistingState { state, root }, + scheduler: SchedulerKind::Radix, + policy_id: crate::POLICY_ID_NO_POLICY_V0, + telemetry: None, + } + } + + /// Builds the engine. + /// + /// # Errors + /// + /// Returns [`EngineError::UnknownWarp`] if the root warp instance is missing. + /// Returns [`EngineError::InternalCorruption`] if the root is invalid. + pub fn build(self) -> Result { + let telemetry = self + .telemetry + .unwrap_or_else(|| Arc::new(NullTelemetrySink)); + Engine::with_state_and_telemetry( + self.source.state, + self.source.root, + self.scheduler, + self.policy_id, + telemetry, + ) + } +} + +impl EngineBuilder { + /// Sets the scheduler variant. + #[must_use] + pub fn scheduler(mut self, kind: SchedulerKind) -> Self { + self.scheduler = kind; + self + } + + /// Sets the policy identifier. + /// + /// The policy ID is committed into `patch_digest` and `commit_id` v2. + #[must_use] + pub fn policy_id(mut self, id: u32) -> Self { + self.policy_id = id; + self + } + + /// Sets the telemetry sink for observability events. + #[must_use] + pub fn telemetry(mut self, sink: Arc) -> Self { + self.telemetry = Some(sink); + self + } +} + +// ============================================================================ +// Engine +// ============================================================================ + /// Core rewrite engine used by the spike. /// /// It owns a `GraphStore`, the registered rules, and the deterministic @@ -62,6 +234,19 @@ pub enum EngineError { /// little-endian and ids are raw 32-byte values. Changing any of these rules is /// a breaking change to snapshot identity and must be recorded in the /// determinism spec and tests. +/// +/// # Construction +/// +/// Use [`EngineBuilder`] for fluent configuration: +/// +/// ```ignore +/// let engine = EngineBuilder::new(store, root) +/// .scheduler(SchedulerKind::Radix) +/// .policy_id(42) +/// .build(); +/// ``` +/// +/// Legacy constructors are also available for backward compatibility. pub struct Engine { state: WarpState, rules: HashMap<&'static str, RewriteRule>, @@ -79,6 +264,11 @@ pub struct Engine { live_txs: HashSet, current_root: NodeKey, last_snapshot: Option, + /// Sequential history of all committed ticks (Snapshot, Receipt, Patch). + tick_history: Vec<(Snapshot, TickReceipt, WarpTickPatchV1)>, + intent_log: Vec<(u64, crate::attachment::AtomPayload)>, + /// Initial state (U0) snapshot preserved for replay via `jump_to_tick`. + initial_state: WarpState, } struct ReserveOutcome { @@ -120,7 +310,7 @@ impl Engine { /// Constructs a new engine with explicit scheduler kind and policy identifier. /// - /// This is the canonical constructor; all other constructors delegate here. + /// Uses a null telemetry sink (events are discarded). /// /// # Parameters /// - `store`: Backing graph store. @@ -134,6 +324,28 @@ impl Engine { root: NodeId, kind: SchedulerKind, policy_id: u32, + ) -> Self { + Self::with_telemetry(store, root, kind, policy_id, Arc::new(NullTelemetrySink)) + } + + /// Constructs a new engine with explicit telemetry sink. + /// + /// This is the canonical constructor; all other constructors delegate here. + /// + /// # Parameters + /// - `store`: Backing graph store. + /// The supplied store is assigned to the canonical root warp instance; any pre-existing + /// `warp_id` on the store is overwritten. + /// - `root`: Root node id for snapshot hashing. + /// - `kind`: Scheduler variant (Radix vs Legacy). + /// - `policy_id`: Policy identifier committed into `patch_digest` and `commit_id` v2. + /// - `telemetry`: Telemetry sink for observability events. + pub fn with_telemetry( + store: GraphStore, + root: NodeId, + kind: SchedulerKind, + policy_id: u32, + telemetry: Arc, ) -> Self { // NOTE: The supplied `GraphStore` is assigned to the canonical root warp instance. // Any pre-existing `warp_id` on the store is overwritten. @@ -149,13 +361,15 @@ impl Engine { }, store, ); + // Preserve the initial state (U0) for replay via `jump_to_tick`. + let initial_state = state.clone(); Self { state, rules: HashMap::new(), rules_by_id: HashMap::new(), compact_rule_ids: HashMap::new(), rules_by_compact: HashMap::new(), - scheduler: DeterministicScheduler::new(kind), + scheduler: DeterministicScheduler::new(kind, telemetry), policy_id, tx_counter: 0, live_txs: HashSet::new(), @@ -164,6 +378,9 @@ impl Engine { local_id: root, }, last_snapshot: None, + tick_history: Vec::new(), + intent_log: Vec::new(), + initial_state, } } @@ -215,18 +432,60 @@ impl Engine { return Err(EngineError::UnknownWarp(root.warp_id)); } + Self::with_state_and_telemetry(state, root, kind, policy_id, Arc::new(NullTelemetrySink)) + } + + /// Constructs an engine from an existing multi-instance [`WarpState`] with telemetry. + /// + /// This is the canonical constructor for existing state; [`Engine::with_state`] delegates here + /// with a null telemetry sink. + /// + /// # Errors + /// + /// Returns [`EngineError::UnknownWarp`] if the root warp ID is not present in the state. + /// Returns [`EngineError::InternalCorruption`] if the root instance declares a parent + /// or if the root node does not match the instance's `root_node`. + pub fn with_state_and_telemetry( + state: WarpState, + root: NodeKey, + kind: SchedulerKind, + policy_id: u32, + telemetry: Arc, + ) -> Result { + let Some(root_instance) = state.instance(&root.warp_id) else { + return Err(EngineError::UnknownWarp(root.warp_id)); + }; + if root_instance.parent.is_some() { + return Err(EngineError::InternalCorruption( + "root warp instance must not declare a parent", + )); + } + if root_instance.root_node != root.local_id { + return Err(EngineError::InternalCorruption( + "Engine root must match WarpInstance.root_node", + )); + } + if state.store(&root.warp_id).is_none() { + return Err(EngineError::UnknownWarp(root.warp_id)); + } + + // Preserve the initial state (U0) for replay via `jump_to_tick`. + let initial_state = state.clone(); Ok(Self { state, rules: HashMap::new(), rules_by_id: HashMap::new(), compact_rule_ids: HashMap::new(), rules_by_compact: HashMap::new(), - scheduler: DeterministicScheduler::new(kind), + scheduler: DeterministicScheduler::new(kind, telemetry), policy_id, tx_counter: 0, live_txs: HashSet::new(), current_root: root, last_snapshot: None, + tick_history: Vec::new(), + intent_log: Vec::new(), + initial_state, }) } @@ -449,12 +708,22 @@ impl Engine { tx, }; self.last_snapshot = Some(snapshot.clone()); + self.tick_history + .push((snapshot.clone(), receipt.clone(), patch.clone())); // Mark transaction as closed/inactive and finalize scheduler accounting. self.live_txs.remove(&tx.value()); self.scheduler.finalize_tx(tx); Ok((snapshot, receipt, patch)) } + /// Aborts a transaction without committing a tick. + /// + /// This closes the transaction and releases any resources reserved in the scheduler. + pub fn abort(&mut self, tx: TxId) { + self.live_txs.remove(&tx.value()); + self.scheduler.finalize_tx(tx); + } + fn reserve_for_receipt( &mut self, tx: TxId, @@ -598,6 +867,271 @@ impl Engine { } } + /// Returns a cloned view of the current warp's graph store (for tests/tools). + /// + /// This is a snapshot-only view; mutations must go through engine APIs. + /// + /// # Panics + /// + /// Panics if the root warp store doesn't exist, which indicates a bug in + /// engine construction (the root store should always be present). + #[must_use] + #[allow(clippy::expect_used)] // Documented panic: root store missing is a construction bug + pub fn store_clone(&self) -> GraphStore { + let warp_id = self.current_root.warp_id; + self.state + .store(&warp_id) + .cloned() + .expect("root warp store missing - engine construction bug") + } + + /// Legacy ingest helper: ingests an inbox event from a [`crate::attachment::AtomPayload`]. + /// + /// This method exists for older call sites that pre-wrap intent bytes in an + /// atom payload and/or provide an arrival `seq`. + /// + /// Canonical semantics: + /// - `payload.bytes` are treated as `intent_bytes` and forwarded to [`Engine::ingest_intent`]. + /// - `seq` is ignored for identity; event nodes are content-addressed by `intent_id`. + /// - Invalid intent envelopes are ignored deterministically (no graph mutation). + /// + /// For debugging only, the provided `(seq, payload)` is recorded in an in-memory + /// log when the intent is newly accepted. + /// + /// # Errors + /// Returns [`EngineError::UnknownWarp`] if the current warp store is missing. + pub fn ingest_inbox_event( + &mut self, + seq: u64, + payload: &crate::attachment::AtomPayload, + ) -> Result<(), EngineError> { + // Legacy API retained for compatibility with older call sites. The new + // canonical ingress is content-addressed (`intent_id = H(intent_bytes)`) + // via [`Engine::ingest_intent`]; the `seq` input is ignored for identity. + // + // We still record the provided `seq` in the in-memory intent log for + // debugging, but it has no effect on graph identity or hashing. + let intent_bytes = payload.bytes.as_ref(); + let disposition = self.ingest_intent(intent_bytes)?; + if let IngestDisposition::Accepted { .. } = disposition { + self.intent_log.push((seq, payload.clone())); + } + Ok(()) + } + + /// Ingest a canonical intent envelope (`intent_bytes`) into the runtime inbox. + /// + /// This is the causality-first boundary for writes: + /// - `intent_id = H(intent_bytes)` is computed immediately (domain-separated). + /// - The event node id is derived from `intent_id` (content-addressed), not arrival order. + /// - Ingress is idempotent: re-ingesting identical `intent_bytes` returns `Duplicate` and + /// does not create additional ledger entries or pending edges. + /// + /// Inbox mechanics (pending vs. applied) are tracked via edges: + /// - While pending, an edge of type `edge:pending` exists from `sim/inbox` to the event node. + /// - When consumed, the pending edge is deleted as queue maintenance; the event node remains + /// as an append-only ledger entry. + /// + /// # Errors + /// Returns [`EngineError::UnknownWarp`] if the current warp store is missing. + pub fn ingest_intent(&mut self, intent_bytes: &[u8]) -> Result { + let intent_id = crate::inbox::compute_intent_id(intent_bytes); + let event_id = NodeId(intent_id); + + let warp_id = self.current_root.warp_id; + let store = self + .state + .store_mut(&warp_id) + .ok_or(EngineError::UnknownWarp(warp_id))?; + + let root_id = self.current_root.local_id; + + let sim_id = make_node_id("sim"); + let inbox_id = make_node_id(INBOX_PATH); + + let sim_ty = make_type_id("sim"); + let inbox_ty = make_type_id(INBOX_PATH); + let event_ty = make_type_id(INBOX_EVENT_TYPE); + + // Structural nodes/edges (idempotent). + store.insert_node(sim_id, NodeRecord { ty: sim_ty }); + store.insert_node(inbox_id, NodeRecord { ty: inbox_ty }); + + store.insert_edge( + root_id, + crate::record::EdgeRecord { + id: make_edge_id("edge:root/sim"), + from: root_id, + to: sim_id, + ty: make_type_id("edge:sim"), + }, + ); + store.insert_edge( + sim_id, + crate::record::EdgeRecord { + id: make_edge_id("edge:sim/inbox"), + from: sim_id, + to: inbox_id, + ty: make_type_id("edge:inbox"), + }, + ); + + if store.node(&event_id).is_some() { + return Ok(IngestDisposition::Duplicate { intent_id }); + } + + // Ledger entry: immutable event node keyed by content hash. + store.insert_node(event_id, NodeRecord { ty: event_ty }); + let payload = crate::attachment::AtomPayload::new( + make_type_id(INTENT_ATTACHMENT_TYPE), + bytes::Bytes::copy_from_slice(intent_bytes), + ); + store.set_node_attachment(event_id, Some(AttachmentValue::Atom(payload))); + + // Pending queue membership (edge id derived from inbox_id + intent_id). + store.insert_edge( + inbox_id, + crate::record::EdgeRecord { + id: crate::inbox::pending_edge_id(&inbox_id, &intent_id), + from: inbox_id, + to: event_id, + ty: make_type_id(PENDING_EDGE_TYPE), + }, + ); + + Ok(IngestDisposition::Accepted { intent_id }) + } + + /// Returns the number of currently pending intents in `sim/inbox`. + /// + /// This counts `edge:pending` edges from the inbox node; ledger nodes are + /// append-only and are not required to remain reachable once their pending + /// edge is removed. + /// + /// # Errors + /// Returns [`EngineError::UnknownWarp`] if the current warp store is missing. + pub fn pending_intent_count(&self) -> Result { + let warp_id = self.current_root.warp_id; + let store = self + .state + .store(&warp_id) + .ok_or(EngineError::UnknownWarp(warp_id))?; + let inbox_id = make_node_id(INBOX_PATH); + let pending_ty = make_type_id(PENDING_EDGE_TYPE); + Ok(store + .edges_from(&inbox_id) + .filter(|e| e.ty == pending_ty) + .count()) + } + + /// Dispatches exactly one pending intent (if any) in canonical `intent_id` order. + /// + /// Canonical ordering is defined by ascending byte order over `intent_id`. + /// + /// Mechanically: + /// - Select the pending event node with the smallest `intent_id`. + /// - Attempt to enqueue exactly one `cmd/*` rule for that node, using stable + /// rule id order as the tie-break when multiple handlers exist. + /// - Enqueue `sys/ack_pending` to delete the pending edge (queue maintenance). + /// + /// # Errors + /// Returns [`EngineError::UnknownTx`] if `tx` is invalid, or + /// [`EngineError::UnknownWarp`] if the current warp store is missing. + pub fn dispatch_next_intent(&mut self, tx: TxId) -> Result { + let warp_id = self.current_root.warp_id; + let store = self + .state + .store(&warp_id) + .ok_or(EngineError::UnknownWarp(warp_id))?; + let inbox_id = make_node_id(INBOX_PATH); + let pending_ty = make_type_id(PENDING_EDGE_TYPE); + + let mut next: Option = None; + for edge in store.edges_from(&inbox_id) { + if edge.ty != pending_ty { + continue; + } + let cand = edge.to; + next = Some(next.map_or(cand, |current| current.min(cand))); + } + + let Some(event_id) = next else { + return Ok(DispatchDisposition::NoPending); + }; + + // Deterministic handler order: rule_id ascending over cmd/* rules. + let mut cmd_rules: Vec<(Hash, &'static str)> = self + .rules + .values() + .filter(|r| r.name.starts_with("cmd/")) + .map(|r| (r.id, r.name)) + .collect(); + cmd_rules.sort_unstable_by(|(a_id, a_name), (b_id, b_name)| { + a_id.cmp(b_id).then_with(|| a_name.cmp(b_name)) + }); + + let mut handler_matched = false; + for (_id, name) in cmd_rules { + if matches!(self.apply(tx, name, &event_id)?, ApplyResult::Applied) { + handler_matched = true; + break; + } + } + + // Always consume one pending intent per tick (queue maintenance). + let _ = self.apply(tx, crate::inbox::ACK_PENDING_RULE_NAME, &event_id)?; + + Ok(DispatchDisposition::Consumed { + intent_id: event_id.0, + handler_matched, + }) + } + + /// Returns the sequence of all committed ticks (Snapshot, Receipt, Patch). + #[must_use] + pub fn get_ledger(&self) -> &[(Snapshot, TickReceipt, WarpTickPatchV1)] { + &self.tick_history + } + + /// Resets the engine state to the beginning of time (U0) and re-applies all patches + /// up to and including the specified tick index. + /// + /// # Errors + /// - Returns [`EngineError::InvalidTickIndex`] if `tick_index` exceeds ledger length. + /// - Returns [`EngineError::InternalCorruption`] if a patch fails to apply. + pub fn jump_to_tick(&mut self, tick_index: usize) -> Result<(), EngineError> { + let ledger_len = self.tick_history.len(); + if tick_index >= ledger_len { + return Err(EngineError::InvalidTickIndex(tick_index, ledger_len)); + } + + // 1. Restore state to the preserved initial state (U0). + // This ensures patches are replayed against the exact original base, + // rather than a fresh WarpState which would discard the original U0. + self.state = self.initial_state.clone(); + + // 2. Re-apply patches from index 0 to tick_index + for i in 0..=tick_index { + let (_, _, patch) = &self.tick_history[i]; + patch.apply_to_state(&mut self.state).map_err(|_| { + EngineError::InternalCorruption("failed to replay patch during jump") + })?; + } + + Ok(()) + } + + /// Returns a shared view of the current warp state. + #[must_use] + pub fn state(&self) -> &WarpState { + &self.state + } + + /// Returns a mutable view of the current warp state. + pub fn state_mut(&mut self) -> &mut WarpState { + &mut self.state + } + /// Returns a shared view of a node when it exists. /// /// # Errors @@ -769,6 +1303,19 @@ impl Engine { } } +impl Engine { + /// Returns a reference to the intent log. + /// + /// Each entry is a `(seq, crate::attachment::AtomPayload)` pair where: + /// - `seq` is the ingest sequence number provided to [`Engine::ingest_inbox_event`]. + /// - `AtomPayload` is the stored payload associated with the ingested intent. + /// + /// This log is populated by [`Engine::ingest_inbox_event`] for debugging purposes; + /// it does not affect graph identity or deterministic hashing. + pub fn get_intent_log(&self) -> &[(u64, crate::attachment::AtomPayload)] { + &self.intent_log + } +} /// Computes the canonical scope hash used for deterministic scheduler ordering. /// /// This value is the first component of the scheduler’s canonical ordering key @@ -841,14 +1388,73 @@ fn extend_slots_from_footprint( mod tests { use super::*; use crate::attachment::{AttachmentKey, AttachmentValue}; - use crate::demo::motion::{motion_rule, MOTION_RULE_NAME}; use crate::ident::{make_node_id, make_type_id}; use crate::payload::encode_motion_atom_payload; use crate::record::NodeRecord; + const TEST_RULE_NAME: &str = "test/motion"; + + fn test_motion_rule() -> RewriteRule { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"rule:"); + hasher.update(TEST_RULE_NAME.as_bytes()); + let id: Hash = hasher.finalize().into(); + RewriteRule { + id, + name: TEST_RULE_NAME, + left: crate::rule::PatternGraph { nodes: vec![] }, + matcher: |store, scope| { + matches!( + store.node_attachment(scope), + Some(AttachmentValue::Atom(payload)) if crate::payload::decode_motion_atom_payload(payload).is_some() + ) + }, + executor: |store, scope| { + let Some(AttachmentValue::Atom(payload)) = store.node_attachment_mut(scope) else { + return; + }; + let Some((pos_raw, vel_raw)) = + crate::payload::decode_motion_atom_payload_q32_32(payload) + else { + return; + }; + let new_pos_raw = [ + pos_raw[0].saturating_add(vel_raw[0]), + pos_raw[1].saturating_add(vel_raw[1]), + pos_raw[2].saturating_add(vel_raw[2]), + ]; + payload.type_id = crate::payload::motion_payload_type_id(); + payload.bytes = crate::payload::encode_motion_payload_q32_32(new_pos_raw, vel_raw); + }, + compute_footprint: |store, scope| { + let mut a_write = crate::AttachmentSet::default(); + if store.node(scope).is_some() { + a_write.insert(AttachmentKey::node_alpha(NodeKey { + warp_id: store.warp_id(), + local_id: *scope, + })); + } + crate::Footprint { + n_read: crate::IdSet::default(), + n_write: crate::IdSet::default(), + e_read: crate::IdSet::default(), + e_write: crate::IdSet::default(), + a_read: crate::AttachmentSet::default(), + a_write, + b_in: crate::PortSet::default(), + b_out: crate::PortSet::default(), + factor_mask: 0, + } + }, + factor_mask: 0, + conflict_policy: crate::rule::ConflictPolicy::Abort, + join_fn: None, + } + } + #[test] fn scope_hash_stable_for_rule_and_scope() { - let rule = motion_rule(); + let rule = test_motion_rule(); let warp_id = crate::ident::make_warp_id("scope-hash-test-warp"); let scope_node = make_node_id("scope-hash-entity"); let scope = NodeKey { @@ -898,11 +1504,11 @@ mod tests { store.set_node_attachment(entity, Some(AttachmentValue::Atom(payload))); let mut engine = Engine::new(store, entity); - let register = engine.register_rule(motion_rule()); + let register = engine.register_rule(test_motion_rule()); assert!(register.is_ok(), "rule registration failed: {register:?}"); let tx = engine.begin(); - let applied = engine.apply(tx, MOTION_RULE_NAME, &entity); + let applied = engine.apply(tx, TEST_RULE_NAME, &entity); assert!( matches!(applied, Ok(ApplyResult::Applied)), "expected ApplyResult::Applied, got {applied:?}" diff --git a/crates/warp-core/src/graph.rs b/crates/warp-core/src/graph.rs index ee18e419..60006d52 100644 --- a/crates/warp-core/src/graph.rs +++ b/crates/warp-core/src/graph.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; use crate::attachment::AttachmentValue; -use crate::ident::{EdgeId, NodeId, WarpId}; +use crate::ident::{EdgeId, Hash, NodeId, WarpId}; use crate::record::{EdgeRecord, NodeRecord}; /// In-memory graph storage for the spike. @@ -12,6 +12,7 @@ use crate::record::{EdgeRecord, NodeRecord}; /// The production engine will eventually swap in a content-addressed store, /// but this structure keeps the motion rewrite spike self-contained. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct GraphStore { /// Warp instance identifier for this store (Stage B1). pub(crate) warp_id: WarpId, @@ -261,8 +262,19 @@ impl GraphStore { /// Deletes a node and removes any attachments and incident edges. /// - /// Returns `true` if the node existed and was removed. - pub(crate) fn delete_node_cascade(&mut self, node: NodeId) -> bool { + /// This is a cascading delete: all edges where this node is the source (`from`) + /// or target (`to`) are also removed, along with their attachments. Use this + /// when completely removing an entity from the graph. + /// + /// # Returns + /// + /// `true` if the node existed and was removed, `false` if the node was not found. + /// + /// # Note + /// + /// This operation is not transactional on its own. If used during a transaction, + /// the caller must ensure consistency with the transaction's isolation guarantees. + pub fn delete_node_cascade(&mut self, node: NodeId) -> bool { if self.nodes.remove(&node).is_none() { return false; } @@ -345,7 +357,7 @@ impl GraphStore { /// /// Returns `true` if an edge was removed; returns `false` if the edge did not exist or /// if the reverse index indicates the edge belongs to a different bucket. - pub(crate) fn delete_edge_exact(&mut self, from: NodeId, edge_id: EdgeId) -> bool { + pub fn delete_edge_exact(&mut self, from: NodeId, edge_id: EdgeId) -> bool { match self.edge_index.get(&edge_id) { Some(current_from) if *current_from == from => {} _ => return false, @@ -398,6 +410,74 @@ impl GraphStore { self.edge_attachments.remove(&edge_id); true } + + /// Computes a canonical hash of the entire graph state. + /// + /// This traversal is strictly deterministic: + /// 1. Header: `b"DIND_STATE_HASH_V1\0"` + /// 2. Node Count (u32 LE) + /// 3. Nodes (sorted by NodeId): `b"N\0"` + `NodeId` + `TypeId` + Attachment(if any) + /// 4. Edge Count (u32 LE) + /// 5. Edges (sorted by EdgeId): `b"E\0"` + `EdgeId` + From + To + Type + Attachment(if any) + #[allow(clippy::cast_possible_truncation)] + pub fn canonical_state_hash(&self) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"DIND_STATE_HASH_V1\0"); + + // 1. Nodes + hasher.update(&(self.nodes.len() as u32).to_le_bytes()); + for (node_id, record) in &self.nodes { + hasher.update(b"N\0"); + hasher.update(&node_id.0); + hasher.update(&record.ty.0); + + if let Some(att) = self.node_attachments.get(node_id) { + hasher.update(b"\x01"); // Has attachment + Self::hash_attachment(&mut hasher, att); + } else { + hasher.update(b"\x00"); // No attachment + } + } + + // 2. Edges (Global sort by EdgeId) + // We collect all edges first to sort them definitively. + let mut all_edges: Vec<&EdgeRecord> = self.edges_from.values().flatten().collect(); + all_edges.sort_by_key(|e| e.id); + + hasher.update(&(all_edges.len() as u32).to_le_bytes()); + for edge in all_edges { + hasher.update(b"E\0"); + hasher.update(&edge.id.0); + hasher.update(&edge.from.0); + hasher.update(&edge.to.0); + hasher.update(&edge.ty.0); + + if let Some(att) = self.edge_attachments.get(&edge.id) { + hasher.update(b"\x01"); // Has attachment + Self::hash_attachment(&mut hasher, att); + } else { + hasher.update(b"\x00"); // No attachment + } + } + + *hasher.finalize().as_bytes() + } + + #[allow(clippy::cast_possible_truncation)] + fn hash_attachment(hasher: &mut blake3::Hasher, val: &AttachmentValue) { + match val { + AttachmentValue::Atom(atom) => { + hasher.update(b"ATOM"); // Tag + hasher.update(&atom.type_id.0); + hasher.update(&(atom.bytes.len() as u32).to_le_bytes()); + hasher.update(&atom.bytes); + } + AttachmentValue::Descend(warp_id) => { + hasher.update(b"DESC"); // Tag + hasher.update(&warp_id.0); + } + } + } } #[cfg(test)] diff --git a/crates/warp-core/src/ident.rs b/crates/warp-core/src/ident.rs index 7b6cd4e3..13e63b57 100644 --- a/crates/warp-core/src/ident.rs +++ b/crates/warp-core/src/ident.rs @@ -7,11 +7,20 @@ use blake3::Hasher; /// types, snapshots, and rewrite rules. pub type Hash = [u8; 32]; -/// Strongly typed identifier for a registered entity or structural node. +/// Strongly typed identifier for a node in the skeleton graph. /// -/// `NodeId` values are obtained from [`make_node_id`] and remain stable across -/// runs because they are derived from a BLAKE3 hash of a string label. +/// `NodeId` is an opaque 32-byte identifier (`Hash`). Many nodes in Echo use +/// stable, label-derived ids via [`make_node_id`] (`blake3("node:" || label)`), +/// but this is a convention, not a global constraint. +/// +/// Other subsystems may construct content-addressed `NodeId`s derived from +/// different domain-separated hashes (for example, inbox/ledger event nodes +/// keyed by `intent_id = blake3("intent:" || intent_bytes)`). +/// +/// Tooling must not assume that every `NodeId` corresponds to a human-readable +/// label, or that ids are reversible back into strings. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NodeId(pub Hash); impl NodeId { @@ -27,10 +36,12 @@ impl NodeId { /// `TypeId` values are produced by [`make_type_id`] which hashes a label; using /// a dedicated wrapper prevents accidental mixing of node and type identifiers. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TypeId(pub Hash); /// Identifier for a directed edge within the graph. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct EdgeId(pub Hash); /// Strongly typed identifier for a WARP instance. @@ -39,6 +50,7 @@ pub struct EdgeId(pub Hash); /// descended attachments: nodes and edges live in instance-scoped graphs /// addressed by `(warp_id, local_id)`. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct WarpId(pub Hash); impl WarpId { @@ -91,6 +103,7 @@ pub fn make_warp_id(label: &str) -> WarpId { /// Instance-scoped identifier for a node. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NodeKey { /// Warp instance that namespaces the local node id. pub warp_id: WarpId, @@ -100,6 +113,7 @@ pub struct NodeKey { /// Instance-scoped identifier for an edge. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct EdgeKey { /// Warp instance that namespaces the local edge id. pub warp_id: WarpId, diff --git a/crates/warp-core/src/inbox.rs b/crates/warp-core/src/inbox.rs new file mode 100644 index 00000000..b7536736 --- /dev/null +++ b/crates/warp-core/src/inbox.rs @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Inbox handling primitives for the website kernel spike. +//! +//! The inbox lives at `sim/inbox` under the current root instance and contains +//! deterministic event nodes produced during ingest. +//! +//! # Ledger vs. Queue Maintenance +//! +//! Inbox **event nodes** are treated as immutable, append-only ledger entries. +//! Pending vs. processed is tracked via **edges**, not by deleting ledger nodes. +//! In the minimal model: +//! - A pending intent is represented by a `edge:pending` edge from `sim/inbox` +//! to the event node. +//! - When the intent is consumed, the pending edge is deleted as **queue +//! maintenance**; the event node remains in the graph forever. +//! +//! 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 + +use blake3::Hasher; + +use crate::footprint::{AttachmentSet, Footprint, IdSet, PortSet}; +use crate::graph::GraphStore; +use crate::ident::{make_node_id, make_type_id, EdgeId, Hash, NodeId}; +use crate::rule::{ConflictPolicy, PatternGraph, RewriteRule}; + +/// Human-readable name for the dispatch rule. +pub const DISPATCH_INBOX_RULE_NAME: &str = "sys/dispatch_inbox"; + +/// Human-readable name for the pending-edge acknowledgment rule. +pub const ACK_PENDING_RULE_NAME: &str = "sys/ack_pending"; + +/// Node path for the simulation inbox. +pub const INBOX_PATH: &str = "sim/inbox"; + +/// Type identifier for inbox event nodes. +pub const INBOX_EVENT_TYPE: &str = "sim/inbox/event"; + +/// Type identifier for pending edges. +pub const PENDING_EDGE_TYPE: &str = "edge:pending"; + +/// Type identifier for intent attachments. +pub const INTENT_ATTACHMENT_TYPE: &str = "intent"; + +/// Hash domain prefix for pending edge IDs. +const PENDING_EDGE_HASH_DOMAIN: &[u8] = b"sim/inbox/pending:"; + +/// Constructs the `sys/dispatch_inbox` rewrite rule. +/// +/// This rule drains all pending events from `sim/inbox` by deleting the +/// `edge:pending` edges that link the inbox to event nodes. Event nodes +/// themselves are preserved (ledger is append-only); only the pending +/// markers are removed as queue maintenance. +/// +/// # Matching +/// The rule matches when the scope node has type `sim/inbox` and has at +/// least one outgoing `edge:pending` edge. +/// +/// # Effects +/// - Deletes all `edge:pending` edges from the inbox node. +/// - Does NOT delete the event nodes (ledger entries remain). +#[must_use] +pub fn dispatch_inbox_rule() -> RewriteRule { + let id: Hash = make_type_id("rule:sys/dispatch_inbox").0; + RewriteRule { + id, + name: DISPATCH_INBOX_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: inbox_matcher, + executor: inbox_executor, + compute_footprint: inbox_footprint, + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +/// Constructs the `sys/ack_pending` rewrite rule. +/// +/// 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] +pub fn ack_pending_rule() -> RewriteRule { + let id: Hash = make_type_id("rule:sys/ack_pending").0; + RewriteRule { + id, + name: ACK_PENDING_RULE_NAME, + left: PatternGraph { nodes: vec![] }, + matcher: ack_pending_matcher, + executor: ack_pending_executor, + compute_footprint: ack_pending_footprint, + factor_mask: 0, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + } +} + +fn inbox_matcher(store: &GraphStore, scope: &NodeId) -> bool { + let pending_ty = make_type_id(PENDING_EDGE_TYPE); + store + .node(scope) + .is_some_and(|n| n.ty == make_type_id(INBOX_PATH)) + && store.edges_from(scope).any(|e| e.ty == pending_ty) +} + +fn inbox_executor(store: &mut GraphStore, scope: &NodeId) { + // Drain the pending set by deleting `edge:pending` edges only. + // + // Ledger nodes are append-only; removing pending edges is queue maintenance. + let pending_ty = make_type_id(PENDING_EDGE_TYPE); + let mut pending_edges: Vec = store + .edges_from(scope) + .filter(|e| e.ty == pending_ty) + .map(|e| e.id) + .collect(); + pending_edges.sort_unstable(); + for edge_id in pending_edges { + let _ = store.delete_edge_exact(*scope, edge_id); + } +} + +fn inbox_footprint(store: &GraphStore, scope: &NodeId) -> Footprint { + let mut n_read = IdSet::default(); + let mut e_read = IdSet::default(); + let mut e_write = IdSet::default(); + let pending_ty = make_type_id(PENDING_EDGE_TYPE); + + n_read.insert_node(scope); + + for e in store.edges_from(scope) { + if e.ty != pending_ty { + continue; + } + // Record edge read for conflict detection before writing + e_read.insert_edge(&e.id); + e_write.insert_edge(&e.id); + } + + Footprint { + n_read, + n_write: IdSet::default(), + e_read, + e_write, + a_read: AttachmentSet::default(), + a_write: AttachmentSet::default(), + b_in: PortSet::default(), + b_out: PortSet::default(), + factor_mask: 0, + } +} + +fn ack_pending_matcher(store: &GraphStore, scope: &NodeId) -> bool { + let inbox_id = make_node_id(INBOX_PATH); + let edge_id = pending_edge_id(&inbox_id, &scope.0); + store.has_edge(&edge_id) +} + +fn ack_pending_executor(store: &mut GraphStore, scope: &NodeId) { + let inbox_id = make_node_id(INBOX_PATH); + let edge_id = pending_edge_id(&inbox_id, &scope.0); + let _ = store.delete_edge_exact(inbox_id, edge_id); +} + +fn ack_pending_footprint(_store: &GraphStore, scope: &NodeId) -> Footprint { + let mut n_read = IdSet::default(); + let mut e_read = IdSet::default(); + let mut e_write = IdSet::default(); + + let inbox_id = make_node_id(INBOX_PATH); + n_read.insert_node(&inbox_id); + n_read.insert_node(scope); + + let edge_id = pending_edge_id(&inbox_id, &scope.0); + // Record edge read for conflict detection before writing + e_read.insert_edge(&edge_id); + e_write.insert_edge(&edge_id); + + Footprint { + n_read, + n_write: IdSet::default(), + e_read, + e_write, + a_read: AttachmentSet::default(), + a_write: AttachmentSet::default(), + b_in: PortSet::default(), + b_out: PortSet::default(), + factor_mask: 0, + } +} + +pub(crate) fn compute_intent_id(intent_bytes: &[u8]) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(b"intent:"); + hasher.update(intent_bytes); + hasher.finalize().into() +} + +pub(crate) fn pending_edge_id(inbox_id: &NodeId, intent_id: &Hash) -> EdgeId { + let mut hasher = Hasher::new(); + hasher.update(b"edge:"); + hasher.update(PENDING_EDGE_HASH_DOMAIN); + hasher.update(inbox_id.as_bytes()); + hasher.update(intent_id); + EdgeId(hasher.finalize().into()) +} + +// NOTE: Intent routing logic lives in `crate::cmd` so it is shared between `sys/dispatch_inbox` +// and the standalone command rewrite rules (e.g. `cmd/route_push`). diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 4758073f..350f8ad6 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -5,6 +5,17 @@ //! The current implementation executes queued rewrites deterministically via the //! motion-rule spike utilities. Broader storage and scheduling features will //! continue to land over subsequent phases. +//! +//! # Protocol Determinism +//! +//! `warp-core` enforces strict determinism for all protocol artifacts (snapshots, patches, receipts). +//! +//! - **Wire Format:** Canonical CBOR via `echo_wasm_abi`. +//! - Maps must have sorted keys. +//! - Floats are forbidden or strictly canonicalized (see `math` module). +//! - **JSON:** Forbidden for protocol/hashing. Allowed ONLY for debug/view layers (e.g. telemetry). +//! - **Float Math:** The default `F32Scalar` backend is optimistic (assumes IEEE 754). +//! For strict cross-platform consensus, use the `det_fixed` feature. #![forbid(unsafe_code)] #![deny(missing_docs, rust_2018_idioms, unused_must_use)] #![deny( @@ -32,84 +43,92 @@ clippy::module_name_repetitions, clippy::use_self )] -// Permit intentional name repetition for public API clarity (e.g., FooFoo types) and -// functions named after their module for discoverability (e.g., `motion_rule`). /// Deterministic math subsystem (Vec3, Mat4, Quat, PRNG). pub mod math; mod attachment; +mod cmd; mod constants; -/// Demo implementations showcasing engine capabilities (e.g., motion rule). -pub mod demo; mod engine_impl; mod footprint; mod graph; mod ident; +/// Canonical inbox management for deterministic intent sequencing. +pub mod inbox; mod payload; mod receipt; mod record; mod rule; mod sandbox; mod scheduler; +#[cfg(feature = "serde")] +mod serializable; mod snapshot; +mod telemetry; mod tick_patch; mod tx; mod warp_state; // Re-exports for stable public API -/// Attachment-plane atoms and codec boundaries. pub use attachment::{ AtomPayload, AttachmentKey, AttachmentOwner, AttachmentPlane, AttachmentValue, Codec, CodecRegistry, DecodeError, ErasedCodec, RegistryError, }; -/// Canonical digests (e.g., empty inputs, empty length-prefixed lists). pub use constants::{BLAKE3_EMPTY, DIGEST_LEN0_U64, POLICY_ID_NO_POLICY_V0}; -/// Demo helpers and constants for the motion rule. -pub use demo::motion::{build_motion_demo_engine, motion_rule, MOTION_RULE_NAME}; -/// Rewrite engine and canonical hashing helpers. -pub use engine_impl::{scope_hash, ApplyResult, Engine, EngineError}; -/// Footprint utilities for MWMR independence checks. -/// `pack_port_key(node, port_id, dir_in)` packs a 64‑bit key as: -/// - upper 32 bits: low 32 bits of the `NodeId` (LE) -/// - bits 31..2: `port_id` (must be < 2^30) -/// - bit 1: reserved (0) -/// - bit 0: direction flag (`1` = input, `0` = output) -/// -/// Collisions are possible across nodes that share the same low 32‑bit -/// fingerprint; choose ids/ports accordingly. -pub use footprint::{pack_port_key, Footprint, PortKey}; -/// In-memory graph store used by the engine spike. +pub use engine_impl::{ + scope_hash, ApplyResult, DispatchDisposition, Engine, EngineBuilder, EngineError, + ExistingState, FreshStore, IngestDisposition, +}; +pub use footprint::{pack_port_key, AttachmentSet, Footprint, IdSet, PortKey, PortSet}; pub use graph::GraphStore; -/// Core identifier types and constructors for nodes, types, and edges. pub use ident::{ make_edge_id, make_node_id, make_type_id, make_warp_id, EdgeId, EdgeKey, Hash, NodeId, NodeKey, TypeId, WarpId, }; -/// Motion payload encoding/decoding helpers. pub use payload::{ - decode_motion_atom_payload, decode_motion_payload, encode_motion_atom_payload, - encode_motion_atom_payload_v0, encode_motion_payload, encode_motion_payload_q32_32, - encode_motion_payload_v0, motion_payload_type_id, motion_payload_type_id_v0, + decode_motion_atom_payload, decode_motion_atom_payload_q32_32, decode_motion_payload, + encode_motion_atom_payload, encode_motion_atom_payload_v0, encode_motion_payload, + encode_motion_payload_q32_32, encode_motion_payload_v0, motion_payload_type_id, + motion_payload_type_id_v0, }; -/// Tick receipts for deterministic commits (accepted vs rejected rewrites). pub use receipt::{TickReceipt, TickReceiptDisposition, TickReceiptEntry, TickReceiptRejection}; -/// Graph node and edge record types. pub use record::{EdgeRecord, NodeRecord}; -/// Rule primitives for pattern/match/execute. pub use rule::{ConflictPolicy, ExecuteFn, MatchFn, PatternGraph, RewriteRule}; -/// Sandbox helpers for constructing and comparing isolated Echo instances. pub use sandbox::{build_engine, run_pair_determinism, DeterminismError, EchoConfig}; -/// Scheduler selection (Radix vs Legacy) for sandbox/engine builders. pub use scheduler::SchedulerKind; -/// Immutable deterministic snapshot. +#[cfg(feature = "serde")] +pub use serializable::{ + SerializableReceipt, SerializableReceiptEntry, SerializableSnapshot, SerializableTick, +}; pub use snapshot::Snapshot; -/// Tick patch boundary artifacts (Paper III): replayable delta ops + slot sets. +pub use telemetry::{NullTelemetrySink, TelemetrySink}; pub use tick_patch::{ slice_worldline_indices, PortalInit, SlotId, TickCommitStatus, TickPatchError, WarpOp, WarpTickPatchV1, }; -/// Transaction identifier type. pub use tx::TxId; -/// Stage B1 multi-instance state types (`WarpInstances`). pub use warp_state::{WarpInstance, WarpState}; + +/// Zero-copy typed view over an atom payload. +pub trait AtomView<'a>: Sized { + /// Generated constant identifying the type. + const TYPE_ID: TypeId; + /// Required exact byte length for the payload. + const BYTE_LEN: usize; + + /// Parse a raw byte slice into the typed view. + fn parse(bytes: &'a [u8]) -> Option; + + /// Safe downcast from a generic `AtomPayload`. + #[inline] + fn try_from_payload(payload: &'a AtomPayload) -> Option { + if payload.type_id != Self::TYPE_ID { + return None; + } + if payload.bytes.len() != Self::BYTE_LEN { + return None; + } + Self::parse(payload.bytes.as_ref()) + } +} diff --git a/crates/warp-core/src/math/fixed_q32_32.rs b/crates/warp-core/src/math/fixed_q32_32.rs index d939146d..aef6314a 100644 --- a/crates/warp-core/src/math/fixed_q32_32.rs +++ b/crates/warp-core/src/math/fixed_q32_32.rs @@ -80,7 +80,7 @@ fn saturate_i128_to_i64(value: i128) -> i64 { /// - `NaN` maps to `0` (fixed-point has no NaN representation). /// - `+∞`/`-∞` saturate to `i64::MAX`/`i64::MIN`. /// - Values are rounded to nearest with ties-to-even at the Q32.32 boundary. -pub(crate) fn from_f32(value: f32) -> i64 { +pub fn from_f32(value: f32) -> i64 { if value.is_nan() { return 0; } @@ -143,7 +143,7 @@ pub(crate) fn from_f32(value: f32) -> i64 { /// Deterministically converts a Q32.32 raw `i64` to an `f32`. /// /// Rounds to nearest with ties-to-even at the `f32` boundary. -pub(crate) fn to_f32(raw: i64) -> f32 { +pub fn to_f32(raw: i64) -> f32 { if raw == 0 { return 0.0; } diff --git a/crates/warp-core/src/math/mod.rs b/crates/warp-core/src/math/mod.rs index 14b3c599..38be1c77 100644 --- a/crates/warp-core/src/math/mod.rs +++ b/crates/warp-core/src/math/mod.rs @@ -9,7 +9,7 @@ use std::f32::consts::TAU; /// Deterministic Q32.32 conversion helpers used by fixed-point lanes and payload codecs. -pub(crate) mod fixed_q32_32; +pub mod fixed_q32_32; mod mat4; mod prng; mod quat; @@ -45,7 +45,6 @@ pub const EPSILON: f32 = 1e-6; /// # NaN handling /// If `value`, `min`, or `max` is `NaN`, the result is `NaN`. Callers must /// ensure inputs are finite if deterministic behavior is required. -#[allow(clippy::manual_clamp)] pub fn clamp(value: f32, min: f32, max: f32) -> f32 { assert!(min <= max, "invalid clamp range: {min} > {max}"); value.clamp(min, max) diff --git a/crates/warp-core/src/math/prng.rs b/crates/warp-core/src/math/prng.rs index ed86427e..0b346853 100644 --- a/crates/warp-core/src/math/prng.rs +++ b/crates/warp-core/src/math/prng.rs @@ -6,14 +6,6 @@ /// * Not cryptographically secure; use only for gameplay/state simulation. /// * Seeding controls reproducibility within a single process/run and matching /// seeds yield identical sequences across supported platforms. -/// -/// Algorithm version for PRNG bit‑exact behavior. -/// Bump this only when intentionally changing the algorithm or seeding rules -/// and update any golden regression tests accordingly. -#[allow(dead_code)] -pub const PRNG_ALGO_VERSION: u32 = 1; - -/// Stateful PRNG instance. #[derive(Debug, Clone)] pub struct Prng { state: [u64; 2], diff --git a/crates/warp-core/src/receipt.rs b/crates/warp-core/src/receipt.rs index ca66489d..55a1a2aa 100644 --- a/crates/warp-core/src/receipt.rs +++ b/crates/warp-core/src/receipt.rs @@ -21,6 +21,7 @@ use crate::tx::TxId; /// A tick receipt: the per-candidate outcomes for a single commit attempt. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TickReceipt { tx: TxId, entries: Vec, @@ -106,6 +107,7 @@ impl TickReceipt { /// One candidate rewrite and its tick outcome. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TickReceiptEntry { /// Canonical rule family id. pub rule_id: Hash, @@ -120,8 +122,18 @@ pub struct TickReceiptEntry { pub disposition: TickReceiptDisposition, } +#[cfg(feature = "serde")] +impl TickReceiptEntry { + /// Returns a short hex representation of the rule id. + #[must_use] + pub fn rule_id_short(&self) -> String { + hex::encode(&self.rule_id[0..8]) + } +} + /// Outcome of a tick candidate. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TickReceiptDisposition { /// Candidate rewrite was accepted and applied. Applied, @@ -131,6 +143,7 @@ pub enum TickReceiptDisposition { /// Why a tick candidate was rejected. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TickReceiptRejection { /// Candidate footprint conflicts with an already-accepted footprint. FootprintConflict, diff --git a/crates/warp-core/src/record.rs b/crates/warp-core/src/record.rs index f8e56e59..2a73d7e2 100644 --- a/crates/warp-core/src/record.rs +++ b/crates/warp-core/src/record.rs @@ -16,6 +16,7 @@ use crate::ident::{EdgeId, NodeId, TypeId}; /// - `ty` must be a valid type identifier in the current schema. /// - The node identifier is not embedded here; the store supplies it externally. #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NodeRecord { /// Type identifier describing the node. pub ty: TypeId, @@ -23,13 +24,22 @@ pub struct NodeRecord { /// Materialised record for a single edge stored in the graph. /// +/// Edge records are **skeleton-plane only**: they describe the structural link +/// between two nodes (from/to) and the link's type, but do not carry +/// attachment payloads. +/// +/// Attachment-plane payloads for edges are stored separately (see +/// [`crate::AttachmentValue`]) and are addressed via [`crate::AttachmentKey`] +/// (using the edge's `id`). +/// /// Invariants +/// - `id` is stable across runs because it is derived via [`crate::make_edge_id`]. /// - `from` and `to` reference existing nodes in the same store. -/// - `id` is stable across runs for the same logical edge. /// - `ty` must be a valid edge type in the current schema. #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct EdgeRecord { - /// Stable identifier for the edge. + /// Stable identifier for the edge (see [`crate::make_edge_id`]). pub id: EdgeId, /// Source node identifier. pub from: NodeId, diff --git a/crates/warp-core/src/scheduler.rs b/crates/warp-core/src/scheduler.rs index 8328b0f9..125b2d51 100644 --- a/crates/warp-core/src/scheduler.rs +++ b/crates/warp-core/src/scheduler.rs @@ -14,10 +14,11 @@ use std::collections::{BTreeMap, HashMap}; use rustc_hash::FxHashMap; +use std::sync::Arc; + use crate::footprint::Footprint; use crate::ident::{CompactRuleId, EdgeKey, Hash, NodeId, NodeKey}; -#[cfg(feature = "telemetry")] -use crate::telemetry; +use crate::telemetry::TelemetrySink; use crate::tx::TxId; /// Active footprint tracking using generation-stamped sets for O(1) conflict detection. @@ -61,15 +62,12 @@ pub(crate) struct RadixScheduler { /// Active footprints per transaction for O(m) independence checking via `GenSets`. /// Checks all aspects: nodes (read/write), edges (read/write), and boundary ports. pub(crate) active: HashMap, - #[cfg(feature = "telemetry")] - pub(crate) counters: HashMap, // (reserved, conflict) } /// Internal representation of a rewrite waiting to be applied. #[derive(Debug, Clone)] pub(crate) struct PendingRewrite { /// Identifier of the rule to execute. - #[cfg_attr(not(feature = "telemetry"), allow(dead_code))] pub rule_id: Hash, /// Compact in-process rule handle used on hot paths. pub compact_rule: CompactRuleId, @@ -137,42 +135,22 @@ impl RadixScheduler { let active = self.active.entry(tx).or_insert_with(ActiveFootprints::new); if Self::has_conflict(active, pr) { - return self.on_conflict(tx, pr); + return Self::on_conflict(pr); } Self::mark_all(active, pr); - self.on_reserved(tx, pr) + Self::on_reserved(pr) } #[inline] - #[allow(clippy::needless_pass_by_ref_mut)] - #[cfg_attr(not(feature = "telemetry"), allow(clippy::unused_self))] - #[cfg_attr(not(feature = "telemetry"), allow(unused_variables))] - fn on_conflict(&mut self, tx: TxId, pr: &mut PendingRewrite) -> bool { + fn on_conflict(pr: &mut PendingRewrite) -> bool { pr.phase = RewritePhase::Aborted; - #[cfg(feature = "telemetry")] - { - let entry = self.counters.entry(tx).or_default(); - entry.1 += 1; - } - #[cfg(feature = "telemetry")] - telemetry::conflict(tx, &pr.rule_id); false } #[inline] - #[allow(clippy::needless_pass_by_ref_mut)] - #[cfg_attr(not(feature = "telemetry"), allow(clippy::unused_self))] - #[cfg_attr(not(feature = "telemetry"), allow(unused_variables))] - fn on_reserved(&mut self, tx: TxId, pr: &mut PendingRewrite) -> bool { + fn on_reserved(pr: &mut PendingRewrite) -> bool { pr.phase = RewritePhase::Reserved; - #[cfg(feature = "telemetry")] - { - let entry = self.counters.entry(tx).or_default(); - entry.0 += 1; - } - #[cfg(feature = "telemetry")] - telemetry::reserved(tx, &pr.rule_id); true } @@ -299,12 +277,8 @@ impl RadixScheduler { } } - /// Finalizes accounting for `tx`: emits telemetry summary and clears state. + /// Finalizes accounting for `tx`: clears internal state. pub(crate) fn finalize_tx(&mut self, tx: TxId) { - #[cfg(feature = "telemetry")] - if let Some((reserved, conflict)) = self.counters.remove(&tx) { - telemetry::summary(tx, reserved, conflict); - } self.active.remove(&tx); self.pending.remove(&tx); } @@ -547,13 +521,6 @@ impl GenSet { } } - /// Begins a new commit generation (call once per transaction). - #[inline] - #[allow(dead_code)] - pub fn begin_commit(&mut self) { - self.gen = self.gen.wrapping_add(1); - } - /// Returns true if `key` was marked in the current generation. #[inline] pub fn contains(&self, key: K) -> bool { @@ -565,20 +532,6 @@ impl GenSet { pub fn mark(&mut self, key: K) { self.seen.insert(key, self.gen); } - - /// Returns true if `key` conflicts with current generation, otherwise marks it. - /// This is a convenience method combining `contains` and `mark` for cases where - /// atomicity is needed. - #[inline] - #[allow(dead_code)] - pub fn conflict_or_mark(&mut self, key: K) -> bool { - if self.contains(key) { - true - } else { - self.mark(key); - false - } - } } // ============================================================================ @@ -589,8 +542,6 @@ impl GenSet { pub(crate) struct LegacyScheduler { pending: HashMap>, active: HashMap>, - #[cfg(feature = "telemetry")] - counters: HashMap, // (reserved, conflict) } impl LegacyScheduler { @@ -628,33 +579,15 @@ impl LegacyScheduler { for fp in frontier.iter() { if !pr.footprint.independent(fp) { pr.phase = RewritePhase::Aborted; - #[cfg(feature = "telemetry")] - { - let entry = self.counters.entry(tx).or_default(); - entry.1 += 1; - } - #[cfg(feature = "telemetry")] - telemetry::conflict(tx, &pr.rule_id); return false; } } pr.phase = RewritePhase::Reserved; frontier.push(pr.footprint.clone()); - #[cfg(feature = "telemetry")] - { - let entry = self.counters.entry(tx).or_default(); - entry.0 += 1; - } - #[cfg(feature = "telemetry")] - telemetry::reserved(tx, &pr.rule_id); true } pub(crate) fn finalize_tx(&mut self, tx: TxId) { - #[cfg(feature = "telemetry")] - if let Some((reserved, conflict)) = self.counters.remove(&tx) { - telemetry::summary(tx, reserved, conflict); - } self.active.remove(&tx); self.pending.remove(&tx); } @@ -673,9 +606,21 @@ pub enum SchedulerKind { Legacy, } -#[derive(Debug)] +/// Deterministic scheduler wrapper with telemetry support. pub(crate) struct DeterministicScheduler { inner: SchedulerImpl, + telemetry: Arc, + /// Per-transaction counters: (reserved, conflict). + counters: HashMap, +} + +impl std::fmt::Debug for DeterministicScheduler { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DeterministicScheduler") + .field("inner", &self.inner) + .field("counters", &self.counters) + .finish_non_exhaustive() + } } #[derive(Debug)] @@ -686,17 +631,24 @@ enum SchedulerImpl { impl Default for DeterministicScheduler { fn default() -> Self { - Self::new(SchedulerKind::Radix) + Self::new( + SchedulerKind::Radix, + Arc::new(crate::telemetry::NullTelemetrySink), + ) } } impl DeterministicScheduler { - pub(crate) fn new(kind: SchedulerKind) -> Self { + pub(crate) fn new(kind: SchedulerKind, telemetry: Arc) -> Self { let inner = match kind { SchedulerKind::Radix => SchedulerImpl::Radix(RadixScheduler::default()), SchedulerKind::Legacy => SchedulerImpl::Legacy(LegacyScheduler::default()), }; - Self { inner } + Self { + inner, + telemetry, + counters: HashMap::new(), + } } pub(crate) fn enqueue(&mut self, tx: TxId, rewrite: PendingRewrite) { @@ -727,13 +679,31 @@ impl DeterministicScheduler { /// footprint conflicts), upgrade the return type to an explicit reason enum /// so callers can distinguish between them. pub(crate) fn reserve(&mut self, tx: TxId, pr: &mut PendingRewrite) -> bool { - match &mut self.inner { + let rule_id = pr.rule_id; + let accepted = match &mut self.inner { SchedulerImpl::Radix(s) => s.reserve(tx, pr), SchedulerImpl::Legacy(s) => s.reserve(tx, pr), + }; + + // Track counters and emit telemetry + let entry = self.counters.entry(tx).or_default(); + if accepted { + entry.0 += 1; + self.telemetry.on_reserved(tx, &rule_id); + } else { + entry.1 += 1; + self.telemetry.on_conflict(tx, &rule_id); } + + accepted } pub(crate) fn finalize_tx(&mut self, tx: TxId) { + // Emit summary telemetry before clearing state + if let Some((reserved, conflict)) = self.counters.remove(&tx) { + self.telemetry.on_summary(tx, reserved, conflict); + } + match &mut self.inner { SchedulerImpl::Radix(s) => s.finalize_tx(tx), SchedulerImpl::Legacy(s) => s.finalize_tx(tx), @@ -849,9 +819,15 @@ mod tests { let node_a = make_node_id("a"); let node_b = make_node_id("b"); - assert!(!gen.conflict_or_mark(node_a), "first mark"); - assert!(gen.conflict_or_mark(node_a), "conflict on same gen"); - assert!(!gen.conflict_or_mark(node_b), "different node ok"); + // First access: not seen, then mark + assert!(!gen.contains(node_a), "node_a not yet seen"); + gen.mark(node_a); + + // Second access: now conflicts + assert!(gen.contains(node_a), "node_a conflicts after mark"); + + // Different node: no conflict + assert!(!gen.contains(node_b), "node_b is independent"); } // ======================================================================== diff --git a/crates/warp-core/src/serializable.rs b/crates/warp-core/src/serializable.rs new file mode 100644 index 00000000..b38be645 --- /dev/null +++ b/crates/warp-core/src/serializable.rs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS + +//! UI-friendly serializable wrappers for ledger artifacts. +//! +//! These types can be serialized using echo-wasm-abi's deterministic CBOR encoder. + +use crate::ident::NodeKey; +use crate::receipt::{TickReceipt, TickReceiptDisposition}; +use crate::snapshot::Snapshot; +use crate::tick_patch::WarpTickPatchV1; +use crate::TxId; + +/// A UI-friendly wrapper for a single tick's ledger entry. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SerializableTick { + /// Snapshot metadata. + pub snapshot: SerializableSnapshot, + /// Receipt metadata. + pub receipt: SerializableReceipt, + /// The actual state patch delta. + pub patch: WarpTickPatchV1, +} + +/// UI-friendly snapshot metadata. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SerializableSnapshot { + /// Root node of the snapshot. + pub root: NodeKey, + /// Raw commit hash. + pub hash: [u8; 32], + /// Hex-encoded commit hash. + pub hash_hex: String, + /// Transaction ID. + pub tx: TxId, +} + +/// UI-friendly receipt metadata. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SerializableReceipt { + /// Transaction ID. + pub tx: TxId, + /// Individual candidate outcomes. + pub entries: Vec, +} + +/// UI-friendly receipt entry. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SerializableReceiptEntry { + /// Raw rule family ID. + pub rule_id: [u8; 32], + /// Short hex representation of the rule ID. + pub rule_id_short: String, + /// Scope node. + pub scope: NodeKey, + /// Acceptance/rejection disposition. + pub disposition: TickReceiptDisposition, +} + +impl SerializableTick { + /// Constructs a serializable tick from its raw engine components. + #[must_use] + pub fn from_parts(snapshot: &Snapshot, receipt: &TickReceipt, patch: &WarpTickPatchV1) -> Self { + Self { + snapshot: SerializableSnapshot { + root: snapshot.root, + hash: snapshot.hash, + hash_hex: hex::encode(snapshot.hash), + tx: snapshot.tx, + }, + receipt: SerializableReceipt { + tx: receipt.tx(), + entries: receipt + .entries() + .iter() + .map(|e| SerializableReceiptEntry { + rule_id: e.rule_id, + rule_id_short: hex::encode(&e.rule_id[0..8]), + scope: e.scope, + disposition: e.disposition, + }) + .collect(), + }, + patch: patch.clone(), + } + } +} diff --git a/crates/warp-core/src/snapshot.rs b/crates/warp-core/src/snapshot.rs index d9986812..06621584 100644 --- a/crates/warp-core/src/snapshot.rs +++ b/crates/warp-core/src/snapshot.rs @@ -45,6 +45,7 @@ use crate::warp_state::WarpState; /// `state_root` (graph-only hash) and commit metadata (parents, digests, /// policy). Parents are explicit to support merges. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Snapshot { /// Node identifier that serves as the root of the snapshot. pub root: NodeKey, @@ -74,6 +75,15 @@ pub struct Snapshot { pub tx: TxId, } +#[cfg(feature = "serde")] +impl Snapshot { + /// Returns the commit hash as a lowercase hex string. + #[must_use] + pub fn hash_hex(&self) -> String { + hex::encode(self.hash) + } +} + /// Computes the canonical state root hash (graph only). pub(crate) fn compute_state_root(state: &WarpState, root: &NodeKey) -> Hash { // 1) Determine reachable nodes across instances via deterministic BFS: @@ -202,8 +212,7 @@ pub(crate) fn compute_state_root(state: &WarpState, root: &NodeKey) -> Hash { /// /// This is the legacy v1 commit header hash (plan/decision/rewrites digests). /// It is retained for reference and potential migration tooling. -#[allow(dead_code)] -pub(crate) fn compute_commit_hash_v1( +pub(crate) fn _compute_commit_hash( state_root: &Hash, parents: &[Hash], plan_digest: &Hash, diff --git a/crates/warp-core/src/telemetry.rs b/crates/warp-core/src/telemetry.rs index 78e6982b..f753db14 100644 --- a/crates/warp-core/src/telemetry.rs +++ b/crates/warp-core/src/telemetry.rs @@ -1,105 +1,52 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -// Telemetry helpers for JSONL logging when the `telemetry` feature is enabled. - -#[cfg(feature = "telemetry")] -use serde::Serialize; +//! Telemetry sink trait for observability without coupling to I/O. +//! +//! The core engine emits telemetry events through this trait, allowing +//! adapters to decide how to handle them (stdout, file, network, etc.). +//! This maintains hexagonal architecture by keeping I/O concerns outside +//! the deterministic core. use crate::ident::Hash; use crate::tx::TxId; -#[cfg(feature = "telemetry")] -#[derive(Serialize)] -struct Event<'a> { - timestamp_micros: u128, - tx_id: u64, - event: &'a str, - rule_id_short: String, -} - -#[inline] -fn short_id(h: &Hash) -> String { - #[cfg(feature = "telemetry")] - { - let mut short = [0u8; 8]; - short.copy_from_slice(&h[0..8]); - return hex::encode(short); - } - #[allow(unreachable_code)] - String::new() -} - -#[cfg(feature = "telemetry")] -fn ts_micros() -> u128 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_micros() -} - -#[cfg(feature = "telemetry")] -fn emit(kind: &str, tx: TxId, rule: &Hash) { - let ev = Event { - timestamp_micros: ts_micros(), - tx_id: tx.value(), - event: kind, - rule_id_short: short_id(rule), - }; - // Best-effort stdout with a single locked write sequence to avoid interleaving. - let mut out = std::io::stdout().lock(); - let _ = serde_json::to_writer(&mut out, &ev); - use std::io::Write as _; - let _ = out.write_all(b"\n"); -} - -/// Emits a conflict telemetry event when a rewrite fails independence checks. +/// Telemetry sink for observing scheduler events. /// -/// Logs the transaction id and rule id (shortened) as a JSON line to stdout -/// when the `telemetry` feature is enabled. Best-effort: I/O errors are -/// ignored and timestamps fall back to 0 on clock errors. -#[cfg(feature = "telemetry")] -pub fn conflict(tx: TxId, rule: &Hash) { - emit("conflict", tx, rule); -} - -/// Emits a reserved telemetry event when a rewrite passes independence checks. +/// Implementations can log to stdout, files, metrics systems, or discard events. +/// The core engine calls these methods during scheduling but does not depend +/// on any specific I/O implementation. /// -/// Logs the transaction id and rule id (shortened) as a JSON line to stdout -/// when the `telemetry` feature is enabled. Best-effort: I/O errors are -/// ignored and timestamps fall back to 0 on clock errors. -#[cfg(feature = "telemetry")] -pub fn reserved(tx: TxId, rule: &Hash) { - emit("reserved", tx, rule); +/// All methods have default no-op implementations, so callers can implement +/// only the events they care about. +pub trait TelemetrySink: Send + Sync { + /// Called when a rewrite fails independence checks (conflict detected). + /// + /// # Arguments + /// * `tx` - The transaction ID + /// * `rule_id` - The rule that conflicted + fn on_conflict(&self, _tx: TxId, _rule_id: &Hash) {} + + /// Called when a rewrite passes independence checks (successfully reserved). + /// + /// # Arguments + /// * `tx` - The transaction ID + /// * `rule_id` - The rule that was reserved + fn on_reserved(&self, _tx: TxId, _rule_id: &Hash) {} + + /// Called when a transaction is finalized with summary statistics. + /// + /// # Arguments + /// * `tx` - The transaction ID + /// * `reserved_count` - Number of rewrites successfully reserved + /// * `conflict_count` - Number of rewrites that conflicted + fn on_summary(&self, _tx: TxId, _reserved_count: u64, _conflict_count: u64) {} } -/// Emits a summary telemetry event with transaction statistics. +/// No-op telemetry sink that discards all events. /// -/// Logs the transaction id, reserved count, and conflict count as a JSON line -/// to stdout when the `telemetry` feature is enabled. Called at transaction -/// finalization. Best-effort: I/O errors are ignored and timestamps may fall -/// back to 0 on clock errors. -#[cfg(feature = "telemetry")] -pub fn summary(tx: TxId, reserved_count: u64, conflict_count: u64) { - use serde::Serialize; - #[derive(Serialize)] - struct Summary { - timestamp_micros: u128, - tx_id: u64, - event: &'static str, - reserved: u64, - conflicts: u64, - } - let s = Summary { - timestamp_micros: ts_micros(), - tx_id: tx.value(), - event: "summary", - reserved: reserved_count, - conflicts: conflict_count, - }; - let mut out = std::io::stdout().lock(); - let _ = serde_json::to_writer(&mut out, &s); - use std::io::Write as _; - let _ = out.write_all(b"\n"); -} +/// Use this when observability is not required or events should be ignored. +#[derive(Debug, Default, Clone, Copy)] +pub struct NullTelemetrySink; + +impl TelemetrySink for NullTelemetrySink {} diff --git a/crates/warp-core/src/tick_patch.rs b/crates/warp-core/src/tick_patch.rs index 0d7aceaf..bbc0c2aa 100644 --- a/crates/warp-core/src/tick_patch.rs +++ b/crates/warp-core/src/tick_patch.rs @@ -26,6 +26,7 @@ use crate::warp_state::{WarpInstance, WarpState}; /// Commit status of a tick patch. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TickCommitStatus { /// Tick committed successfully. Committed, @@ -44,6 +45,7 @@ impl TickCommitStatus { /// Unversioned slot identifier for slicing and provenance bookkeeping. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum SlotId { /// Full node record at `NodeKey` (instance-scoped skeleton record). Node(NodeKey), @@ -91,6 +93,7 @@ impl Ord for SlotId { /// A canonical delta operation applied to the graph store. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum WarpOp { /// Open a descended attachment portal atomically (Stage B1.1). /// @@ -163,6 +166,7 @@ pub enum WarpOp { /// Initialization policy for [`WarpOp::OpenPortal`]. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum PortalInit { /// Create a new child instance with only a root node (using `root_record`). Empty { @@ -296,6 +300,7 @@ impl WarpOp { /// - `in_slots` / `out_slots`, and /// - `ops`. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct WarpTickPatchV1 { policy_id: u32, rule_pack_id: ContentHash, @@ -401,6 +406,26 @@ impl WarpTickPatchV1 { self.digest } + /// Verifies that the internal digest matches the computed digest of the patch contents. + /// + /// # Errors + /// Returns `TickPatchError::DigestMismatch` if the digests do not match. + pub fn validate_digest(&self) -> Result<(), TickPatchError> { + let expected = compute_patch_digest_v2( + self.policy_id, + &self.rule_pack_id, + self.commit_status, + &self.in_slots, + &self.out_slots, + &self.ops, + ); + if self.digest == expected { + Ok(()) + } else { + Err(TickPatchError::DigestMismatch) + } + } + /// Applies the patch delta to `state`. /// /// # Errors @@ -722,6 +747,9 @@ pub enum TickPatchError { /// `OpenPortal` invariants were violated (dangling portal / inconsistent parent chain / root mismatch). #[error("portal invariant violation")] PortalInvariantViolation, + /// The patch digest did not match its contents. + #[error("patch digest mismatch")] + DigestMismatch, } fn compute_patch_digest_v2( diff --git a/crates/warp-core/src/tx.rs b/crates/warp-core/src/tx.rs index 7feacbcf..683a73cc 100644 --- a/crates/warp-core/src/tx.rs +++ b/crates/warp-core/src/tx.rs @@ -21,6 +21,7 @@ /// the same memory layout as `u64` across the FFI/Wasm boundary. #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TxId(u64); impl TxId { diff --git a/crates/warp-core/src/warp_state.rs b/crates/warp-core/src/warp_state.rs index 8be8fba1..fe9fd461 100644 --- a/crates/warp-core/src/warp_state.rs +++ b/crates/warp-core/src/warp_state.rs @@ -20,6 +20,7 @@ use crate::ident::{NodeId, WarpId}; /// deterministic “include the portal chain” slicing without searching the /// entire attachment plane. #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct WarpInstance { /// Instance identifier (namespace for local node/edge ids). pub warp_id: WarpId, @@ -38,6 +39,7 @@ pub struct WarpInstance { /// treated as internal corruption and should be prevented by constructors and /// patch replay validation. #[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct WarpState { pub(crate) stores: BTreeMap, pub(crate) instances: BTreeMap, diff --git a/crates/warp-core/tests/determinism_audit.rs b/crates/warp-core/tests/determinism_audit.rs new file mode 100644 index 00000000..ee3b2d87 --- /dev/null +++ b/crates/warp-core/tests/determinism_audit.rs @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS + +//! Audit tests for floating-point determinism in hashing paths. +//! +//! These tests verify: +//! 1. Whether f32 bit-flips affect canonical hashes (Sensitivity). +//! 2. Whether identical inputs produce identical hashes (Repeatability). +//! +//! If sensitivity is high, then cross-platform determinism relies on +//! `F32Scalar` arithmetic being bit-perfect across architectures. + +use warp_core::{ + encode_motion_atom_payload_v0, encode_motion_payload, make_node_id, make_type_id, make_warp_id, + AtomPayload, AttachmentValue, Engine, GraphStore, Hash, NodeRecord, +}; + +/// Helper to compute the commit hash for a single-node state with a given payload. +fn compute_hash_for_payload(payload: AtomPayload) -> Hash { + let warp_id = make_warp_id("audit"); + let mut store = GraphStore::new(warp_id); + let node_id = make_node_id("node"); + + store.insert_node( + node_id, + NodeRecord { + ty: make_type_id("test_entity"), + }, + ); + store.set_node_attachment(node_id, Some(AttachmentValue::Atom(payload))); + + // Create a fresh engine. It has no history, so no parents. + // Committing immediately will hash the initial state. + let mut engine = Engine::new(store, node_id); + let tx = engine.begin(); + let snapshot = engine.commit(tx).expect("commit should succeed"); + + snapshot.hash +} + +#[test] +fn audit_float_sensitivity_v0() { + // V0 payload is raw f32 bytes. + // Changing the LSB of an f32 MUST change the commit hash. + + let pos_a = [1.0, 2.0, 3.0]; + let vel = [0.0, 0.0, 0.0]; + + // Create pos_b differing by 1 ULP in the first component + let pos_bits = 1.0f32.to_bits(); + let pos_b_val = f32::from_bits(pos_bits + 1); + let pos_b = [pos_b_val, 2.0, 3.0]; + + assert_ne!(pos_a[0], pos_b[0]); + + let payload_a = encode_motion_atom_payload_v0(pos_a, vel); + let payload_b = encode_motion_atom_payload_v0(pos_b, vel); + + let hash_a = compute_hash_for_payload(payload_a); + let hash_b = compute_hash_for_payload(payload_b); + + assert_ne!( + hash_a, hash_b, + "Commit hash MUST change when f32 payload changes by 1 ULP (v0)" + ); +} + +#[test] +fn audit_float_sensitivity_v2() { + // V2 payload is Q32.32 derived from f32. + // Q32.32 has higher precision than f32, so 1 ULP change in f32 + // should still result in a different Q32.32 integer, and thus a different hash. + + let pos_a = [1.0, 2.0, 3.0]; + let vel = [0.0, 0.0, 0.0]; + + // Create pos_b differing by 1 ULP in the first component + let pos_bits = 1.0f32.to_bits(); + let pos_b_val = f32::from_bits(pos_bits + 1); + let pos_b = [pos_b_val, 2.0, 3.0]; + + // Encode using canonical V2 + let bytes_a = encode_motion_payload(pos_a, vel); + let bytes_b = encode_motion_payload(pos_b, vel); + + // Verify the bytes differ (Q32.32 captured the f32 diff) + assert_ne!( + bytes_a, bytes_b, + "Q32.32 encoding MUST capture 1 ULP f32 difference" + ); + + let payload_a = AtomPayload::new(warp_core::motion_payload_type_id(), bytes_a); + let payload_b = AtomPayload::new(warp_core::motion_payload_type_id(), bytes_b); + + let hash_a = compute_hash_for_payload(payload_a); + let hash_b = compute_hash_for_payload(payload_b); + + assert_ne!( + hash_a, hash_b, + "Commit hash MUST change when input f32 changes by 1 ULP (v2)" + ); +} + +#[test] +fn audit_repeatability() { + // Verify that encoding the same float values repeatedly produces identical hashes. + // This guards against non-deterministic iteration or internal state leakage. + + let pos = [1.23456, -7.89012, 3.0]; + let vel = [0.001, -0.002, 0.003]; + + let mut hashes = Vec::new(); + + for _ in 0..100 { + let payload = AtomPayload::new( + warp_core::motion_payload_type_id(), + encode_motion_payload(pos, vel), + ); + hashes.push(compute_hash_for_payload(payload)); + } + + let first = hashes[0]; + for (i, h) in hashes.iter().enumerate().skip(1) { + assert_eq!(*h, first, "Hash mismatch at iteration {}", i); + } +} diff --git a/crates/warp-core/tests/determinism_policy_tests.rs b/crates/warp-core/tests/determinism_policy_tests.rs index 54eb4536..80a88d4e 100644 --- a/crates/warp-core/tests/determinism_policy_tests.rs +++ b/crates/warp-core/tests/determinism_policy_tests.rs @@ -97,6 +97,7 @@ fn test_policy_subnormal_flushing() { #[test] #[cfg(feature = "serde")] +#[allow(clippy::disallowed_methods)] fn test_policy_serialization_guard() { // Manually construct JSON with -0.0 let json = r#"-0.0"#; diff --git a/crates/warp-core/tests/deterministic_sin_cos_tests.rs b/crates/warp-core/tests/deterministic_sin_cos_tests.rs index 35e249b4..8d7b58a4 100644 --- a/crates/warp-core/tests/deterministic_sin_cos_tests.rs +++ b/crates/warp-core/tests/deterministic_sin_cos_tests.rs @@ -174,8 +174,8 @@ fn test_sin_cos_error_budget_pinned_against_deterministic_oracle() { // - Absolute error budget: measured in f64 space vs the f64 reference. // NOTE: These thresholds are pinned to the current LUT+interpolation - // backend in `warp_core::math::trig` and should only be loosened with an - // explicit decision-log entry. + // backend in `warp_core::math::trig` and should only be loosened with + // explicit documentation of the change. // // ULP metrics across a zero crossing are not especially meaningful, so we // only apply the ULP budget when the f32-rounded reference magnitude is diff --git a/crates/warp-core/tests/dispatch_inbox.rs b/crates/warp-core/tests/dispatch_inbox.rs new file mode 100644 index 00000000..cead92c0 --- /dev/null +++ b/crates/warp-core/tests/dispatch_inbox.rs @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Tests for the generic `sys/dispatch_inbox` rule. + +use echo_dry_tests::build_engine_with_root; +use warp_core::{ + inbox::{ack_pending_rule, dispatch_inbox_rule}, + make_node_id, make_type_id, ApplyResult, IngestDisposition, NodeId, +}; + +#[test] +fn dispatch_inbox_drains_pending_edges_but_keeps_event_nodes() { + let root = make_node_id("root"); + let mut engine = build_engine_with_root(root); + + // Register rule + engine + .register_rule(dispatch_inbox_rule()) + .expect("register rule"); + + // Seed two intents (opaque bytes - core is byte-blind). + let intent1: &[u8] = b"intent-one"; + let intent2: &[u8] = b"intent-two"; + + let intent_id1 = match engine.ingest_intent(intent1).expect("ingest") { + IngestDisposition::Accepted { intent_id } => intent_id, + other => panic!("expected Accepted, got {other:?}"), + }; + let intent_id2 = match engine.ingest_intent(intent2).expect("ingest") { + IngestDisposition::Accepted { intent_id } => intent_id, + other => panic!("expected Accepted, got {other:?}"), + }; + + let inbox_id = make_node_id("sim/inbox"); + + // Apply + commit + let tx = engine.begin(); + let applied = engine + .apply(tx, warp_core::inbox::DISPATCH_INBOX_RULE_NAME, &inbox_id) + .expect("apply rule"); + assert!(matches!(applied, ApplyResult::Applied)); + engine.commit(tx).expect("commit"); + + let store = engine.store_clone(); + + // Inbox remains + assert!(store.node(&inbox_id).is_some()); + + // Ledger nodes remain (append-only). + let event1 = NodeId(intent_id1); + let event2 = NodeId(intent_id2); + assert!(store.node(&event1).is_some()); + assert!(store.node(&event2).is_some()); + + // Pending edges drained (queue maintenance). + let pending_ty = make_type_id("edge:pending"); + assert!(!store.edges_from(&inbox_id).any(|e| e.ty == pending_ty)); +} + +#[test] +fn dispatch_inbox_handles_missing_event_attachments() { + let root = make_node_id("root"); + let mut engine = build_engine_with_root(root); + + engine + .register_rule(dispatch_inbox_rule()) + .expect("register rule"); + + let intent = b"test-intent"; + let intent_id = match engine.ingest_intent(intent).expect("ingest") { + IngestDisposition::Accepted { intent_id } => intent_id, + other => panic!("expected Accepted, got {other:?}"), + }; + let event_id = NodeId(intent_id); + + // Simulate corrupted state: event exists, but attachment was cleared. + engine.set_node_attachment(event_id, None).unwrap(); + + let inbox_id = make_node_id("sim/inbox"); + + let tx = engine.begin(); + let applied = engine + .apply(tx, warp_core::inbox::DISPATCH_INBOX_RULE_NAME, &inbox_id) + .expect("apply rule"); + assert!(matches!(applied, ApplyResult::Applied)); + engine.commit(tx).expect("commit"); + + let store = engine.store_clone(); + assert!( + store.node(&event_id).is_some(), + "ledger nodes are append-only" + ); + let pending_ty = make_type_id("edge:pending"); + assert!(!store.edges_from(&inbox_id).any(|e| e.ty == pending_ty)); +} + +#[test] +fn dispatch_inbox_no_match_when_scope_is_not_inbox() { + let root = make_node_id("root"); + let mut engine = build_engine_with_root(root); + + engine + .register_rule(dispatch_inbox_rule()) + .expect("register rule"); + + let tx = engine.begin(); + let applied = engine + .apply(tx, warp_core::inbox::DISPATCH_INBOX_RULE_NAME, &root) + .expect("apply rule"); + assert!(matches!(applied, ApplyResult::NoMatch)); + engine.commit(tx).expect("commit"); +} + +#[test] +fn ack_pending_consumes_one_event_edge() { + let root = make_node_id("root"); + let mut engine = build_engine_with_root(root); + + engine + .register_rule(ack_pending_rule()) + .expect("register rule"); + + let intent1 = b"intent-alpha"; + let intent2 = b"intent-beta"; + + let intent_id1 = match engine.ingest_intent(intent1).expect("ingest") { + IngestDisposition::Accepted { intent_id } => intent_id, + other => panic!("expected Accepted, got {other:?}"), + }; + let intent_id2 = match engine.ingest_intent(intent2).expect("ingest") { + IngestDisposition::Accepted { intent_id } => intent_id, + other => panic!("expected Accepted, got {other:?}"), + }; + let event1 = NodeId(intent_id1); + let event2 = NodeId(intent_id2); + let inbox_id = make_node_id("sim/inbox"); + + let tx = engine.begin(); + let applied = engine + .apply(tx, warp_core::inbox::ACK_PENDING_RULE_NAME, &event1) + .expect("apply rule"); + assert!(matches!(applied, ApplyResult::Applied)); + engine.commit(tx).expect("commit"); + + let store = engine.store_clone(); + let pending_ty = make_type_id("edge:pending"); + let mut pending: Vec<_> = store + .edges_from(&inbox_id) + .filter(|e| e.ty == pending_ty) + .map(|e| e.to) + .collect(); + pending.sort_unstable(); + assert_eq!(pending, vec![event2]); +} diff --git a/crates/warp-core/tests/dpo_concurrency_litmus.rs b/crates/warp-core/tests/dpo_concurrency_litmus.rs index 25bb45cd..f34949d9 100644 --- a/crates/warp-core/tests/dpo_concurrency_litmus.rs +++ b/crates/warp-core/tests/dpo_concurrency_litmus.rs @@ -7,7 +7,7 @@ //! enqueue order must not change the terminal digests for commuting cases, and //! overlap must yield deterministic rejection for conflicting cases. -use warp_core::demo::ports::{port_rule, PORT_RULE_NAME}; +use echo_dry_tests::{motion_rule, port_rule, MOTION_RULE_NAME, PORT_RULE_NAME}; use warp_core::{ encode_motion_atom_payload, make_node_id, make_type_id, ApplyResult, AttachmentValue, Engine, EngineError, Footprint, GraphStore, NodeId, NodeRecord, PatternGraph, RewriteRule, @@ -86,7 +86,7 @@ fn build_litmus_engine() -> Result { ); let mut engine = Engine::new(store, root); - engine.register_rule(warp_core::motion_rule())?; + engine.register_rule(motion_rule())?; engine.register_rule(port_rule())?; engine.register_rule(litmus_port_read_0_rule())?; engine.register_rule(litmus_port_read_1_rule())?; @@ -150,7 +150,7 @@ fn dpo_litmus_commuting_independent_pair() -> Result<(), EngineError> { let (mut a, entity_motion, entity_port) = setup()?; let tx_a = a.begin(); assert!(matches!( - a.apply(tx_a, warp_core::MOTION_RULE_NAME, &entity_motion)?, + a.apply(tx_a, MOTION_RULE_NAME, &entity_motion)?, ApplyResult::Applied )); assert!(matches!( @@ -166,7 +166,7 @@ fn dpo_litmus_commuting_independent_pair() -> Result<(), EngineError> { ApplyResult::Applied )); assert!(matches!( - b.apply(tx_b, warp_core::MOTION_RULE_NAME, &entity_motion)?, + b.apply(tx_b, MOTION_RULE_NAME, &entity_motion)?, ApplyResult::Applied )); let (snap_b, receipt_b, _patch_b) = b.commit_with_receipt(tx_b)?; @@ -230,7 +230,7 @@ fn dpo_litmus_conflicting_pair_is_deterministically_rejected() -> Result<(), Eng let (mut a, entity) = setup()?; let tx_a = a.begin(); assert!(matches!( - a.apply(tx_a, warp_core::MOTION_RULE_NAME, &entity)?, + a.apply(tx_a, MOTION_RULE_NAME, &entity)?, ApplyResult::Applied )); assert!(matches!( @@ -246,7 +246,7 @@ fn dpo_litmus_conflicting_pair_is_deterministically_rejected() -> Result<(), Eng ApplyResult::Applied )); assert!(matches!( - b.apply(tx_b, warp_core::MOTION_RULE_NAME, &entity)?, + b.apply(tx_b, MOTION_RULE_NAME, &entity)?, ApplyResult::Applied )); let (snap_b, receipt_b, _patch_b) = b.commit_with_receipt(tx_b)?; diff --git a/crates/warp-core/tests/duplicate_rule_registration_tests.rs b/crates/warp-core/tests/duplicate_rule_registration_tests.rs index 6139c59d..44964c9f 100644 --- a/crates/warp-core/tests/duplicate_rule_registration_tests.rs +++ b/crates/warp-core/tests/duplicate_rule_registration_tests.rs @@ -2,6 +2,7 @@ // © James Ross Ω FLYING•ROBOTS #![allow(missing_docs)] use blake3::Hasher; +use echo_dry_tests::{motion_rule, MOTION_RULE_NAME}; use warp_core::{ make_node_id, make_type_id, ConflictPolicy, Engine, GraphStore, NodeRecord, PatternGraph, RewriteRule, @@ -22,11 +23,11 @@ fn registering_duplicate_rule_name_is_rejected() { let world_ty = make_type_id("world"); store.insert_node(root, NodeRecord { ty: world_ty }); let mut engine = Engine::new(store, root); - engine.register_rule(warp_core::motion_rule()).unwrap(); - let err = engine.register_rule(warp_core::motion_rule()).unwrap_err(); + engine.register_rule(motion_rule()).unwrap(); + let err = engine.register_rule(motion_rule()).unwrap_err(); match err { warp_core::EngineError::DuplicateRuleName(name) => { - assert_eq!(name, warp_core::MOTION_RULE_NAME) + assert_eq!(name, MOTION_RULE_NAME) } other => panic!("unexpected error: {other:?}"), } @@ -39,12 +40,12 @@ fn registering_duplicate_rule_id_is_rejected() { let world_ty = make_type_id("world"); store.insert_node(root, NodeRecord { ty: world_ty }); let mut engine = Engine::new(store, root); - engine.register_rule(warp_core::motion_rule()).unwrap(); + engine.register_rule(motion_rule()).unwrap(); // Compute the same family id used by the motion rule. let mut hasher = Hasher::new(); hasher.update(b"rule:"); - hasher.update(warp_core::MOTION_RULE_NAME.as_bytes()); + hasher.update(MOTION_RULE_NAME.as_bytes()); let same_id: warp_core::Hash = hasher.finalize().into(); let duplicate = RewriteRule { diff --git a/crates/warp-core/tests/engine_motion_negative_tests.rs b/crates/warp-core/tests/engine_motion_negative_tests.rs index 6559e364..a9487cc1 100644 --- a/crates/warp-core/tests/engine_motion_negative_tests.rs +++ b/crates/warp-core/tests/engine_motion_negative_tests.rs @@ -17,10 +17,11 @@ //! - `+∞`/`-∞` → saturated extrema (≈ ±2^31 when decoded as `f32`) use bytes::Bytes; +use echo_dry_tests::{motion_rule, MOTION_RULE_NAME}; use warp_core::{ decode_motion_atom_payload, encode_motion_atom_payload_v0, make_node_id, make_type_id, motion_payload_type_id, ApplyResult, AtomPayload, AttachmentValue, Engine, GraphStore, - NodeRecord, MOTION_RULE_NAME, + NodeRecord, }; fn run_motion_once_with_payload(payload: AtomPayload) -> (warp_core::TypeId, [f32; 3], [f32; 3]) { @@ -32,7 +33,7 @@ fn run_motion_once_with_payload(payload: AtomPayload) -> (warp_core::TypeId, [f3 let mut engine = Engine::new(store, ent); engine - .register_rule(warp_core::motion_rule()) + .register_rule(motion_rule()) .expect("register motion rule"); let tx = engine.begin(); @@ -71,7 +72,7 @@ fn motion_invalid_payload_size_returns_nomatch() { let mut engine = Engine::new(store, ent); engine - .register_rule(warp_core::motion_rule()) + .register_rule(motion_rule()) .expect("register motion rule"); let tx = engine.begin(); let res = engine.apply(tx, MOTION_RULE_NAME, &ent).expect("apply"); diff --git a/crates/warp-core/tests/engine_motion_tests.rs b/crates/warp-core/tests/engine_motion_tests.rs index 71b8d1b2..d741ffe2 100644 --- a/crates/warp-core/tests/engine_motion_tests.rs +++ b/crates/warp-core/tests/engine_motion_tests.rs @@ -2,9 +2,10 @@ // © James Ross Ω FLYING•ROBOTS #![allow(missing_docs)] +use echo_dry_tests::{motion_rule, MOTION_RULE_NAME}; use warp_core::{ decode_motion_atom_payload, encode_motion_atom_payload, make_node_id, make_type_id, - ApplyResult, AttachmentValue, Engine, EngineError, GraphStore, NodeRecord, MOTION_RULE_NAME, + ApplyResult, AttachmentValue, Engine, EngineError, GraphStore, NodeRecord, }; #[test] @@ -19,7 +20,7 @@ fn motion_rule_updates_position_deterministically() { let mut engine = Engine::new(store, entity); engine - .register_rule(warp_core::motion_rule()) + .register_rule(motion_rule()) .expect("duplicate rule name"); let tx = engine.begin(); @@ -37,7 +38,7 @@ fn motion_rule_updates_position_deterministically() { let mut engine_b = Engine::new(store_b, entity); engine_b - .register_rule(warp_core::motion_rule()) + .register_rule(motion_rule()) .expect("duplicate rule name"); let tx_b = engine_b.begin(); let apply_b = engine_b.apply(tx_b, MOTION_RULE_NAME, &entity).unwrap(); @@ -69,7 +70,7 @@ fn motion_rule_no_match_on_missing_payload() { let mut engine = Engine::new(store, entity); engine - .register_rule(warp_core::motion_rule()) + .register_rule(motion_rule()) .expect("duplicate rule name"); // Capture hash before any tx @@ -97,7 +98,7 @@ fn motion_rule_twice_is_deterministic_across_engines() { store_a.set_node_attachment(entity, Some(AttachmentValue::Atom(payload.clone()))); let mut engine_a = Engine::new(store_a, entity); engine_a - .register_rule(warp_core::motion_rule()) + .register_rule(motion_rule()) .expect("duplicate rule name"); for _ in 0..2 { let tx = engine_a.begin(); @@ -110,7 +111,7 @@ fn motion_rule_twice_is_deterministic_across_engines() { store_b.set_node_attachment(entity, Some(AttachmentValue::Atom(payload))); let mut engine_b = Engine::new(store_b, entity); engine_b - .register_rule(warp_core::motion_rule()) + .register_rule(motion_rule()) .expect("duplicate rule name"); for _ in 0..2 { let tx = engine_b.begin(); diff --git a/crates/warp-core/tests/inbox.rs b/crates/warp-core/tests/inbox.rs new file mode 100644 index 00000000..13068771 --- /dev/null +++ b/crates/warp-core/tests/inbox.rs @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Inbox ingestion scaffolding tests. + +use bytes::Bytes; +use echo_dry_tests::{build_engine_with_root, make_intent_id}; +use warp_core::{make_node_id, make_type_id, AtomPayload, AttachmentValue, Hash, NodeId}; + +#[test] +fn ingest_inbox_event_creates_path_and_pending_edge_from_opaque_intent_bytes() { + let root = make_node_id("root"); + let mut engine = build_engine_with_root(root); + + // Core is byte-blind: any bytes are valid intents. + let intent_bytes: &[u8] = b"opaque-test-intent"; + let payload_bytes = Bytes::copy_from_slice(intent_bytes); + let payload = AtomPayload::new(make_type_id("legacy/payload"), payload_bytes.clone()); + + engine + .ingest_inbox_event(42, &payload) + .expect("ingest should succeed"); + + let store = engine.store_clone(); + + let sim_id = make_node_id("sim"); + let inbox_id = make_node_id("sim/inbox"); + let intent_id: Hash = make_intent_id(intent_bytes); + let event_id = NodeId(intent_id); + + // Nodes exist with expected types + assert_eq!(store.node(&sim_id).unwrap().ty, make_type_id("sim")); + assert_eq!(store.node(&inbox_id).unwrap().ty, make_type_id("sim/inbox")); + assert_eq!( + store.node(&event_id).unwrap().ty, + make_type_id("sim/inbox/event") + ); + + // Event attachment is present and matches payload + let attachment = store + .node_attachment(&event_id) + .and_then(|v| match v { + AttachmentValue::Atom(a) => Some(a), + _ => None, + }) + .expect("event attachment"); + assert_eq!(attachment.type_id, make_type_id("intent")); + assert_eq!(attachment.bytes, payload_bytes); + + // Pending membership is an edge from inbox → event. + let pending_ty = make_type_id("edge:pending"); + assert!( + store + .edges_from(&inbox_id) + .any(|e| e.ty == pending_ty && e.to == event_id), + "expected a pending edge from sim/inbox → event" + ); +} + +#[test] +fn ingest_inbox_event_is_idempotent_by_intent_bytes_not_seq() { + let root = make_node_id("root"); + let mut engine = build_engine_with_root(root); + + let intent_bytes: &[u8] = b"idempotent-intent"; + let payload_bytes = Bytes::copy_from_slice(intent_bytes); + let payload = AtomPayload::new(make_type_id("legacy/payload"), payload_bytes.clone()); + + engine.ingest_inbox_event(1, &payload).unwrap(); + engine.ingest_inbox_event(2, &payload).unwrap(); + + let store = engine.store_clone(); + + let sim_id = make_node_id("sim"); + let inbox_id = make_node_id("sim/inbox"); + + // Only one structural edge root->sim and sim->inbox should exist. + let root_edges: Vec<_> = store.edges_from(&root).collect(); + assert_eq!(root_edges.len(), 1); + assert_eq!(root_edges[0].to, sim_id); + + let sim_edges: Vec<_> = store.edges_from(&sim_id).collect(); + assert_eq!(sim_edges.len(), 1); + assert_eq!(sim_edges[0].to, inbox_id); + + // Ingress idempotency is keyed by intent_id, so the same intent_bytes must not create + // additional events or pending edges even if callers vary the seq input. + let pending_ty = make_type_id("edge:pending"); + let inbox_pending_edges: Vec<_> = store + .edges_from(&inbox_id) + .filter(|e| e.ty == pending_ty) + .collect(); + assert_eq!(inbox_pending_edges.len(), 1); + + let intent_id: Hash = make_intent_id(intent_bytes); + assert!(store.node(&NodeId(intent_id)).is_some()); +} + +#[test] +fn ingest_inbox_event_creates_distinct_events_for_distinct_intents() { + let root = make_node_id("root"); + let mut engine = build_engine_with_root(root); + + let intent_a: &[u8] = b"intent-alpha"; + let intent_b: &[u8] = b"intent-beta"; + let payload_a = AtomPayload::new( + make_type_id("legacy/payload"), + Bytes::copy_from_slice(intent_a), + ); + let payload_b = AtomPayload::new( + make_type_id("legacy/payload"), + Bytes::copy_from_slice(intent_b), + ); + + engine.ingest_inbox_event(1, &payload_a).unwrap(); + engine.ingest_inbox_event(2, &payload_b).unwrap(); + + let store = engine.store_clone(); + let inbox_id = make_node_id("sim/inbox"); + + let pending_ty = make_type_id("edge:pending"); + let inbox_pending_edges: Vec<_> = store + .edges_from(&inbox_id) + .filter(|e| e.ty == pending_ty) + .collect(); + assert_eq!(inbox_pending_edges.len(), 2); + + let intent_id_a: Hash = make_intent_id(intent_a); + let intent_id_b: Hash = make_intent_id(intent_b); + + assert!(store.node(&NodeId(intent_id_a)).is_some()); + assert!(store.node(&NodeId(intent_id_b)).is_some()); +} + +// NOTE: The `ingest_inbox_event_ignores_invalid_intent_bytes_without_mutating_graph` test +// was removed because the core is now byte-blind: all bytes are valid intents and +// validation is the caller's responsibility (hexagonal architecture). diff --git a/crates/warp-core/tests/ledger_tests.rs b/crates/warp-core/tests/ledger_tests.rs new file mode 100644 index 00000000..ddb95a4b --- /dev/null +++ b/crates/warp-core/tests/ledger_tests.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Tests for the Engine ledger/history API. + +use warp_core::{make_node_id, make_type_id, Engine, GraphStore, NodeRecord}; + +#[test] +fn engine_ledger_records_commits() { + let root = make_node_id("root"); + let mut store = GraphStore::default(); + store.insert_node( + root, + NodeRecord { + ty: make_type_id("root"), + }, + ); + let mut engine = Engine::new(store, root); + + // Ledger should be empty initially + assert_eq!(engine.get_ledger().len(), 0); + + // Commit 1 + let tx1 = engine.begin(); + engine.commit(tx1).expect("commit 1"); + assert_eq!(engine.get_ledger().len(), 1); + + // Commit 2 + let tx2 = engine.begin(); + engine.commit(tx2).expect("commit 2"); + assert_eq!(engine.get_ledger().len(), 2); + + let (snapshot, receipt, patch) = &engine.get_ledger()[1]; + assert_eq!(snapshot.tx.value(), 2); + assert_eq!(receipt.tx().value(), 2); + assert_eq!(patch.policy_id(), warp_core::POLICY_ID_NO_POLICY_V0); +} diff --git a/crates/warp-core/tests/math_validation.rs b/crates/warp-core/tests/math_validation.rs index aaa97b2d..ed59cf55 100644 --- a/crates/warp-core/tests/math_validation.rs +++ b/crates/warp-core/tests/math_validation.rs @@ -11,13 +11,17 @@ use std::sync::LazyLock; use warp_core::math::{self, Mat4, Prng, Quat, Vec3}; -const FIXTURE_PATH: &str = "crates/warp-core/tests/fixtures/math-fixtures.json"; +/// Path relative to repo root, for error messages only. +const FIXTURE_PATH: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/math-fixtures.json" +); static RAW_FIXTURES: &str = include_str!("fixtures/math-fixtures.json"); -#[allow(clippy::expect_fun_call)] static FIXTURES: LazyLock = LazyLock::new(|| { let fixtures: MathFixtures = { #[allow(clippy::expect_fun_call)] + #[allow(clippy::disallowed_methods)] { serde_json::from_str(RAW_FIXTURES).expect(&format!( "failed to parse math fixtures at {}", diff --git a/crates/warp-core/tests/permutation_commute_tests.rs b/crates/warp-core/tests/permutation_commute_tests.rs index 96cdd25d..9fe01305 100644 --- a/crates/warp-core/tests/permutation_commute_tests.rs +++ b/crates/warp-core/tests/permutation_commute_tests.rs @@ -2,6 +2,7 @@ // © James Ross Ω FLYING•ROBOTS #![allow(missing_docs)] +use echo_dry_tests::motion_rule; use warp_core::{ encode_motion_atom_payload, make_node_id, make_type_id, AttachmentValue, GraphStore, NodeRecord, }; @@ -45,7 +46,7 @@ fn n_permutation_commute_n3_and_n4() { store.insert_edge(root, edge); scopes.push(id); } - let rule = warp_core::motion_rule(); + let rule = motion_rule(); // Enumerate a few permutations deterministically (not all for n=4 to keep runtime low). let perms: Vec> = match n { diff --git a/crates/warp-core/tests/property_commute_tests.rs b/crates/warp-core/tests/property_commute_tests.rs index 7367c53e..526b9f21 100644 --- a/crates/warp-core/tests/property_commute_tests.rs +++ b/crates/warp-core/tests/property_commute_tests.rs @@ -2,6 +2,7 @@ // © James Ross Ω FLYING•ROBOTS #![allow(missing_docs)] +use echo_dry_tests::motion_rule; use warp_core::{ encode_motion_atom_payload, make_node_id, make_type_id, AttachmentValue, GraphStore, NodeRecord, }; @@ -58,7 +59,7 @@ fn independent_motion_rewrites_commute_on_distinct_nodes() { ); let mut store2 = store1.clone(); - let rule = warp_core::motion_rule(); + let rule = motion_rule(); // Order 1: apply to A then B (rule.executor)(&mut store1, &a); diff --git a/crates/warp-core/tests/proptest_seed_pinning.rs b/crates/warp-core/tests/proptest_seed_pinning.rs index 78af4523..5fd3a768 100644 --- a/crates/warp-core/tests/proptest_seed_pinning.rs +++ b/crates/warp-core/tests/proptest_seed_pinning.rs @@ -5,10 +5,11 @@ use proptest::prelude::*; use proptest::test_runner::{Config as PropConfig, RngAlgorithm, TestRng, TestRunner}; +use echo_dry_tests::{motion_rule, MOTION_RULE_NAME}; use warp_core::{ decode_motion_atom_payload, decode_motion_payload, encode_motion_atom_payload, encode_motion_payload, make_node_id, make_type_id, ApplyResult, AttachmentValue, Engine, - GraphStore, NodeRecord, MOTION_RULE_NAME, + GraphStore, NodeRecord, }; // Demonstrates how to pin a deterministic seed for property tests so failures @@ -51,7 +52,7 @@ fn proptest_seed_pinned_motion_updates() { let mut engine = Engine::new(store, entity); engine - .register_rule(warp_core::motion_rule()) + .register_rule(motion_rule()) .expect("register motion rule"); let tx = engine.begin(); diff --git a/crates/warp-core/tests/reserve_gate_tests.rs b/crates/warp-core/tests/reserve_gate_tests.rs index dcbc9631..3c9eb630 100644 --- a/crates/warp-core/tests/reserve_gate_tests.rs +++ b/crates/warp-core/tests/reserve_gate_tests.rs @@ -2,6 +2,7 @@ // © James Ross Ω FLYING•ROBOTS #![allow(missing_docs)] +use echo_dry_tests::{build_port_demo_engine, PORT_RULE_NAME}; use warp_core::{ decode_motion_atom_payload, make_node_id, make_type_id, AttachmentValue, NodeRecord, }; @@ -9,7 +10,7 @@ use warp_core::{ #[test] fn reserve_gate_aborts_second_on_port_conflict() { // Engine with a single entity; register the port rule; apply it twice on same scope in one tx. - let mut engine = warp_core::demo::ports::build_port_demo_engine(); + let mut engine = build_port_demo_engine(); // Create an entity node under root that we’ll target. let entity = make_node_id("reserve-entity"); @@ -19,8 +20,8 @@ fn reserve_gate_aborts_second_on_port_conflict() { .expect("insert_node"); let tx = engine.begin(); - let _ = engine.apply(tx, warp_core::demo::ports::PORT_RULE_NAME, &entity); - let _ = engine.apply(tx, warp_core::demo::ports::PORT_RULE_NAME, &entity); + let _ = engine.apply(tx, PORT_RULE_NAME, &entity); + let _ = engine.apply(tx, PORT_RULE_NAME, &entity); let _snap = engine.commit(tx).expect("commit"); // Exactly one executor should have run: pos.x == 1.0 diff --git a/crates/warp-core/tests/rule_id_domain_tests.rs b/crates/warp-core/tests/rule_id_domain_tests.rs deleted file mode 100644 index 9d13a3ae..00000000 --- a/crates/warp-core/tests/rule_id_domain_tests.rs +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// © James Ross Ω FLYING•ROBOTS - -#![allow(missing_docs)] - -#[test] -fn motion_rule_family_id_uses_domain_separation() { - let mut hasher = blake3::Hasher::new(); - hasher.update(b"rule:motion/update"); - let expected: [u8; 32] = hasher.finalize().into(); - // Access const exposed via the motion demo module. - assert_eq!(expected, warp_core::demo::motion::MOTION_UPDATE_FAMILY_ID); -} diff --git a/crates/warp-core/tests/tick_receipt_tests.rs b/crates/warp-core/tests/tick_receipt_tests.rs index 9fd3ebbd..a8b5a405 100644 --- a/crates/warp-core/tests/tick_receipt_tests.rs +++ b/crates/warp-core/tests/tick_receipt_tests.rs @@ -3,11 +3,11 @@ #![allow(missing_docs)] +use echo_dry_tests::{motion_rule, MOTION_RULE_NAME}; use warp_core::{ encode_motion_atom_payload, make_node_id, make_type_id, scope_hash, AttachmentValue, ConflictPolicy, Engine, Footprint, GraphStore, Hash, NodeId, NodeKey, NodeRecord, PatternGraph, RewriteRule, TickReceiptDisposition, TickReceiptEntry, TickReceiptRejection, TxId, WarpId, - MOTION_RULE_NAME, }; fn rule_id(name: &str) -> Hash { @@ -130,14 +130,14 @@ fn commit_with_receipt_records_accept_reject_and_matches_snapshot_digests() { let mut engine = Engine::new(store, entity); engine - .register_rule(warp_core::motion_rule()) + .register_rule(motion_rule()) .expect("motion rule registers"); // Register a second rule with a distinct id and name but the same matcher/executor/footprint. // This lets us generate two candidates that touch the same node without being de-duped by // the scheduler’s last-wins key. let rule2_name: &'static str = "motion/update-alt"; - let base = warp_core::motion_rule(); + let base = motion_rule(); let rule2 = RewriteRule { id: rule_id(rule2_name), name: rule2_name, diff --git a/crates/warp-core/tests/tx_lifecycle_tests.rs b/crates/warp-core/tests/tx_lifecycle_tests.rs index dbba32ef..ee02a611 100644 --- a/crates/warp-core/tests/tx_lifecycle_tests.rs +++ b/crates/warp-core/tests/tx_lifecycle_tests.rs @@ -2,9 +2,10 @@ // © James Ross Ω FLYING•ROBOTS #![allow(missing_docs)] +use echo_dry_tests::{motion_rule, MOTION_RULE_NAME}; use warp_core::{ encode_motion_atom_payload, make_node_id, make_type_id, AttachmentValue, EngineError, - GraphStore, NodeRecord, MOTION_RULE_NAME, + GraphStore, NodeRecord, }; #[test] @@ -19,7 +20,7 @@ fn tx_invalid_after_commit() { let mut engine = warp_core::Engine::new(store, entity); engine - .register_rule(warp_core::motion_rule()) + .register_rule(motion_rule()) .expect("duplicate rule name"); let tx = engine.begin(); diff --git a/crates/warp-ffi/src/lib.rs b/crates/warp-ffi/src/lib.rs index 55beecc7..5e9cf0d6 100644 --- a/crates/warp-ffi/src/lib.rs +++ b/crates/warp-ffi/src/lib.rs @@ -1,21 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! C-compatible bindings for the motion rewrite spike. +//! C-compatible bindings for the warp-core engine. //! //! This module exposes a minimal ABI that higher-level languages (Rhai host modules, Python, //! etc.) can use to interact with the deterministic engine without knowing the //! internal Rust types. #![deny(missing_docs)] -use std::ffi::CStr; -use std::os::raw::c_char; -use std::slice; - -use warp_core::{ - build_motion_demo_engine, decode_motion_atom_payload, encode_motion_atom_payload, make_node_id, - make_type_id, ApplyResult, AttachmentValue, Engine, NodeId, NodeRecord, TxId, MOTION_RULE_NAME, -}; +use warp_core::{Engine, TxId}; /// Opaque engine pointer exposed over the C ABI. pub struct WarpEngine { @@ -46,22 +39,10 @@ pub struct warp_snapshot { pub hash: [u8; 32], } -/// Creates a new engine with the motion rule registered. +/// Releases the engine allocation. /// /// # Safety -/// The caller assumes ownership of the returned pointer and must release it -/// via [`warp_engine_free`] to avoid leaking memory. -#[no_mangle] -pub unsafe extern "C" fn warp_engine_new() -> *mut WarpEngine { - Box::into_raw(Box::new(WarpEngine { - inner: build_motion_demo_engine(), - })) -} - -/// Releases the engine allocation created by [`warp_engine_new`]. -/// -/// # Safety -/// `engine` must be a pointer previously returned by [`warp_engine_new`] that +/// `engine` must be a pointer previously returned by an engine constructor that /// has not already been freed. #[no_mangle] pub unsafe extern "C" fn warp_engine_free(engine: *mut WarpEngine) { @@ -73,59 +54,10 @@ pub unsafe extern "C" fn warp_engine_free(engine: *mut WarpEngine) { } } -/// Spawns an entity with encoded motion data and returns its identifier. -/// -/// # Safety -/// `engine`, `label`, and `out_handle` must be valid pointers. `label` must -/// reference a null-terminated UTF-8 string. -#[no_mangle] -pub unsafe extern "C" fn warp_engine_spawn_motion_entity( - engine: *mut WarpEngine, - label: *const c_char, - px: f32, - py: f32, - pz: f32, - vx: f32, - vy: f32, - vz: f32, - out_handle: *mut warp_node_id, -) -> bool { - if engine.is_null() || label.is_null() || out_handle.is_null() { - return false; - } - let engine = unsafe { &mut *engine }; - let label = unsafe { CStr::from_ptr(label) }; - let label_str = match label.to_str() { - Ok(value) => value, - Err(_) => return false, - }; - - let node_id = make_node_id(label_str); - let entity_type = make_type_id("entity"); - let payload = encode_motion_atom_payload([px, py, pz], [vx, vy, vz]); - - if engine - .inner - .insert_node_with_attachment( - node_id, - NodeRecord { ty: entity_type }, - Some(AttachmentValue::Atom(payload)), - ) - .is_err() - { - return false; - } - - unsafe { - (*out_handle).bytes = *node_id.as_bytes(); - } - true -} - /// Starts a new transaction and returns its identifier. /// /// # Safety -/// `engine` must be a valid pointer created by [`warp_engine_new`]. +/// `engine` must be a valid pointer to a `WarpEngine`. #[no_mangle] pub unsafe extern "C" fn warp_engine_begin(engine: *mut WarpEngine) -> warp_tx_id { if engine.is_null() { @@ -136,37 +68,6 @@ pub unsafe extern "C" fn warp_engine_begin(engine: *mut WarpEngine) -> warp_tx_i warp_tx_id { value: tx.value() } } -/// Applies the motion rewrite to the provided entity within transaction `tx`. -/// -/// # Safety -/// All pointers must be valid. `tx` must reference an active transaction. -#[no_mangle] -pub unsafe extern "C" fn warp_engine_apply_motion( - engine: *mut WarpEngine, - tx: warp_tx_id, - node_handle: *const warp_node_id, -) -> bool { - let engine = match unsafe { engine.as_mut() } { - Some(engine) => engine, - None => return false, - }; - if tx.value == 0 { - return false; - } - let node_id = match handle_to_node_id(node_handle) { - Some(id) => id, - None => return false, - }; - match engine - .inner - .apply(TxId::from_raw(tx.value), MOTION_RULE_NAME, &node_id) - { - Ok(ApplyResult::Applied) => true, - Ok(ApplyResult::NoMatch) => false, - Err(_) => false, - } -} - /// Commits the transaction and writes the resulting snapshot hash. /// /// # Safety @@ -191,138 +92,3 @@ pub unsafe extern "C" fn warp_engine_commit( Err(_) => false, } } - -/// Reads the decoded position and velocity for an entity. -/// -/// # Safety -/// Pointers must be valid; output buffers must have length at least three. -#[no_mangle] -pub unsafe extern "C" fn warp_engine_read_motion( - engine: *mut WarpEngine, - node_handle: *const warp_node_id, - out_position: *mut f32, - out_velocity: *mut f32, -) -> bool { - let engine = match unsafe { engine.as_ref() } { - Some(engine) => engine, - None => return false, - }; - if out_position.is_null() || out_velocity.is_null() { - return false; - } - let node_id = match handle_to_node_id(node_handle) { - Some(id) => id, - None => return false, - }; - if engine.inner.node(&node_id).ok().flatten().is_none() { - return false; - } - let payload = match engine.inner.node_attachment(&node_id) { - Ok(Some(AttachmentValue::Atom(payload))) => payload, - _ => return false, - }; - let (position, velocity) = match decode_motion_atom_payload(payload) { - Some(values) => values, - None => return false, - }; - copy_vec3(out_position, &position); - copy_vec3(out_velocity, &velocity); - true -} - -fn handle_to_node_id(handle: *const warp_node_id) -> Option { - // Helper used internally by the ABI; callers pass raw bytes from C. - if handle.is_null() { - return None; - } - let bytes = unsafe { (*handle).bytes }; - Some(NodeId(bytes)) -} - -fn copy_vec3(ptr: *mut f32, values: &[f32; 3]) { - // Safety: callers guarantee `ptr` references a buffer with len >= 3. - unsafe { - let slice = slice::from_raw_parts_mut(ptr, 3); - slice.copy_from_slice(values); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::ffi::CString; - - unsafe fn spawn(engine: *mut WarpEngine, label: &str) -> warp_node_id { - let c_label = CString::new(label).unwrap(); - let mut handle = warp_node_id { bytes: [0; 32] }; - let ok = unsafe { - warp_engine_spawn_motion_entity( - engine, - c_label.as_ptr(), - 1.0, - 2.0, - 3.0, - 0.5, - -1.0, - 0.25, - &mut handle as *mut _, - ) - }; - assert!(ok); - handle - } - - #[test] - fn ffi_motion_rewrite_is_deterministic() { - unsafe { - let engine_a = warp_engine_new(); - let handle_a = spawn(engine_a, "entity-ffi"); - let tx_a = warp_engine_begin(engine_a); - assert!(warp_engine_apply_motion( - engine_a, - tx_a, - &handle_a as *const _ - )); - let mut snap_a = warp_snapshot { hash: [0; 32] }; - assert!(warp_engine_commit(engine_a, tx_a, &mut snap_a as *mut _)); - - let engine_b = warp_engine_new(); - let handle_b = spawn(engine_b, "entity-ffi"); - let tx_b = warp_engine_begin(engine_b); - assert!(warp_engine_apply_motion( - engine_b, - tx_b, - &handle_b as *const _ - )); - let mut snap_b = warp_snapshot { hash: [0; 32] }; - assert!(warp_engine_commit(engine_b, tx_b, &mut snap_b as *mut _)); - - assert_eq!(snap_a.hash, snap_b.hash); - - let mut position = [0f32; 3]; - let mut velocity = [0f32; 3]; - assert!(warp_engine_read_motion( - engine_a, - &handle_a as *const _, - position.as_mut_ptr(), - velocity.as_mut_ptr() - )); - assert_eq!(position, [1.5, 1.0, 3.25]); - assert_eq!(velocity, [0.5, -1.0, 0.25]); - - warp_engine_free(engine_a); - warp_engine_free(engine_b); - } - } - - #[test] - fn ffi_apply_no_match_returns_false() { - unsafe { - let engine = warp_engine_new(); - let tx = warp_engine_begin(engine); - let bogus = warp_node_id { bytes: [1; 32] }; - assert!(!warp_engine_apply_motion(engine, tx, &bogus as *const _)); - warp_engine_free(engine); - } - } -} diff --git a/crates/warp-wasm/Cargo.toml b/crates/warp-wasm/Cargo.toml index 619d5fa6..57cfdc29 100644 --- a/crates/warp-wasm/Cargo.toml +++ b/crates/warp-wasm/Cargo.toml @@ -20,14 +20,17 @@ default = [] console-panic = ["console_error_panic_hook", "web-sys"] [dependencies] -warp-core = { workspace = true } +echo-registry-api = { workspace = true } +echo-wasm-abi = { workspace = true } wasm-bindgen = "0.2.106" js-sys = "0.3.83" web-sys = { version = "0.3.83", optional = true, features = ["console"] } console_error_panic_hook = { version = "0.1.7", optional = true } +serde = { version = "1.0", features = ["derive"] } +serde-value = "0.7" [dev-dependencies] -wasm-bindgen-test = "0.3.42" +serde_json = "1.0" [package.metadata.wasm-pack.profile.release] diff --git a/crates/warp-wasm/src/lib.rs b/crates/warp-wasm/src/lib.rs index 6a87a9bb..c4a334eb 100644 --- a/crates/warp-wasm/src/lib.rs +++ b/crates/warp-wasm/src/lib.rs @@ -1,270 +1,368 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! wasm-bindgen bindings that expose the motion rewrite spike to tooling. +//! wasm-bindgen bindings for warp-core engine. //! -//! The exported `WasmEngine` mirrors the C ABI surface so browser clients can -//! create entities, drive transactions, and read deterministic hashes. +//! Provides WASM exports for browser clients to interact with the +//! deterministic engine and registry. #![deny(missing_docs)] -use std::cell::RefCell; -use std::rc::Rc; +use std::sync::OnceLock; +use echo_registry_api::RegistryProvider; +use echo_wasm_abi::{decode_cbor, encode_cbor}; use js_sys::Uint8Array; -use warp_core::{ - build_motion_demo_engine, decode_motion_atom_payload, encode_motion_atom_payload, make_node_id, - make_type_id, ApplyResult, AttachmentValue, Engine, NodeId, NodeRecord, TxId, MOTION_RULE_NAME, -}; use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; -// Generates a 3D vector type with wasm_bindgen bindings. -macro_rules! wasm_vector_type { - ($struct_doc:literal, $name:ident, $ctor_doc:literal, $x_doc:literal, $y_doc:literal, $z_doc:literal) => { - #[wasm_bindgen] - #[doc = $struct_doc] - pub struct $name { - x: f32, - y: f32, - z: f32, - } +/// Placeholder ABI bytes for empty responses. +fn empty_bytes() -> Uint8Array { + Uint8Array::new_with_length(0) +} - #[wasm_bindgen] - impl $name { - #[wasm_bindgen(constructor)] - #[doc = $ctor_doc] - pub fn new(x: f32, y: f32, z: f32) -> $name { - assert!( - x.is_finite(), - concat!(stringify!($name), " x component must be finite") - ); - assert!( - y.is_finite(), - concat!(stringify!($name), " y component must be finite") - ); - assert!( - z.is_finite(), - concat!(stringify!($name), " z component must be finite") - ); - $name { x, y, z } - } +// ------------------------------------------------------------------------- +// Registry provider (placeholder until app-supplied registry is linked). +// ------------------------------------------------------------------------- - #[wasm_bindgen(getter)] - #[doc = $x_doc] - pub fn x(&self) -> f32 { - self.x - } +static REGISTRY: OnceLock<&'static dyn RegistryProvider> = OnceLock::new(); - #[wasm_bindgen(getter)] - #[doc = $y_doc] - pub fn y(&self) -> f32 { - self.y - } +/// Install an application-supplied registry provider. +/// +/// Must be called exactly once at application startup before any registry-dependent +/// operations (e.g., `execute_query`, `encode_command`). The provider must have +/// `'static` lifetime as it's stored in a global `OnceLock`. +/// +/// # Panics +/// +/// Panics if called more than once (registry already installed). +/// +/// # Thread Safety +/// +/// Uses `OnceLock` internally, so the first successful call wins in concurrent scenarios. +pub fn install_registry(provider: &'static dyn RegistryProvider) { + if REGISTRY.set(provider).is_err() { + panic!("registry already installed"); + } +} - #[wasm_bindgen(getter)] - #[doc = $z_doc] - pub fn z(&self) -> f32 { - self.z - } +fn registry() -> Option<&'static dyn RegistryProvider> { + REGISTRY.get().copied() +} + +/// Validates a serde value against an argument definition list. +/// +/// Returns `true` if `value` is a map where every key corresponds to an `ArgDef`, +/// all required fields are present, and all values pass type checks against their +/// declared types (including enum validation). +fn validate_object_against_args( + value: &serde_value::Value, + args: &[echo_registry_api::ArgDef], + enums: &[echo_registry_api::EnumDef], +) -> bool { + let obj = match value { + serde_value::Value::Map(map) => map, + _ => return false, + }; + + // Unknown keys? + for key in obj.keys() { + let serde_value::Value::String(s) = key else { + return false; + }; + if !args.iter().any(|a| a.name == s.as_str()) { + return false; } + } - impl $name { - pub(crate) fn components(&self) -> [f32; 3] { - [self.x, self.y, self.z] + // Required + type checks + for arg in args { + let v = obj.get(&serde_value::Value::String(arg.name.to_string())); + let Some(v) = v else { + if arg.required { + return false; + } + continue; + }; + // Type check + let ok = if arg.list { + match v { + serde_value::Value::Seq(items) => { + items.iter().all(|item| scalar_type_ok(item, arg.ty, enums)) + } + _ => false, } + } else { + scalar_type_ok(v, arg.ty, enums) + }; + if !ok { + return false; } - }; + } + true } -/// Builds a fresh engine with the motion rule pre-registered. -fn build_engine() -> Engine { - build_motion_demo_engine() +/// Checks if a scalar value matches the expected GraphQL type. +/// +/// Handles String, ID, Boolean, Int, Float, and enum types. For enums, validates +/// that the string value is a member of the enum's defined values. +fn scalar_type_ok(v: &serde_value::Value, ty: &str, enums: &[echo_registry_api::EnumDef]) -> bool { + match ty { + "String" | "ID" => matches!(v, serde_value::Value::String(_)), + "Boolean" => matches!(v, serde_value::Value::Bool(_)), + "Int" => matches!( + v, + serde_value::Value::I8(_) + | serde_value::Value::I16(_) + | serde_value::Value::I32(_) + | serde_value::Value::I64(_) + | serde_value::Value::U8(_) + | serde_value::Value::U16(_) + | serde_value::Value::U32(_) + | serde_value::Value::U64(_) + ), + "Float" => matches!( + v, + serde_value::Value::F32(_) + | serde_value::Value::F64(_) + | serde_value::Value::I8(_) + | serde_value::Value::I16(_) + | serde_value::Value::I32(_) + | serde_value::Value::I64(_) + | serde_value::Value::U8(_) + | serde_value::Value::U16(_) + | serde_value::Value::U32(_) + | serde_value::Value::U64(_) + ), + other => { + // enum check + if let Some(def) = enums.iter().find(|e| e.name == other) { + if let serde_value::Value::String(s) = v { + def.values.contains(&s.as_str()) + } else { + false + } + } else { + false // unknown type -> reject to prevent schema drift + } + } + } } #[cfg(feature = "console-panic")] #[wasm_bindgen(start)] +/// Initialize console panic hook for better error messages in browser. pub fn init_console_panic_hook() { console_error_panic_hook::set_once(); } -/// Converts a 32-byte buffer into a [`NodeId`]. -fn bytes_to_node_id(bytes: &[u8]) -> Option { - if bytes.len() != 32 { - return None; - } - let mut id = [0u8; 32]; - id.copy_from_slice(bytes); - Some(NodeId(id)) +// ----------------------------------------------------------------------------- +// Frozen ABI exports (website kernel spike) +// ----------------------------------------------------------------------------- + +/// Enqueue a canonical intent payload (opaque bytes). Placeholder: currently no-op. +#[wasm_bindgen] +pub fn dispatch_intent(_intent_bytes: &[u8]) { + // TODO: wire to ingest_inbox_event once kernel plumbing lands in warp-wasm. } -/// WASM-friendly wrapper around the deterministic engine. +/// Run deterministic steps up to a budget. Placeholder: returns empty StepResult bytes. #[wasm_bindgen] -pub struct WasmEngine { - inner: Rc>, +pub fn step(_step_budget: u32) -> Uint8Array { + empty_bytes() } -wasm_vector_type!( - "Position vector expressed in meters.\n\nProvides deterministic float32 components shared between host and Wasm callers. Callers must supply finite values; non-finite components will cause construction to panic.\n\n# Usage\nPass a `Position` reference to `WasmEngine::spawn_motion_entity` to seed an entity's initial transform.\n\n# Example\n```\nlet position = Position::new(1.0, 2.0, 3.0);\n```\n", - Position, - "Creates a new position vector.", - "Returns the X component in meters.", - "Returns the Y component in meters.", - "Returns the Z component in meters." -); - -wasm_vector_type!( - "Velocity vector expressed in meters/second.\n\nEncapsulates deterministic float32 velocity components used by the motion demo rewrite. Callers must supply finite values; non-finite components will cause construction to panic.\n\n# Usage\nConstruct a `Velocity` and pass it by reference to `WasmEngine::spawn_motion_entity` alongside a `Position` to initialise entity motion.\n\n# Example\n```\nlet velocity = Velocity::new(0.5, -1.0, 0.25);\n```\n", - Velocity, - "Creates a new velocity vector.", - "Returns the X component in meters/second.", - "Returns the Y component in meters/second.", - "Returns the Z component in meters/second." -); - -impl Default for WasmEngine { - fn default() -> Self { - Self::new() - } + +/// Drain emitted ViewOps since last drain. Placeholder: returns empty array. +#[wasm_bindgen] +pub fn drain_view_ops() -> Uint8Array { + empty_bytes() } +/// Get head info (tick/seq/state root). Placeholder: returns empty bytes. #[wasm_bindgen] -impl WasmEngine { - #[wasm_bindgen(constructor)] - /// Creates a new engine with the motion rule registered. - pub fn new() -> WasmEngine { - WasmEngine { - inner: Rc::new(RefCell::new(build_engine())), - } - } +pub fn get_head() -> Uint8Array { + empty_bytes() +} - #[wasm_bindgen] - /// Spawns an entity with encoded motion payload. - /// - /// * `label` – stable identifier used to derive the entity node id. Must be - /// unique for the caller's scope. - /// * `position` – initial position in meters. - /// * `velocity` – velocity components in meters/second. - /// - /// Returns the 32-byte node id as a `Uint8Array` for JavaScript consumers. - pub fn spawn_motion_entity( - &self, - label: &str, - position: &Position, - velocity: &Velocity, - ) -> Uint8Array { - let mut engine = self.inner.borrow_mut(); - let node_id = make_node_id(label); - let entity_type = make_type_id("entity"); - let payload = encode_motion_atom_payload(position.components(), velocity.components()); - - if let Err(_err) = engine.insert_node_with_attachment( - node_id, - NodeRecord { ty: entity_type }, - Some(AttachmentValue::Atom(payload)), - ) { - #[cfg(feature = "console-panic")] - web_sys::console::error_1( - &format!("spawn_motion_entity failed for node {node_id:?}: {_err:?}").into(), - ); - return Uint8Array::new_with_length(0); - } +/// Execute a read-only query by ID with canonical vars. +#[wasm_bindgen] +pub fn execute_query(_query_id: u32, _vars_bytes: &[u8]) -> Uint8Array { + let reg = registry().expect("registry not installed"); + let Some(op) = reg.op_by_id(_query_id) else { + #[cfg(feature = "console-panic")] + web_sys::console::error_1(&format!("execute_query: unknown op_id {_query_id}").into()); + return empty_bytes(); + }; - Uint8Array::from(node_id.as_bytes().as_slice()) + // PURITY GUARD: Refuse to execute Mutations via execute_query. + if op.kind != echo_registry_api::OpKind::Query { + #[cfg(feature = "console-panic")] + web_sys::console::error_1( + &format!( + "execute_query purity violation: op '{}' is a Mutation", + op.name + ) + .into(), + ); + return empty_bytes(); } - #[wasm_bindgen] - /// Begins a new transaction and returns its identifier. - pub fn begin(&self) -> u64 { - self.inner.borrow_mut().begin().value() + // Decode and validate vars against schema + let Ok(value) = decode_cbor::(_vars_bytes) else { + #[cfg(feature = "console-panic")] + web_sys::console::error_1(&"execute_query: failed to decode CBOR vars".into()); + return empty_bytes(); + }; + if !validate_object_against_args(&value, op.args, reg.all_enums()) { + #[cfg(feature = "console-panic")] + web_sys::console::error_1( + &format!( + "execute_query: schema validation failed for op '{}'", + op.name + ) + .into(), + ); + return empty_bytes(); } - #[wasm_bindgen] - /// Applies the motion rewrite to the entity identified by `entity_id`. - /// - /// Returns `true` on success and `false` if the transaction id, entity id, - /// or rule match is invalid. Future revisions will surface richer error - /// information. - pub fn apply_motion(&self, tx_id: u64, entity_id: &[u8]) -> bool { - if tx_id == 0 { - return false; - } - let node_id = match bytes_to_node_id(entity_id) { - Some(id) => id, - None => return false, - }; - let mut engine = self.inner.borrow_mut(); - match engine.apply(TxId::from_raw(tx_id), MOTION_RULE_NAME, &node_id) { - Ok(ApplyResult::Applied) => true, - Ok(ApplyResult::NoMatch) => false, - Err(_) => false, - } - } + // TODO: execute against read-only graph once available. + empty_bytes() +} - #[wasm_bindgen] - /// Commits the transaction and returns the resulting snapshot hash. - pub fn commit(&self, tx_id: u64) -> Option> { - if tx_id == 0 { - return None; - } - let mut engine = self.inner.borrow_mut(); - let snapshot = engine.commit(TxId::from_raw(tx_id)).ok()?; - Some(snapshot.hash.to_vec()) - } +/// Snapshot at a tick (sandbox replay). Placeholder: returns empty bytes. +#[wasm_bindgen] +pub fn snapshot_at(_tick: u64) -> Uint8Array { + empty_bytes() +} - #[wasm_bindgen] - /// Reads the decoded position/velocity tuple for the provided entity. - pub fn read_motion(&self, entity_id: &[u8]) -> Option> { - let engine = self.inner.borrow(); - let node_id = bytes_to_node_id(entity_id)?; - let payload = match engine.node_attachment(&node_id) { - Ok(Some(value)) => value, - Ok(None) => return None, - Err(_) => return None, - }; - let AttachmentValue::Atom(payload) = payload else { - return None; - }; - let (position, velocity) = decode_motion_atom_payload(payload)?; - let mut data = Vec::with_capacity(6); - data.extend_from_slice(&position); - data.extend_from_slice(&velocity); - Some(data.into_boxed_slice()) +/// Render a snapshot to ViewOps. Placeholder: returns empty bytes. +#[wasm_bindgen] +pub fn render_snapshot(_snapshot_bytes: &[u8]) -> Uint8Array { + empty_bytes() +} + +/// Return registry metadata (schema hash, codec id, registry version). +#[wasm_bindgen] +pub fn get_registry_info() -> Uint8Array { + let reg = registry().expect("registry not installed"); + let info = reg.info(); + #[derive(serde::Serialize)] + struct Info<'a> { + codec_id: &'a str, + registry_version: u32, + schema_sha256_hex: &'a str, } + let dto = Info { + codec_id: info.codec_id, + registry_version: info.registry_version, + schema_sha256_hex: info.schema_sha256_hex, + }; + match encode_cbor(&dto) { + Ok(bytes) => Uint8Array::from(bytes.as_slice()), + Err(_) => empty_bytes(), + } +} + +#[wasm_bindgen] +/// Get the codec identifier from the installed registry. +pub fn get_codec_id() -> JsValue { + registry() + .map(|r| JsValue::from_str(r.info().codec_id)) + .unwrap_or_else(|| JsValue::NULL) +} + +#[wasm_bindgen] +/// Get the registry version from the installed registry. +pub fn get_registry_version() -> JsValue { + registry() + .map(|r| JsValue::from_f64(r.info().registry_version as f64)) + .unwrap_or_else(|| JsValue::NULL) +} + +#[wasm_bindgen] +/// Get the schema hash (hex) from the installed registry. +pub fn get_schema_sha256_hex() -> JsValue { + registry() + .map(|r| JsValue::from_str(r.info().schema_sha256_hex)) + .unwrap_or_else(|| JsValue::NULL) } -#[cfg(all(test, target_arch = "wasm32"))] -mod tests { +#[cfg(test)] +mod schema_validation_tests { use super::*; - use wasm_bindgen_test::*; - - fn spawn(engine: &WasmEngine) -> Vec { - let position = Position::new(1.0, 2.0, 3.0); - let velocity = Velocity::new(0.5, -1.0, 0.25); - engine - .spawn_motion_entity("entity-wasm", &position, &velocity) - .to_vec() + use echo_registry_api::{ArgDef, EnumDef}; + + fn enums_theme() -> Vec { + vec![EnumDef { + name: "Theme", + values: &["LIGHT", "DARK", "SYSTEM"], + }] + } + + #[test] + fn reject_unknown_keys() { + let args = vec![ArgDef { + name: "path", + ty: "String", + required: true, + list: false, + }]; + let val = serde_json::json!({"path":"ok","extra":1}); + let val_sv: serde_value::Value = serde_value::to_value(val).unwrap(); + assert!(!validate_object_against_args( + &val_sv, + &args, + &enums_theme(), + )); + } + + #[test] + fn reject_missing_required() { + let args = vec![ArgDef { + name: "path", + ty: "String", + required: true, + list: false, + }]; + let val = serde_json::json!({}); + let val_sv: serde_value::Value = serde_value::to_value(val).unwrap(); + assert!(!validate_object_against_args( + &val_sv, + &args, + &enums_theme(), + )); + } + + #[test] + fn reject_enum_mismatch() { + let args = vec![ArgDef { + name: "mode", + ty: "Theme", + required: true, + list: false, + }]; + let val = serde_json::json!({"mode":"WRONG"}); + let val_sv: serde_value::Value = serde_value::to_value(val).unwrap(); + assert!(!validate_object_against_args( + &val_sv, + &args, + &enums_theme(), + )); } - #[wasm_bindgen_test] - fn wasm_motion_is_deterministic() { - let engine_a = WasmEngine::new(); - let handle_a = spawn(&engine_a); - let tx_a = engine_a.begin(); - assert!(engine_a.apply_motion(tx_a, &handle_a)); - let hash_a = engine_a.commit(tx_a).expect("snapshot"); - - let engine_b = WasmEngine::new(); - let handle_b = spawn(&engine_b); - let tx_b = engine_b.begin(); - assert!(engine_b.apply_motion(tx_b, &handle_b)); - let hash_b = engine_b.commit(tx_b).expect("snapshot"); - - assert_eq!(hash_a, hash_b); - - let motion = engine_a.read_motion(&handle_a).expect("motion payload"); - assert!((motion[0] - 1.5).abs() < 1e-6); - assert!((motion[1] - 1.0).abs() < 1e-6); - assert!((motion[2] - 3.25).abs() < 1e-6); - assert!((motion[3] - 0.5).abs() < 1e-6); - assert!((motion[4] + 1.0).abs() < 1e-6); - assert!((motion[5] - 0.25).abs() < 1e-6); + #[test] + fn reject_unknown_type() { + let args = vec![ArgDef { + name: "obj", + ty: "AppState", + required: true, + list: false, + }]; + let val = serde_json::json!({"obj":{"routePath":"/"}}); + let val_sv: serde_value::Value = serde_value::to_value(val).unwrap(); + assert!(!validate_object_against_args( + &val_sv, + &args, + &enums_theme(), + )); } } diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index d4e9dc2a..7b151ccc 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -30,13 +30,6 @@ export default defineConfig({ { text: 'WVP', link: '/spec-warp-view-protocol' } ] }, - { - text: 'Log', - items: [ - { text: 'Execution Plan', link: '/execution-plan' }, - { text: 'Decision Log', link: '/decision-log' } - ] - } ], sidebar: { '/': [ @@ -63,13 +56,6 @@ export default defineConfig({ text: 'Subsystem Hubs', items: [{ text: 'Scheduler', link: '/scheduler' }] }, - { - text: 'Project Log', - items: [ - { text: 'Execution Plan', link: '/execution-plan' }, - { text: 'Decision Log', link: '/decision-log' } - ] - } ], '/guide/': [ { diff --git a/docs/METHODOLOGY.md b/docs/METHODOLOGY.md index b56bd2b7..a1bcac7b 100644 --- a/docs/METHODOLOGY.md +++ b/docs/METHODOLOGY.md @@ -110,7 +110,6 @@ A feature is not "Done" until: - [ ] The code builds and tests pass: `cargo test`. - [ ] Public APIs are documented and the docs gate is clean: `cargo clippy --all-targets -- -D missing_docs`. - [ ] SPDX header policy is satisfied: `scripts/check_spdx.sh --check --all`. -- [ ] Docs Guard is satisfied: update `docs/execution-plan.md` and `docs/decision-log.md` when non-doc code changes. - [ ] If the change is spec-facing: a `specs/spec-XXX` directory exists and the spec page explains the concept. - [ ] If the change is spec-facing: the spec imports the relevant kernel slice and provides an interactive demo harness. - [ ] If/when certification is enabled: the spec defines a deterministic “win condition” that can emit a completion proof (planned; not yet implemented). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 01e8f50f..deefc1fb 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -5,9 +5,7 @@ This roadmap reconciles our current plan with GitHub milestones, issues, and the Project board (Project 9). It is the single source of truth for “what’s next”. If you feel lost in doc sprawl, use this order: -- `docs/ROADMAP.md` (this file): milestones and what we’re doing next -- `docs/execution-plan.md`: day-by-day intent and active threads -- `docs/decision-log.md`: what we decided and why +- `docs/ROADMAP.md` (this file): milestones and what we're doing next - `docs/docs-index.md`: map of everything else (only when needed) --- diff --git a/docs/THEORY.md b/docs/THEORY.md index d6495d19..20359215 100644 --- a/docs/THEORY.md +++ b/docs/THEORY.md @@ -975,4 +975,4 @@ When Echo diverges, it should not be a mystery or an accident. > [!note] > Echo does this differently (by policy). Echo prioritizes determinism + replayability *and* runtime performance. It works like this because Echo is meant to run real simulations, not just prove theorems — but every deviation from the Foundations series should be explained so readers can map paper concepts to the codebase without guesswork. -For canonical mappings and explicit deviation rationale, see `docs/aion-papers-bridge.md` and `docs/decision-log.md`. +For canonical mappings and explicit deviation rationale, see `docs/aion-papers-bridge.md`. diff --git a/docs/adr/ADR-0003-Materialization-Bus.md b/docs/adr/ADR-0003-Materialization-Bus.md new file mode 100644 index 00000000..712a8386 --- /dev/null +++ b/docs/adr/ADR-0003-Materialization-Bus.md @@ -0,0 +1,183 @@ + + +# ADR-000X: Causality-First API — Ingress + MaterializationPort, No Direct Graph Writes + +- **Status:** Accepted +- **Date:** 2026-01-14 +- **Deciders:** James Ross Ω (and the increasingly judgmental kernel) +- **Context:** Echo / WARP deterministic runtime; web + tooling + inspector ecosystem; strict determinism and provenance requirements. + +--- + +## Context + +Echo/WARP is a deterministic system where: + +1. **WARP State** is a projection (a derived worldline snapshot). +2. **Causality** is the true API (the event/intent ledger that produces state). + +We want: + +- Generic web-based devtools/inspectors +- Local-in-browser runtime mode (WASM) +- Remote runtime mode (native process + WebSocket) +- High reliability at the boundary (retries, disconnects) +- **Zero non-deterministic mutation paths** +- Clean separation of concerns: + - **warp-core stays pure** + - boundary “weirdness” lives at ports/adapters + +We reject exposing raw engine primitives (tx/apply/insert) to tools. Tools should not mutate state “directly”. They should emit causal events. + +> Note: “causal” (not casual). The kernel is not wearing Crocs. + +--- + +## Decision + +### 1) All writes go through **Ingress** (the causal boundary) + +**Rule:** _Literally everything that touches the graph MUST go through the inbox/ingress._ + +- No public `apply(rule, scope)` +- No public `insert_node(...)` +- No public `tx_begin/commit/abort` +- No direct graph mutations from JS/TS or tools + +Instead, all writes are: + +- `ingest_intent(intent_bytes)` (canonical bytes only) +- runtime assigns canonical sequence numbers +- kernel applies rewrites during ticks as internal mechanics + +This makes the ingress/ledger the **API for causality**. + +--- + +### 2) Reads are bus-first via **MaterializationBus**, crossed by a **MaterializationPort** + +We distinguish: + +- **MaterializationBus (internal runtime)**: derived updates emitted during ticks +- **MaterializationPort (boundary API)**: subscriptions, batching, replay(1), transport + +For application UI, we prefer: +- “no direct reads” from WARP state (when feasible) +- UI driven by **bus materializations** (channels) + +Direct state reads exist only for inspectors and are capability-gated. + +--- + +### 3) Two transports share one protocol (bytes-only frames) + +The same binary protocol runs over: + +- **Local Mode:** WASM exports returning `Uint8Array` +- **Remote Mode:** WebSocket **binary** messages (same frames) + +No JSON protocol. No stringly-typed nonsense at the boundary. + +--- + +### 4) Resiliency (“Nine Tails”) lives at the Port via idempotent ingress + +Retries must not create duplicate causality. + +- Define `intent_id = H(intent_bytes)` (hash of canonical intent bytes) +- Ingress is **idempotent**: `intent_id -> seq_assigned` +- Retries return `DUPLICATE` + original seq (no new ledger entry) + +This provides “at-least-once delivery” with “exactly-once causality”. + +--- + +### 5) If/when needed, outbound side effects use a **Causal Outbox (Egress)** + +We may later introduce: + +- **EgressQueue / CausalOutbox**: outbound messages written into the graph during ticks +- Delivery agent sends messages (transport is at-least-once) +- **Acks are causal**: delivery success writes back via Ingress as an ack intent +- Idempotent key: `msg_id = H(message_bytes)` + +This preserves determinism while supporting real-world IO. + +--- + +## System Model + +### Core entities + +- **Ledger (Causality):** append-only input event stream +- **WARP State (Worldline):** deterministic projection of ledger +- **Ingress:** only way to add causal events +- **MaterializationBus:** ephemeral derived outputs from ticks +- **MaterializationPort:** subscription/bridge for bus delivery +- **InspectorPort:** optional direct state reads (gated) + +--- + +## API Surfaces + +### A) WarpIngress (write-only) + +- `ingest_intent(intent_bytes) -> ACKI | ERR!` +- `ingest_intents(batch_bytes) -> ACKI* | ERR!` (optional later) + +Notes: +- intent must be canonical bytes (e.g. `EINT` envelope v1) +- kernel assigns canonical `seq` +- idempotent by `intent_id = H(intent_bytes)` + +--- + +### B) MaterializationPort (bus-driven reads) + +The bus is: + +- **Unidirectional:** `tick -> emit -> subscribers` +- **Ephemeral:** you must be subscribed to see the stream +- **Stateless relative to ledger:** not a source of truth +- **Replay(1) per channel:** caches last materialized value for late joiners + +Port operations (conceptual): + +- `view_subscribe(channels, replay_last) -> sub_id` +- `view_replay_last(sub_id, max_bytes) -> VOPS` +- `view_drain(sub_id, max_bytes) -> VOPS` +- `view_unsubscribe(sub_id)` + +Channel identity: +- `channel_id = 32 bytes` (TypeId-derived; no strings in ABI) + +View ops are deterministic and coalesced (recommended): +- last-write-wins per channel per tick + +--- + +### C) InspectorPort (direct reads, gated) + +Used only for devtools/inspectors; not required for app UI: + +- `read_node(node_id) -> NODE | ERR!` +- `read_attachment(node_id) -> ATTM | ERR!` +- `read_edges_from(node_id, limit) -> EDGE | ERR!` +- `read_edges_to(node_id, limit) -> EDGE | ERR!` + +All enumerations must be canonical order (sorted). + +--- + +## Protocol: FrameV1 (bytes-only) + +All boundary messages use the same framing: + +```text +FrameV1: + magic[4] ASCII (e.g. 'VOPS', 'HEAD') + ver_u16 = 1 + kind_u16 (optional if magic is sufficient; reserved for future) + len_u32 payload byte length + payload[len] +(all integers little-endian) diff --git a/docs/adr/ADR-0004-No-Global-State.md b/docs/adr/ADR-0004-No-Global-State.md new file mode 100644 index 00000000..89d0aa2b --- /dev/null +++ b/docs/adr/ADR-0004-No-Global-State.md @@ -0,0 +1,176 @@ + + +# ADR-000Y: No Global State in Echo — Dependency Injection Only + +- **Status:** Accepted +- **Date:** 2026-01-14 + +## Context + +Global mutable state undermines determinism, testability, and provenance. It creates hidden initialization dependencies (“did `install_*` run?”), makes behavior environment-dependent, and complicates WASM vs native parity. Rust patterns like `OnceLock`/`LazyLock` are safe but still encode hidden global wiring. + +Echo’s architecture benefits from explicit dependency graphs: runtime/kernel as values, ports as components, and deterministic construction. + +## Decision + +### 1) Global state is banned + +Forbidden in Echo/WARP runtime and core crates: +- `OnceLock`, `LazyLock`, `lazy_static!`, `once_cell`, `thread_local!` +- `static mut` +- process-wide “install_*” patterns for runtime dependencies + +Allowed: +- `const` and immutable `static` data (tables, magic bytes, version tags) +- pure functions and types + +### 2) Dependencies are injected explicitly + +All runtime dependencies must be carried by structs and passed explicitly: + +- `EchoKernel { engine, registry, ingress, bus, … }` +- `MaterializationBus` lives inside the runtime +- Registry/codec providers are fields or type parameters (compile-time wiring preferred) + +### 3) Enforced by tooling + +We add a CI ban script that fails builds if forbidden patterns appear in protected crates. Exceptions require explicit allowlisting and justification. + +## Consequences + +- Determinism audits become simpler (no hidden wiring) +- Tests construct runtimes with explicit dependencies +- WASM and native implementations share the same dependency model +- Eliminates global init ordering bugs and accidental shared state across tools + +## Appendix: Ban Global State + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# Ban global state patterns in Echo/WARP core crates. +# Goal: no hidden wiring, no init-order dependency, no mutable process-wide state. +# +# Allowed: +# - const +# - immutable static data (e.g. magic bytes, lookup tables) +# +# Forbidden: +# - OnceLock/LazyLock/lazy_static/once_cell/thread_local/static mut +# - "install_*" global init patterns (heuristic) +# +# Usage: +# ./scripts/ban-globals.sh +# +# Optional env: +# BAN_GLOBALS_PATHS="crates/warp-core crates/warp-wasm crates/flyingrobots-echo-wasm" +# BAN_GLOBALS_ALLOWLIST=".ban-globals-allowlist" + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +PATHS_DEFAULT="crates/warp-core crates/warp-wasm crates/flyingrobots-echo-wasm crates/echo-wasm-abi" +PATHS="${BAN_GLOBALS_PATHS:-$PATHS_DEFAULT}" + +ALLOWLIST="${BAN_GLOBALS_ALLOWLIST:-.ban-globals-allowlist}" + +# ripgrep is fast and consistent +if ! command -v rg >/dev/null 2>&1; then + echo "ERROR: ripgrep (rg) is required." >&2 + exit 2 +fi + +# Patterns are conservative on purpose. +# If you truly need an exception, add an allowlist line with a justification. +PATTERNS=( + '\bOnceLock\b' + '\bLazyLock\b' + '\blazy_static!\b' + '\bonce_cell\b' + '\bthread_local!\b' + '\bstatic mut\b' + '\bunsafe\s*\{' # optional: uncomment if you want "no unsafe" too + '\binstall_[a-zA-Z0-9_]*\b' # heuristic: discourages "install_registry" style globals +) + +# You may want to allow `unsafe` in some crates; if so, delete the unsafe pattern above. + +echo "ban-globals: scanning paths:" +for p in $PATHS; do echo " - $p"; done +echo + +# Build rg args +RG_ARGS=(--hidden --no-ignore --glob '!.git/*' --glob '!target/*' --glob '!**/node_modules/*') + +# Apply allowlist as inverted matches (each line is a regex or fixed substring) +# Allowlist format: +# \t +# or: +# +ALLOW_RG_EXCLUDES=() +if [[ -f "$ALLOWLIST" ]]; then + # Read first column (pattern) per line, ignore comments + while IFS= read -r line; do + [[ -z "$line" ]] && continue + [[ "$line" =~ ^# ]] && continue + pat="${line%%$'\t'*}" + pat="${pat%% *}" # also allow space-separated + [[ -z "$pat" ]] && continue + # Exclude lines matching allowlisted pattern + ALLOW_RG_EXCLUDES+=(--glob "!$pat") + done < "$ALLOWLIST" +fi + +violations=0 + +for pat in "${PATTERNS[@]}"; do + echo "Checking: $pat" + # We can't "glob exclude by line"; allowlist is file-level. Keep it simple: + # If you need surgical exceptions, prefer moving code or refactoring. + if rg "${RG_ARGS[@]}" -n -S "$pat" $PATHS; then + echo + violations=$((violations+1)) + else + echo " OK" + fi + echo +done + +if [[ $violations -ne 0 ]]; then + echo "ban-globals: FAILED ($violations pattern group(s) matched)." + echo "Fix the code or (rarely) justify an exception in $ALLOWLIST." + exit 1 +fi + +echo "ban-globals: PASSED." +``` + +### Optional: Allow List File + +```text +# Keep this tiny. If it grows, you’re lying to yourself. + +# Example (file-level exclusion): +# crates/some-crate/src/vendor/* vendored code, cannot change +``` + +### CI Script + +```bash +./scripts/ban-globals.sh +``` + +## Appendix B: README Brag + +```markdown +## Determinism Doctrine: No Global State + +Echo forbids global mutable state. No init-once singletons, no hidden wiring, no process-wide “install_*” hooks. +All dependencies (registry, codecs, ports, buses) are injected explicitly via runtime structs. + +Why: global state breaks provenance, complicates replay, and creates “it depends how you booted it” bugs. + +Enforcement: `./scripts/ban-globals.sh` runs in CI and rejects forbidden patterns (`OnceLock`, `LazyLock`, `lazy_static`, `thread_local`, `static mut`, etc.). +See ADR-000Y: **No Global State**. +``` diff --git a/docs/adr/ADR-0005-Physics.md b/docs/adr/ADR-0005-Physics.md new file mode 100644 index 00000000..82e8395d --- /dev/null +++ b/docs/adr/ADR-0005-Physics.md @@ -0,0 +1,126 @@ + + +# ADR-0005: Physics as Deterministic Scheduled Rewrites (Footprints + Phases) + +- **Status:** Accepted +- **Date:** 2026-01-14 + +## Context + +Echo runs deterministic ticks over a WARP graph. Some subsystems (physics, constraints, layout) require multi-pass updates and must remain: + +- deterministic across platforms +- schedulable using independence/footprints (confluence-friendly) +- isolated from the public causality API (Inbox/Ingress remains sacred) + +Physics introduces: +- broadphase candidate generation +- narrowphase contact computation (possibly swept / TOI) +- iterative constraint resolution +- multi-body coupling (piles) that emerges from pairwise contacts + +We must avoid designs that serialize derived work into an “inbox-like” ordered stream, which destroys independence batching and confluence benefits. + +## Decision + +### 1) Physics runs as an internal tick phase, not as causal ingress + +Physics is a system phase executed during `step()`. It does not emit causal events into the ledger. +Causal inputs remain: ingested intents (and optionally a causal `dt` parameter). + +### 2) Contacts are modeled as rewrite candidates with explicit footprints + +Physics resolution is expressed as rewrite candidates operating over multiple bodies. + +- Each candidate represents a contact constraint between bodies A and B. +- Footprint writeset includes `{A, B}` (and any shared state they mutate). +- Candidates commute iff their footprints are disjoint. + +This maps directly onto Echo’s scheduler: deterministic conflict resolution and maximal independence batching. + +### 3) Candidate selection is set-based, not queue-based + +Broadphase produces a *set* of candidates. The scheduler: +- canonicalizes ordering +- selects a maximal independent subset (disjoint footprints) +- applies them in deterministic order +- repeats for a fixed number of solver iterations + +We do **not** re-inject derived candidates into a “micro-inbox” stream, because ordered queues erase concurrency benefits. + +### 4) Deterministic ordering rules are mandatory + +All physics candidate generation and application must be canonical: + +- Bodies enumerated in sorted `NodeId` order +- Candidate pair key: `(min(A,B), max(A,B))` +- If swept/TOI is used, include `toi_q` (quantized) in the sort key +- If a manifold produces multiple contacts, include a stable `feature_id` in the key + +Recommended canonical key: +`(toi_q, min_id, max_id, feature_id)` + +### 5) Two-pass / multi-pass is implemented as phases + bounded iterations + +Physics tick phase executes as: + +1. **Integrate (predict)** into working state (or `next`) deterministically. +2. **Generate contact candidates** deterministically (broadphase + narrowphase). +3. **Solve** using K fixed iterations: + - each iteration selects a maximal independent subset via footprints + - apply in canonical order +4. **Finalize (commit)**: swap/commit working state for the tick. + +Iteration budgets are fixed: +- `K_SOLVER_ITERS` (e.g., 4–10) +- Optional `MAX_CCD_STEPS` if swept collisions are enabled + +Optional early exit is allowed only if the quiescence check is deterministic (e.g., no writes occurred or hash unchanged). + +### 6) Swept collisions (CCD) are supported via earliest-TOI banding (optional) + +If CCD is enabled: + +- Compute TOI per candidate pair in `[0, dt]` +- Quantize TOI to an integer bucket `toi_q` +- Choose the minimum bucket `toi_q*` +- Collect all candidates with `toi_q <= toi_q* + ε_bucket` into the solve set +- Solve as above (K iters) +- Advance remaining time and repeat up to `MAX_CCD_STEPS` + +N-body “simultaneous collisions” are treated as connected components in the footprint graph (collision islands), not as N-ary collision events. + +### 7) MaterializationBus emits only post-phase, post-commit outputs + +The UI-facing MaterializationBus (channels) emits after physics stabilization for the tick. +No half-updated physics state is observable via materializations. + +### 8) Inspector visibility is via trace channels, not causal ledger + +Optional debug-only materializations may be emitted: +- `trace/physics/candidates` +- `trace/physics/selected` +- `trace/physics/islands` +- `trace/physics/iters` + +These are outputs, not inputs. + +## Consequences + +### Positive + +- Physics reuses existing determinism machinery (scheduler + footprints) +- Preserves maximal independence batching (confluence-friendly) +- Multi-pass behavior is explicit, bounded, and reproducible +- Works in both wasm and native runtimes with the same semantics + +### Negative / Tradeoffs + +- Requires careful canonical ordering in broadphase/narrowphase +- Requires fixed iteration budgets for convergence (predictable but not “perfect”) +- CCD adds complexity; may be deferred until needed + +## Notes + +Physics is not a public API. It is an internal deterministic phase driven by causal inputs. +Candidate sets + footprint scheduling preserve concurrency benefits; queueing derived work does not. diff --git a/docs/adr/ADR-0006-Ban-Non-Determinism.md b/docs/adr/ADR-0006-Ban-Non-Determinism.md new file mode 100644 index 00000000..0fb47661 --- /dev/null +++ b/docs/adr/ADR-0006-Ban-Non-Determinism.md @@ -0,0 +1,221 @@ + + +## T2000 on 'em + +We already have the **ban-globals** drill sergeant. Now we add the rest of the "you will cry" suite: + +- **ban nondeterministic APIs** +- **ban unordered containers in ABI-ish structs** +- **ban time/rand/JSON** +- **fail CI hard**. + +Below is a clean, repo-friendly setup. + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# Determinism Drill Sergeant: ban nondeterministic APIs and patterns +# +# Usage: +# ./scripts/ban-nondeterminism.sh +# +# Optional env: +# DETERMINISM_PATHS="crates/warp-core crates/warp-wasm crates/flyingrobots-echo-wasm" +# DETERMINISM_ALLOWLIST=".ban-nondeterminism-allowlist" + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +if ! command -v rg >/dev/null 2>&1; then + echo "ERROR: ripgrep (rg) is required." >&2 + exit 2 +fi + +PATHS_DEFAULT="crates/warp-core crates/warp-wasm crates/flyingrobots-echo-wasm crates/echo-wasm-abi" +PATHS="${DETERMINISM_PATHS:-$PATHS_DEFAULT}" + +ALLOWLIST="${DETERMINISM_ALLOWLIST:-.ban-nondeterminism-allowlist}" + +RG_ARGS=(--hidden --no-ignore --glob '!.git/*' --glob '!target/*' --glob '!**/node_modules/*') + +# You can allow file-level exceptions via allowlist (keep it tiny). +ALLOW_GLOBS=() +if [[ -f "$ALLOWLIST" ]]; then + while IFS= read -r line; do + [[ -z "$line" ]] && continue + [[ "$line" =~ ^# ]] && continue + pat="${line%%$'\t'*}" + pat="${pat%% *}" + [[ -z "$pat" ]] && continue + ALLOW_GLOBS+=(--glob "!$pat") + done < "$ALLOWLIST" +fi + +# Patterns: conservative and intentionally annoying. +# If you hit a false positive, refactor; don't immediately allowlist. +PATTERNS=( + # Time / entropy (core determinism killers) + '\bstd::time::SystemTime\b' + '\bSystemTime::now\b' + '\bstd::time::Instant\b' + '\bInstant::now\b' + '\bstd::thread::sleep\b' + '\b(tokio|async_std)::time::sleep\b' + + # Randomness + '\brand::\b' + '\bgetrandom::\b' + '\bfastrand::\b' + + # Unordered containers that will betray you if they cross a boundary + '\bstd::collections::HashMap\b' + '\bstd::collections::HashSet\b' + '\bhashbrown::HashMap\b' + '\bhashbrown::HashSet\b' + + # JSON & “helpful” serialization in core paths + '\bserde_json::\b' + '\bserde_wasm_bindgen::\b' + + # Float nondeterminism hotspots (you can tune these) + '\b(f32|f64)::NAN\b' + '\b(f32|f64)::INFINITY\b' + '\b(f32|f64)::NEG_INFINITY\b' + '\.sin\(' + '\.cos\(' + '\.tan\(' + '\.sqrt\(' + '\.pow[f]?\(' + + # Host/environment variability + '\bstd::env::\b' + '\bstd::fs::\b' + '\bstd::process::\b' + + # Concurrency primitives (optional—uncomment if you want core to be single-thread-only) + # '\bstd::sync::Mutex\b' + # '\bstd::sync::RwLock\b' + # '\bstd::sync::atomic::\b' +) + +echo "ban-nondeterminism: scanning paths:" +for p in $PATHS; do echo " - $p"; done +echo + +violations=0 +for pat in "${PATTERNS[@]}"; do + echo "Checking: $pat" + if rg "${RG_ARGS[@]}" "${ALLOW_GLOBS[@]}" -n -S "$pat" $PATHS; then + echo + violations=$((violations+1)) + else + echo " OK" + fi + echo +done + +if [[ $violations -ne 0 ]]; then + echo "ban-nondeterminism: FAILED ($violations pattern group(s) matched)." + echo "Fix the code or (rarely) justify an exception in $ALLOWLIST." + exit 1 +fi + +echo "ban-nondeterminism: PASSED." +## +``` + +### Optional Allow-List + +```text +# Example: +# crates/some-crate/tests/* tests can use time/rand, not core +``` + +## ABI Police + +### `scripts/ban-unordered-abi.sh` + +This one is narrower: + +- **ban HashMap/HashSet inside anything that looks like ABI/codec/message structs**. + +It’s opinionated: if it’s named like it crosses a boundary, it must be ordered. + +```bash +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +if ! command -v rg >/dev/null 2>&1; then + echo "ERROR: ripgrep (rg) is required." >&2 + exit 2 +fi + +# Adjust these to your repo conventions +ABI_HINTS=( + "abi" + "codec" + "message" + "frame" + "packet" + "envelope" + "dto" + "wire" +) + +RG_ARGS=(--hidden --no-ignore --glob '!.git/*' --glob '!target/*' --glob '!**/node_modules/*') + +# Find Rust files likely involved in ABI/wire formats. +files=$(rg "${RG_ARGS[@]}" -l -g'*.rs' "$(printf '%s|' "${ABI_HINTS[@]}")" crates/ || true) + +if [[ -z "${files}" ]]; then + echo "ban-unordered-abi: no ABI-ish files found (by heuristic). OK." + exit 0 +fi + +echo "ban-unordered-abi: scanning ABI-ish Rust files..." +violations=0 + +# HashMap/HashSet are not allowed in ABI-ish types. Use Vec<(K,V)> sorted, BTreeMap, IndexMap with explicit canonicalization, etc. +if rg "${RG_ARGS[@]}" -n -S '\b(HashMap|HashSet)\b' $files; then + violations=$((violations+1)) +fi + +if [[ $violations -ne 0 ]]; then + echo "ban-unordered-abi: FAILED. Unordered containers found in ABI-ish code." + echo "Fix by using Vec pairs + sorting, or BTreeMap + explicit canonical encode ordering." + exit 1 +fi + +echo "ban-unordered-abi: PASSED." +``` + +## CI Wire It In + +```bash +./scripts/ban-globals.sh +./scripts/ban-nondeterminism.sh +./scripts/ban-unordered-abi.sh +cargo fmt --check +cargo clippy --all-targets --all-features -- -D warnings +cargo test --all-features +``` + +## README Brag + +```markdown +## Determinism Drill Sergeant (Non-Negotiable) + +Echo is a deterministic system. We enforce this with automated bans. + +- **No global state**: no `OnceLock`, `LazyLock`, `lazy_static`, `thread_local`, `static mut`, or `install_*` singletons. + - Enforced by: `./scripts/ban-globals.sh` +- **No nondeterminism in core**: no time, randomness, JSON convenience layers, unordered ABI containers, or host-environment dependencies in protected crates. + - Enforced by: `./scripts/ban-nondeterminism.sh` and `./scripts/ban-unordered-abi.sh` + +If your change trips these scripts, the fix is not “add an allowlist line.” +The fix is **refactor the design** so determinism is true by construction. +``` diff --git a/docs/aion-papers-bridge.md b/docs/aion-papers-bridge.md index 092502e0..5b031ea2 100644 --- a/docs/aion-papers-bridge.md +++ b/docs/aion-papers-bridge.md @@ -34,9 +34,7 @@ Those identifiers have now been mechanically renamed to **WARP** equivalents (e. **Project policy:** - Prefer **WARP** terminology in human-facing docs going forward. -- When Echo intentionally deviates from the paper design (for performance, ergonomics, or game-engine concerns), we **must** document the deviation and rationale: - - record it in `docs/decision-log.md`, and - - reflect it here (so readers of the papers learn what changed and why). +- When Echo intentionally deviates from the paper design (for performance, ergonomics, or game-engine concerns), we **must** document the deviation and rationale here (so readers of the papers learn what changed and why). Status note: the mechanical rename from `rmg-*` → `warp-*` has landed (crates + the session/tooling surface). The session wire protocol prefers `warp_*` op strings and `warp_id`, but decoders accept legacy `rmg_*` / `rmg_id` as compatibility aliases. @@ -224,7 +222,7 @@ Paper V extracts system-level requirements (examples): - Terminology drift: “RMG” vs “WARP” - Papers use “WARP” as the public substrate name; Echo now uses `warp-*` naming in crates and the session/tooling surface. - - Docs and historical artifacts may still mention “RMG”; keep this note (and `docs/decision-log.md`) explicit about why/when a deviation exists. + - Docs and historical artifacts may still mention "RMG"; keep this note explicit about why/when a deviation exists. - Empty-digest semantics for commit metadata - The engine’s canonical empty *length-prefixed list digest* is `blake3(0u64.to_le_bytes())`. - Keep docs consistent because this changes commit identity. diff --git a/docs/append-only-invariants.md b/docs/append-only-invariants.md deleted file mode 100644 index e29c788b..00000000 --- a/docs/append-only-invariants.md +++ /dev/null @@ -1,27 +0,0 @@ - - -# Append-only invariants for onboarding docs - -The following files record chronological intent (not mutable state), so they must only grow: - -- `AGENTS.md` -- `docs/decision-log.md` -- `TASKS-DAG.md` -- `docs/execution-plan.md` - -**Invariant:** updates may only append new lines; deletions, reorders, or inline modifications that rewrite history are forbidden. - -## Enforcement plan - -1. Run `node scripts/check-append-only.js` (or `APPEND_ONLY_BASE= node scripts/check-append-only.js`) in the pre-merge/CI gate before merging any branch that touches these files. The script compares the current tip to the configured base (default `origin/main`) and fails if any of the tracked paths report deleted lines. CI runs this in the `Append-only Guard` workflow (`.github/workflows/append-only-check.yml`). -2. Update `docs/decision-log.md` and `docs/execution-plan.md` whenever you add a new entry so the logs stay current and the append-only policy is clear. -3. Document the first failing diff in `docs/decision-log.md` (with the new entry) and attribute the policy to this doc so future contributors understand why the check runs. - -## When the check runs - -- The script is intended to be wired into CI (pre-merge or GitHub Action) so a failing branch cannot land without resolving the violation. -- Local developers can run the script manually whenever they touch these files; if the check fails, add new content instead of deleting or editing existing lines. - -## Extending the policy - -If additional append-only artifacts are added in the future, add them to the `files` array inside `scripts/check-append-only.js` and document the change in this file. diff --git a/docs/architecture-outline.md b/docs/architecture-outline.md index dec3a852..7c1df305 100644 --- a/docs/architecture-outline.md +++ b/docs/architecture-outline.md @@ -7,9 +7,14 @@ If you’re new here, start with: - [/guide/start-here](/guide/start-here) - [/guide/warp-primer](/guide/warp-primer) -This document is a high-level architecture and “why” artifact. Some sections are aspirational and +This document is a high-level architecture and "why" artifact. Many sections are aspirational and will lag behind the current Rust-first implementation; prefer WARP specs for the runtime boundary. +> **Implementation Status Legend:** +> - ✅ **Implemented** — exists in `warp-core` today +> - ⚠️ **Partial** — some aspects exist, others planned +> - 🗺️ **Planned** — design only, not yet implemented + ## Vision - Reimagine a battle-tested ECS core into **Echo**, a renderer-agnostic spine that survives browsers, native shells, and whatever 2125 invents next. - Empower teams to build 2D, 3D, or abstract simulations with the same spine, swapping adapters instead of rewriting gameplay. @@ -33,7 +38,9 @@ will lag behind the current Rust-first implementation; prefer WARP specs for the ## Domain Layers -### Core ECS +### Core ECS 🗺️ Planned + +> **Note:** The current `warp-core` implementation uses a **WARP graph model** (nodes, edges, rewrite rules), not traditional ECS archetypes. The ECS storage model below is a future design target. - **Entities**: Numerical IDs with sparse/high-watermark managers; creation returns pooled slots to avoid GC pressure. - **Components**: Type-safe registrations with metadata (layout, default state, pooling policy). Storage uses archetype tables or chunked struct-of-arrays chosen at registration time. - **Storage Model**: @@ -54,13 +61,13 @@ will lag behind the current Rust-first implementation; prefer WARP specs for the - **Parallelism Hooks**: Systems may declare `parallelizable: true`; scheduler groups disjoint signature systems into jobs respecting dependencies. - **Queries**: Precompiled views over component sets; incremental membership tracking uses bitset signatures and dirty queues instead of per-frame scans. -### World & Scene Management +### World & Scene Management 🗺️ Planned - **World**: Owns entity/component managers, system registry, event bus, and service container. Supports multiple worlds for split-screen or background sims. - **Prefabs & Assemblers**: Declarative definitions (JSON/YAML/TS factories) converted into entity creation commands, supporting overrides and inheritance. - **Scene Graph / State Machine**: Stack-based and hierarchical scenes with enter/exit hooks, async loading, and transition orchestration. Integrates with scheduler via scene phases. - **Simulation Contexts**: Support for deterministic replay, remote authority, and sub-step simulations (physics, AI planning) within world boundaries. -### Time & Simulation +### Time & Simulation ⚠️ Partial - **Clock Service**: Abstracted time source with fixed-step accumulator, variable-step mode, and manual stepping for tests. - **Pause & Slow-Mo**: Pause flag propagates to scheduler; systems opt into running while paused; time scaling applies per system when needed. - **Deterministic Replay**: Input/event capture via Codex’s Baby, serialized frame seeds, and re-execution hooks for debugging or multiplayer rollback. @@ -70,14 +77,18 @@ will lag behind the current Rust-first implementation; prefer WARP specs for the - **Kairos (Possibility)**: Branch identifier; indexes alternate realities at the same Chronos tick. - **Aion (Significance)**: Scalar weight describing narrative gravity/entropy; influences merge priority, NPC memory retention, and paradox severity. -### Temporal Sandbox (Echo Edge) +### Temporal Sandbox (Echo Edge) 🗺️ Planned - **Branchable Timelines**: Worlds can fork into speculative branches mid-frame; scheduler runs branches sequentially or in parallel workers, then reports diffs back to the main timeline. - **Frame Scrubbing**: Built-in timeline buffer stores component deltas for the last N frames; editor tooling scrubs, rewinds, and reapplies changes without restarting the sim. - **Predictive Queries**: Renderers, netcode, or AI can request projected state N frames ahead using speculative branches, enabling latency hiding and cinematic planning. - **Collaborative Simulation**: Multiple clients can author in shared scenes by editing branches; consensus commits merge deterministic deltas back into the root world. - **AI Co-Pilot Hooks**: Deterministic branches allow automated agents to propose tweaks, run them in sandboxes, and surface accepted diffs to designers. -## Codex’s Baby (Event Bus) +## Event Bus 🗺️ Planned + +> **Note:** The original "Event Bus" spec has been superseded by [ADR-0003 (MaterializationBus)](/ADR-0003-Materialization-Bus.md). See that document for the current boundary API design. +> +> *The content below is preserved for historical context only; [ADR-0003](/ADR-0003-Materialization-Bus.md) is the authoritative specification.* - **Command Buffers**: Events are POD structs appended to per-type ring buffers during a frame; no immediate callbacks inside hot systems. - **Flush Phases**: Scheduler defines flush points (pre-update, post-update, custom phases). Systems subscribe to phases matching their needs. - **Handler Contracts**: Handlers receive batched slices; they may mutate components, enqueue new events, or schedule commands. Return values are ignored for deterministic execution. @@ -86,38 +97,38 @@ will lag behind the current Rust-first implementation; prefer WARP specs for the - **Integration**: Bridges input devices, networking, scripting, and editor tooling without leaking adapter concerns into the domain. - **Inter-Branch Bridge**: Temporal mail service routes events between branches; deliveries create retro branches when targeting past Chronos ticks; paradox guard evaluates conflicts before enqueue. -## Ports & Adapters +## Ports & Adapters 🗺️ Planned -### Renderer Port +### Renderer Port 🗺️ Planned - **Responsibilities**: Receive frame data (render commands, camera states, debug overlays), manage render resources, and report capabilities. - **Data Flow**: Domain produces a `FramePacket` containing archetype-friendly draw data (mesh refs, transforms, materials); adapter translates into API-specific calls. - **Adapters**: Pixi 7/WebGL2 baseline, Canvas2D fallback, WebGPU (wgpu/WASM), native renderer (Skia or bgfx), experimental TUI renderer for debugging. - **Performance Contracts**: Frame submissions are immutable; adapters can reuse GPU buffers across frames; the port discourages per-entity draw calls. -### Input Port +### Input Port 🗺️ Planned - **Responsibilities**: Aggregate device state into consumable snapshots (buttons, axes, gestures) and surface device capabilities. - **Polling Model**: Domain polls once per frame; port ensures event strata are coalesced in consistent order. Scripted or network input injects via Codex’s Baby. - **Adapters**: Browser (keyboard, mouse, pointer, gamepad), native (SDL), synthetic (playback), test harness stubs. -### Physics Port +### Physics Port 🗺️ Planned - **Responsibilities**: Advance simulation, manage bodies/colliders, and synchronize results back into components. - **Integration Strategy**: Dual writes through data bridges. ECS components represent desired state; physics port returns authoritative transforms/velocities at sync points. - **Adapters**: Box2D (planar), Rapier (3D/2D), custom deterministic solver, or headless stub for puzzle games. - **Advanced Features**: Continuous collision, queries (raycasts, sweeps), event hooks for contacts funneled through Codex’s Baby. -### Networking Port +### Networking Port 🗺️ Planned - **Mode Support**: Single-player (loopback), lockstep peer-to-peer, host-client, dedicated server. - **Transport Abstraction**: Reliable/unreliable channels, clock sync, session management. Adapter options: WebRTC, WebSockets, native sockets. - **Replication Strategy**: Deterministic event replication using Codex’s Baby ledger; optional state snapshots for fast-forward joins. - **Rollback Hooks**: Scheduler exposes rewinding API; networking port coordinates branch rewinds and replays when desync detected. - **Security Considerations**: Capability tokens, branch validation, deterministic checksum comparison to detect tampering. -### Audio, Persistence, Telemetry Ports +### Audio, Persistence, Telemetry Ports 🗺️ Planned - **Audio**: Command queue for spatial/ambient playback, timeline control, and crossfade scheduling. - **Persistence**: Abstract reader/writer for save games, cloud sync, diagnostics dumps. Supports structured snapshots and delta patches. - **Telemetry**: Export frame metrics, event traces, and custom probes to external dashboards or editor overlays. -## Cross-Cutting Concerns +## Cross-Cutting Concerns ⚠️ Partial - **Bootstrap Pipeline**: Dependency injection container wires ports, services, systems, and configuration before the first tick. Supports editor-time hot reload. - **Resource Lifecycle**: Asset handles (textures, meshes, scripts) managed through reference-counted registries and async loaders; domain requests are idempotent. - **Serialization**: Schema-driven serialization for components and events. Allows save/load, network replication, and state diffing. @@ -138,12 +149,15 @@ will lag behind the current Rust-first implementation; prefer WARP specs for the - **Outcome**: Comprehensive reference that prevents accidental feature loss and keeps the rewrite grounded in historical context. ## Delivery Roadmap -- **Phase 0 – Spec Deep Dive**: Finalize ECS storage, scheduler, event bus design; complete excavation log; prototype membership benchmarks. -- **Phase 1 – Echo Core MVP**: Stand up TypeScript monorepo, implement entity/component storage, scheduler, Codex’s Baby, and unit tests with headless harness. -- **Phase 2 – Adapter Foundations** *(Milestone: “Double-Jump”)*: Ship Pixi/WebGL renderer adapter, keyboard/mouse input, basic physics stub, and Vite-based playground. -- **Phase 3 – Advanced Adapters**: Integrate Box2D/Rapier, WebGPU renderer, audio port, and telemetry pipeline; add scene/state tooling. -- **Phase 4 – Tooling & Polishing**: Debug inspector, hot-reload workflows, documentation site, samples, and performance tuning. -- **Ongoing**: Benchmark suite, community feedback loop, compatibility shims for legacy prototypes, incremental releases. + +> **Current Status (2026-01):** Phase 0 is largely complete for `warp-core`. The Rust-first WARP graph rewriting engine is implemented with deterministic scheduling, snapshot hashing, and basic math. ECS storage and system scheduler remain future work. + +- **Phase 0 – Spec Deep Dive** ⚠️ Partial: WARP core specs finalized; ECS storage spec exists but not implemented; event bus superseded by ADR-0003. +- **Phase 1 – Echo Core MVP** 🗺️ Planned: Entity/component storage, system scheduler, MaterializationBus integration. +- **Phase 2 – Adapter Foundations** 🗺️ Planned *(Milestone: "Double-Jump")*: Renderer adapter, input, physics stub. +- **Phase 3 – Advanced Adapters** 🗺️ Planned: Physics engines, WebGPU, audio, telemetry. +- **Phase 4 – Tooling & Polishing** 🗺️ Planned: Inspector, hot-reload, documentation site. +- **Ongoing**: Benchmark suite, community feedback loop, incremental releases. ## Open Questions - What minimum target hardware do we optimize for (mobile, desktop, consoles)? diff --git a/docs/book/echo/sections/00-onboarding-roadmap.tex b/docs/book/echo/sections/00-onboarding-roadmap.tex index 66c06c1c..8cd2bef0 100644 --- a/docs/book/echo/sections/00-onboarding-roadmap.tex +++ b/docs/book/echo/sections/00-onboarding-roadmap.tex @@ -23,14 +23,13 @@ \section{Read This First} \item \textbf{Orientation}: \texttt{architecture-outline.md} (why Echo exists). \item \textbf{Determinism contract}: skim \texttt{determinism-invariants.md} for guarantees. \item \textbf{Branching model}: \texttt{branch-merge-playbook.md} (merge-only; no rebases/force pushes). - \item \textbf{Work map}: \texttt{execution-plan.md} to see current intent and milestones. + \item \textbf{Work map}: GitHub Issues and project board for current tasks. \end{enumerate} \section{Do One Hands-On Task} \begin{itemize} \item Run a focused test: \texttt{cargo test radix\_drain --package warp-core}. \item Explore a doc: open \texttt{docs/public/collision-dpo-tour.html} to see visuals. - \item Record what you learned in \texttt{decision-log.md} if you touch code/docs. \end{itemize} \section{Next Hour: Choose Your Track} diff --git a/docs/book/echo/sections/01-glossary.tex b/docs/book/echo/sections/01-glossary.tex index aafe30e3..827917ae 100644 --- a/docs/book/echo/sections/01-glossary.tex +++ b/docs/book/echo/sections/01-glossary.tex @@ -13,5 +13,5 @@ \chapter{Glossary} \item[Scheduler Kind] Configurable scheduler implementation (Radix default; Legacy/BTree fallback). \item[Temporal Bridge] Mechanism for retro/forward delivery without paradox; works with paradox guard. \item[Snapshot / Commit] Canonical state plus provenance (parents, digests) used for replay and verification. - \item[Docs Guard] Policy that code changes must update docs (execution plan + decision log). + \item[Docs Guard] Policy that code changes must update docs (execution plan). \end{description} diff --git a/docs/book/echo/sections/02-high-level-architecture.tex b/docs/book/echo/sections/02-high-level-architecture.tex index a86cd161..4cfae4e9 100644 --- a/docs/book/echo/sections/02-high-level-architecture.tex +++ b/docs/book/echo/sections/02-high-level-architecture.tex @@ -64,7 +64,7 @@ \section{Domain Layers} \item \textbf{Kairos:} The branch identifier (Possibility). \item \textbf{Aion:} The narrative weight/entropy (Significance). \end{itemize} - \item \textbf{Codex's Baby (Event Bus):} A deterministic event bus used for communication between systems and for bridging the gap between the Core and the Ports. + \item \textbf{MaterializationBus:} A deterministic event bus used for communication between systems and for bridging the gap between the Core and the Ports. See ADR-0003 (MaterializationBus). \end{enumerate} \section{Ports \& Adapters} diff --git a/docs/book/echo/sections/05-game-loop.tex b/docs/book/echo/sections/05-game-loop.tex index 0e47580c..d38f2c01 100644 --- a/docs/book/echo/sections/05-game-loop.tex +++ b/docs/book/echo/sections/05-game-loop.tex @@ -34,7 +34,7 @@ \section{The Loop Phases} \item \textbf{Pre-Update:} \begin{itemize} \item Inputs from the `InputPort` are polled and injected into the `InputComponent`. - \item `Codex's Baby` (Event Bus) flushes pending external events. + \item The \texttt{MaterializationBus} flushes pending external events. \end{itemize} \item \textbf{Update (The Core):} \begin{itemize} diff --git a/docs/book/echo/sections/90-ops-and-governance.tex b/docs/book/echo/sections/90-ops-and-governance.tex index f1663929..727d5c60 100644 --- a/docs/book/echo/sections/90-ops-and-governance.tex +++ b/docs/book/echo/sections/90-ops-and-governance.tex @@ -8,7 +8,7 @@ \chapter{Ops and Governance (Starter)} \section{Working Agreements} \begin{itemize} \item No rebases or force pushes; use the branch merge playbook. - \item Keep \texttt{execution-plan.md} and \texttt{decision-log.md} in sync with any runtime change. + \item Update relevant ADRs and specs when runtime behavior changes. \item Run \texttt{cargo clippy --all-targets} and \texttt{cargo test} before PRs; Docs Guard expects updated metadata. \end{itemize} diff --git a/docs/capability-ownership-matrix.md b/docs/capability-ownership-matrix.md index c86bd9c8..bfa191d8 100644 --- a/docs/capability-ownership-matrix.md +++ b/docs/capability-ownership-matrix.md @@ -26,7 +26,7 @@ Use these columns consistently: - **Kernel**: deterministic semantic core (rewrite engine, scheduler, receipts, snapshot/tick structure, deterministic decision records including stream admission decisions). HistoryTime-only. - **Views**: controlled accessors and projections over history (query APIs, inspectors, adapters). Any interaction with HostTime/IO must be recorded as replay-safe claims/decisions. - **Tooling**: UIs, dashboards, CLI workflows (read-only by default; must be usable during pause/rewind; any control surface must be capability-gated and recorded). -- **Docs**: specs, decision log, procedures; the “human-facing API”. +- **Docs**: specs and procedures; the "human-facing API". --- @@ -81,7 +81,7 @@ Legend (compact): | **Scheduling** | owns · beta · best · prov: basic · deps: OS scheduler, tokio | owns · beta · det · prov: strong · deps: none | consumes · beta · det · prov: strong · deps: none | consumes · beta · best · prov: basic · deps: browser/event-loop | owns · stable · det · prov: strong · deps: none | | **Provenance** | consumes · beta · best · prov: basic · deps: FS/network | owns · beta · det · prov: strong · deps: CID/hash | consumes · beta · det · prov: strong · deps: none | consumes · beta · det · prov: strong · deps: none | owns · stable · det · prov: strong · deps: git | | **Schema / Interfaces** | consumes · exp · best · prov: basic · deps: serde/json | owns · exp · det · prov: strong · deps: versioned schemas | owns · exp · det · prov: strong · deps: schema hash pinning | consumes · exp · best · prov: basic · deps: UI contracts | owns · beta · det · prov: strong · deps: docs/specs | -| **Storage / Ledger** | owns · beta · best · prov: basic · deps: FS/DB | owns · exp · det · prov: strong · deps: content hashing | consumes · beta · det · prov: strong · deps: read-only ledger | consumes · beta · best · prov: basic · deps: localStorage/IndexedDB | owns · beta · det · prov: strong · deps: docs/decision-log | +| **Storage / Ledger** | owns · beta · best · prov: basic · deps: FS/DB | owns · exp · det · prov: strong · deps: content hashing | consumes · beta · det · prov: strong · deps: read-only ledger | consumes · beta · best · prov: basic · deps: localStorage/IndexedDB | owns · beta · det · prov: strong · deps: docs/specs | | **Time / Clocks** | owns · beta · best · prov: basic · deps: HostTime | consumes · beta · det · prov: strong · deps: Decision Records | consumes · beta · det · prov: strong · deps: Clock View | consumes · beta · best · prov: basic · deps: tool clock | owns · beta · det · prov: strong · deps: paper/spec | | **Networking / IO** | owns · beta · best · prov: basic · deps: TCP/WS/UDS | consumes · exp · det · prov: strong · deps: recorded claims | consumes · beta · det · prov: strong · deps: stream backlog | consumes · beta · best · prov: basic · deps: Web APIs | owns · beta · det · prov: strong · deps: procedures | | **Auth / Trust** | owns · exp · best · prov: basic · deps: keys/tokens | consumes · exp · det · prov: strong · deps: signed claims | consumes · exp · det · prov: strong · deps: receipts | consumes · exp · best · prov: basic · deps: auth UI | owns · exp · det · prov: strong · deps: policies | diff --git a/docs/code-map.md b/docs/code-map.md index bf859d67..b180099f 100644 --- a/docs/code-map.md +++ b/docs/code-map.md @@ -37,7 +37,7 @@ - ECS storage (future) — docs/spec-ecs-storage.md → new `ecs/*` modules (TBD) - Serialization — docs/spec-serialization-protocol.md → `snapshot.rs` (hashing), future codecs - Deterministic math — docs/SPEC_DETERMINISTIC_MATH.md, docs/math-validation-plan.md → `math/*` -- Temporal bridge/Codex’s Baby — docs/spec-temporal-bridge.md, docs/spec-codex-baby.md → future modules (TBD) +- Temporal bridge — docs/spec-temporal-bridge.md → future modules (TBD) ## Conventions diff --git a/docs/codex-implementation-checklist.md b/docs/codex-implementation-checklist.md deleted file mode 100644 index a64aa812..00000000 --- a/docs/codex-implementation-checklist.md +++ /dev/null @@ -1,63 +0,0 @@ - - -# Codex's Baby Implementation Checklist - -A step-by-step guide for turning the event bus spec into working code. - ---- - -## 1. Core Data Structures -- [ ] Define `CommandEnvelope` type (generics, metadata defaults). -- [ ] Implement `CommandQueue` ring buffer with growable capacity. -- [ ] Create handler registry (`Map>`). -- [ ] Add metrics struct (counters, gauges placeholders). - -## 2. Initialization -- [ ] Accept configuration (queue capacities, backpressure policy, instrumentation options). -- [ ] Instantiate queues for each scheduler phase. -- [ ] Wire instrumentation hooks (no-op stubs when disabled). - -## 3. Enqueue Path -- [ ] Implement `enqueue(phase, envelope)` with capacity checks and growth. -- [ ] Update metrics + tracing ring buffer. -- [ ] Handle immediate channel (`enqueueImmediate`). - -## 4. Flush & Dispatch -- [ ] `flushPhase` iterating queue FIFO order. -- [ ] Dispatch to handlers with deterministic sorting (priority desc, registration order). -- [ ] Support once-handlers (auto-unregister). -- [ ] Record handler duration (dev mode only). - -## 5. Handler Registration API -- [ ] Public `registerHandler` / `unregisterHandler` functions. -- [ ] Validate phase alignment and duplicate detection. -- [ ] Provide scoped registration helper for systems (auto unregister on system remove). - -## 6. Inter-Branch Bridge -- [ ] Implement bridge buffer keyed by `(branchId, chronos)`. -- [ ] Validate chronology; spawn retro branch via callback. -- [ ] Delivery integration in `timeline_flush`. -- [ ] Track entropy/paradox flags. - -## 7. Instrumentation -- [ ] Metrics update points (enqueued, dispatched, dropped). -- [ ] Backpressure alerts (threshold checks). -- [ ] Trace ring buffer with configurable capacity / payload capture. -- [ ] Dev hooks (`onEnqueue`, `onDispatch`). - -## 8. Testing -- [ ] Unit tests for queue wraparound & growth. -- [ ] Handler ordering determinism tests. -- [ ] Backpressure behavior (throw/drop). -- [ ] Bridge delivery test (cross-branch message). -- [ ] Instrumentation toggle tests (metrics increments). - -## 9. Integration -- [ ] Wire into scheduler phases (`pre_update`, `update`, etc.). -- [ ] Expose API on `EchoEngine` (context injection into systems/handlers). -- [ ] Document usage in developer guide. - -## 10. Follow-up -- [ ] Add inspector panel to display metrics. -- [ ] Extend `docs/decision-log.md` with a bus-event template (optional). -- [ ] Profile throughput with scheduler benchmarks. diff --git a/docs/codex-instrumentation.md b/docs/codex-instrumentation.md deleted file mode 100644 index 9e13ff10..00000000 --- a/docs/codex-instrumentation.md +++ /dev/null @@ -1,93 +0,0 @@ - - -# Codex’s Baby Instrumentation Plan - -This document defines the telemetry, logging, and debugging hooks for Codex’s Baby. Instrumentation must be deterministic-friendly and cheap enough for production builds, with richer introspection available in development. - ---- - -## Metrics - -### Counters (per phase) -- `enqueued[phase]` – total envelopes enqueued per tick. -- `dispatched[phase]` – envelopes delivered to handlers. -- `dropped[phase]` – envelopes dropped due to backpressure. -- `flushDuration[phase]` – cumulative time spent flushing the queue (ns). -- `handlerDuration[kind]` – total & average execution time per command kind. -- `bridgeDeliveries` – number of cross-branch messages delivered. -- `paradoxFlags` – count of envelopes flagged by paradox guard. - -All counters reset each tick but accumulated into rolling averages for inspector display (e.g., exponentially weighted moving average). - -### Gauges -- Queue high-water mark (max size reached). -- Current queue size per phase (for HUD display). -- Backpressure severity level (0 = normal, 1 = warning, 2 = critical). - ---- - -## Tracing - -### Event Trace Buffer -- Ring buffer storing up to N envelopes (configurable, default 256). -- Each entry logs: - - `timestamp` (Chronos tick + frame-relative index) - - `phase` - - `kind` - - `kairos` - - `metadata` subset (filtered) - - Optional handler outcome (success, dropped, error) -- Buffer can be sampled by inspector or exported for offline debugging. - -### Debug Hooks -- `codex.onEnqueue(fn)` – receives envelope metadata (without payload unless flag set). -- `codex.onDispatch(fn)` – after handler returns, receives duration + result. -- Hooks only active in dev mode; no-op in production. - ---- - -## Backpressure Alerts -- When queue exceeds 80% capacity, emit warning event (once per tick) to instrumentation system. -- At 95%, escalate to error, optionally trigger gameplay callbacks (e.g., slow down spawn rate). -- Provide optional “dropOldest” logging with summary of dropped kinds. - ---- - -## UI Integration -- Timeline inspector panel: - - Graph of enqueued vs dispatched per phase. - - Table of top command kinds by handler duration. - - Indicator for cross-branch traffic (counts, entropy contribution). -- HUD overlay (optional) showing real-time queue occupancy. - ---- - -## Configuration -```ts -interface CodexInstrumentationOptions { - readonly traceCapacity?: number; - readonly capturePayloads?: boolean; // defaults false, enables deep logging - readonly enableDevAssertions?: boolean; - readonly highWaterThreshold?: number; // default 0.8 - readonly criticalThreshold?: number; // default 0.95 -} -``` -- Engine options pass these into Codex’s Baby on initialization. -- Capture payloads only in secured dev builds to avoid leaking sensitive info. - ---- - -## Determinism Safeguards -- Timestamps use deterministic sequence numbers, not wall-clock time. -- Traces stored per branch; merging traces should maintain order by Chronos/Kairos. -- All instrumentation writes go through dedicated buffer to avoid interfering with queue order. -- No random sampling; use deterministic sampling intervals (e.g., log every Nth envelope). - ---- - -## Tasks -- [ ] Implement metrics struct and per-phase counters. -- [ ] Add `onEnqueue` / `onDispatch` hooks (dev only). -- [ ] Build ring buffer trace with configurable capacity. -- [ ] Expose metrics via inspector API. -- [ ] Add tests covering counter increments and backpressure alerts. diff --git a/docs/decision-log.md b/docs/decision-log.md deleted file mode 100644 index 76526301..00000000 --- a/docs/decision-log.md +++ /dev/null @@ -1,446 +0,0 @@ - - -# Decision Log - -*Demo outcomes should prefix the Decision column with `Demo N — …` to keep entries searchable.* - -| Date | Context | Decision | Rationale | Consequence | -| ---- | ------- | -------- | --------- | ----------- | -| 2026-01-09 | Append-only guardrails for onboarding docs | Append-only files now use `merge=union` plus CI guard (`Append-only Guard` running `scripts/check-append-only.js`; see `docs/append-only-invariants.md`). | Union merges alone can resurrect deletions; pairing union with CI keeps history intact. | CI blocks non-append edits; policy is referenced in AGENTS/plan/log/docs. | -| 2026-01-09 | Tasks DAG automation consolidation | Tasks DAG outputs live under `docs/assets/dags/`; generator runs in the refresh workflow and docs point to `docs/dependency-dags.md`. | Consolidation removes `dags-2` split and keeps automation visible. | DAG refresh produces canonical tasks DAG alongside issue/milestone DAGs. | -| 2026-01-03 | Planning hygiene + M2.1 kickoff | Refresh `docs/execution-plan.md` to reflect current GitHub state and begin issue #206 (DPO concurrency litmus: spec note + tests). | The execution plan had drifted (closed issues and PR-queue work still marked as active), which wastes triage time. Issue #206 is a leverage point: it turns DPO concurrency claims into pinned, executable cases. | The active frontier is now issue-driven again, and #206 work proceeds as a single-purpose branch with deterministic litmus tests + a small spec note. | -| 2026-01-03 | CI: CodeRabbit merge gate | Remove the `PR Merge Gate / CodeRabbit approval required` workflow and treat CodeRabbit approval as a procedural merge requirement instead of a racing status check. | The gate job commonly runs before CodeRabbit can submit a review, producing expected failures and stale red checks; it adds noise without increasing correctness. | PR checks are less noisy; CodeRabbit approval remains required by policy/procedure rather than a flaky “must-pass” status. (Tracking: GitHub issue #248.) | -| 2026-01-02 | Docs tooling: VitePress upgrade | Upgrade the pinned docs site generator from `vitepress@0.1.1` to a modern stable VitePress release, and keep VitePress pages “plain Markdown” (avoid YAML frontmatter) because SPDX HTML comment headers must be first in-file. | The current VitePress pin is too old to run on modern Node (e.g., Node 25 breaks old `fs.rmdir({ recursive: true })` usage). VitePress frontmatter parsing requires `---` at the start of the file, but repo policy requires SPDX headers first. | `pnpm docs:build` and `pnpm docs:dev` become reliable for contributors on current Node toolchains; angle-bracket / generic-type syntax is no longer misinterpreted as HTML; dead-link checks catch broken internal references; docs pages don’t leak frontmatter into rendered output. | -| 2026-01-02 | Docs: scheduler benchmarks drift | Consolidate the implemented `warp-core` scheduler benchmarks into a canonical performance doc (`docs/scheduler-performance-warp-core.md`), keep planned system-scheduler benchmark scenarios in `docs/spec-scheduler.md`, and turn `docs/scheduler-benchmarks.md` into a lightweight redirect. | Bench docs tend to rot when they mix “planned” and “implemented” benchmarks. A canonical, implementation-focused doc keeps links to real bench files accurate and reviewable. | Contributors have a single place to find and run warp-core scheduler benchmarks, while older links remain stable and planned scenarios remain discoverable. | -| 2026-01-02 | Docs: scheduler confusion | Introduce a scheduler landing doc (`docs/scheduler.md`) that distinguishes the implemented `warp-core` rewrite scheduler (`reserve()`/drain) from the planned Echo ECS/system scheduler, and consolidate `reserve()` evidence into a single canonical doc (`docs/scheduler-warp-core.md`) with redirects for older links. | Multiple docs used “scheduler” to mean different mechanisms (rewrite scheduling vs ECS/system ordering), which causes onboarding and review confusion. Consolidating `reserve()` documentation reduces drift and keeps evidence/claims reviewable in one place. | Contributors can quickly pick the correct scheduler document; future changes to either scheduler have canonical entrypoints for documentation updates, and older links remain stable. | -| 2026-01-02 | Contributor workflow docs | Add a `docs/workflows.md` “official workflow index” and link it from `README.md` and `AGENTS.md` so contributors can discover repo policy and blessed tooling entrypoints. | Echo’s workflow rules (docs guard, no-rebase/no-force, issue linkage, etc.) are critical but easy to miss when scattered. A single index improves onboarding and reduces accidental process drift. | Contributors have a clear starting point for “how we work” and how to run the official scripts/automation. | -| 2026-01-02 | Automation: DAG refresh | Add a scheduled GitHub Action that refreshes dependency DAG artifacts and opens a PR only when `docs/assets/dags/*.dot` or `*.svg` change. Use an omitted snapshot label to avoid “date churn” diffs. | Planning artifacts decay without upkeep. Automating refreshes keeps the diagrams credible while preserving human review (PR gate) and minimizing noise. | The repo periodically proposes small PRs updating DAG outputs when issue/milestone metadata changes, keeping `docs/dependency-dags.md` visuals current. | -| 2026-01-02 | Repo tooling: `cargo xtask` entrypoint | Add an `xtask` crate + `cargo xtask` alias that wraps the dependency DAG generator so contributors can run `cargo xtask dags` / `cargo xtask dags --fetch` consistently. | Repo scripts are useful but discoverability suffers. `xtask` provides a single, Rust-native entrypoint that can later host additional automation (demo runners, release prep, wizard flows) without proliferating ad-hoc commands. | Regenerating dependency graphs becomes a one-liner; future repo automation can be consolidated under `cargo xtask`. | -| 2026-01-02 | Planning tooling: DAG regeneration | Add a repo script (`scripts/generate-dependency-dags.js`) and a config file (`docs/assets/dags/deps-config.json`) to regenerate the issue/milestone dependency DAGs from cached or freshly fetched (`gh`) snapshots. | Diagrams rot if regeneration is manual. A deterministic generator makes it cheap to refresh labels/URLs from GitHub and evolve edge hypotheses as the board changes. | The graphs can be refreshed with a single command (`node scripts/generate-dependency-dags.js --fetch --render --snapshot-label none`) without hand-editing DOT. | -| 2026-01-02 | Planning: issue/milestone ordering | Add DOT+SVG dependency DAG sketches for selected GitHub Issues and Milestones, with confidence-styled edges (solid/dashed/dotted) and an explicit edge-direction convention. | When the backlog grows, sequencing becomes implicit and “obvious” dependencies rot in people’s heads. A lightweight, confidence-annotated DAG makes ordering debuggable without pretending the inferred edges are canonical truth. | The repo contains durable visual artifacts in `docs/assets/dags/` and a small explainer (`docs/dependency-dags.md`) that can be iterated as the Project board evolves. | -| 2026-01-02 | Docs hygiene: validation plan drift | Refresh `docs/math-validation-plan.md` to match the current Rust `warp-core` deterministic math surface and CI lanes, and add a dedicated `docs/docs-audit.md` memo to track purge/merge/splurge candidates. | Docs that describe “future JS tests” as if they exist mislead contributors and cause drift. Keeping validation docs grounded in real commands/tests makes determinism work repeatable and reviewable. | The math validation plan now reflects current reality (float lane + `det_fixed` lane, concrete test commands), and the repo has a durable place to track doc consolidation decisions. | -| 2026-01-02 | PR hygiene: ack strategy (reduce notification floods) | Adopt “one PR timeline comment per fix round” as the default ack mechanism: a human posts `✅ Addressed in commit ` plus an explicit `discussion_r` list covering the review-thread items addressed in that round. | Replying to every review thread is mechanically ackable but generates notification floods (especially with CodeRabbit-heavy PRs). A single per-round ack preserves a clean activity timeline, is easy to scan, and keeps ack bookkeeping deterministic (commit SHA + explicit thread ids). | Review rounds become visible as discrete timeline checkpoints; extractor can treat round-ack comments as acks; teams avoid bursts of GitHub alerts while still keeping stale-comment triage cheap. | -| 2026-01-02 | Docs hygiene: collision tour links | Move `docs/collision-dpo-tour.html` into VitePress static assets (`docs/public/collision-dpo-tour.html`) and add a dedicated `docs/spec-geom-collision.md` entrypoint so `/collision-dpo-tour.html` and its “Spec” link are build-visible and non-broken. | The collision tour is linked from docs navigation and should not rely on dead-link ignore rules or point at missing specs. | The docs site serves the tour consistently, and collision-spec links have a stable placeholder entrypoint until the full written spec is re-homed. | -| 2026-01-02 | Docs usability: “ELI5 spiral” on-ramp | Add an “Explain it like I’m not a programmer” spiral guide as the first step in the docs site’s onboarding flow. | A newcomer needs a low-jargon conceptual ramp before encountering terms like “graph rewrite”, “hashes”, or “two-plane law”. A spiral guide enables progressive disclosure and revisits concepts with increasing precision without forcing readers to jump straight into specs. | The docs site has a gentle entry point that orients non-programmers, then hands off to Start Here → WARP Primer → specs without breaking continuity. | -| 2026-01-02 | Docs usability: onboarding + navigation | Promote a guided docs entry path (`Start Here` + a curated Docs Map), and treat “hub” pages (e.g., scheduler) as first-class wayfinding to reduce “doc thunderstorm” navigation. | New readers were being dropped into an uncurated pile without any “what is Echo?” orientation or recommended reading paths. A small set of hub pages plus a curated map makes the docs navigable without requiring repo archaeology. | The VitePress site has an explicit onboarding flow; key pages avoid YAML frontmatter; navigation points to coherent “golden paths” instead of dumping readers into a raw inventory. | -| 2026-01-02 | PR hygiene: actionable review extraction | Include comment author attribution (human vs bot) and optionally include PR conversation comments + review summaries in actionable PR feedback reports so issues raised outside CodeRabbit review threads are surfaced and triaged. | Review loops often involve multiple voices and multiple comment surfaces; if the report only covers inline review threads (or only frames as “CodeRabbit”), human feedback and high-level review summaries can be missed. Attribution + optional wider extraction keeps triage deterministic and makes “who asked for what” explicit. | Reports can be triaged per reviewer/source; needs-attention counts are broken out by human vs bot and by source; full reports include URLs for quick thread navigation. | -| 2026-01-02 | PR hygiene: non-thread noise control | For non-diff sources (PR conversation comments and review summaries), treat bot-authored items as “Unclassified” (context only) rather than “Needs attention”; reserve “Needs attention” for human-authored items on those surfaces. | Bot review summaries/status comments are not reliably ackable (you can’t reply-inline the way you can with review threads), and they often duplicate information already present in the review-thread extraction. Classifying them as “Needs attention” creates persistent noise and hides the real, ackable work. | When `--all-sources` is enabled, the report still shows bot summaries for context, but the “Needs attention” list stays focused on actionable human feedback + review threads. | -| 2026-01-02 | PR hygiene: ack marker reliability | Treat “✅ Addressed in commit …” as acknowledged only when posted by a non-bot user and referencing a commit SHA that is part of the PR; for review threads, only count acks in replies (not inside the original review comment body). | Simple substring matching is unsafe because review bots can include the marker as a template/example in their own text. Commit validation prevents accidental matches and keeps the “ack” mechanism deterministic and trustworthy. | The extractor’s “Acknowledged” count can’t silently mask unresolved actionables; teams must explicitly ack each thread with a real commit reference to close the loop. | -| 2026-01-02 | Deterministic trig audit oracle (issue #177) | Replace the ignored trig “error budget” test’s platform-libm reference with a deterministic oracle using the pure-Rust `libm` crate, and pin explicit thresholds (max abs error vs f64 oracle + max ULP vs f32-rounded oracle for abs(ref) ≥ 0.25). | Cross-platform determinism requires that CI measurements never depend on host libc/libm behavior. Separating absolute-error budgeting (good near zero) from a gated ULP budget (meaningful away from zero) yields stable, interpretable guardrails for the LUT-backed trig backend. | The trig audit test is no longer `#[ignore]` and runs in CI; changes to the trig backend that worsen accuracy will fail fast with a concrete pinned budget. | -| 2026-01-02 | Demo tooling CI coverage (issue #215) | Add a Playwright CI job that runs the Session Dashboard smoke test and uploads the Playwright report + `test-results` artifacts (without mutating tracked docs screenshots). | The embedded dashboard is demo-critical and easy to regress; Rust-only CI can’t catch breaks in embedded HTML/CSS routing, runtime wiring, or browser rendering. Publishing artifacts makes failures actionable without requiring local repro. | PRs/pushes now gate on the dashboard smoke path; failures include an attached screenshot + report bundle for rapid debugging. | -| 2026-01-02 | Session gateway WS Origin policy (issue #214) | Keep `echo-session-ws-gateway`’s `--allow-origin` behavior strict: when an allowlist is configured, requests without an `Origin` header are rejected. Document the policy and add unit tests that lock the behavior. | Opting into `--allow-origin` should mean “require an explicit, allowlisted Origin”, not “best-effort filtering.” Strict behavior avoids accidental bypass if intermediaries strip headers and forces non-browser clients to be explicit. | Operators can rely on a simple rule: configure `--allow-origin` and require `Origin`; CI tests prevent accidental drift to permissive semantics. | -| 2026-01-02 | PR gating policy (issue #219) | Enforce “CodeRabbit must approve” via a dedicated status check that re-evaluates on both PR pushes and PR review events; require that check in the `main` ruleset as a separate GitHub UI / ops step (no bypass). | A social norm is easy to violate under time pressure (or via admin merge tooling). A deterministic status check makes the policy explicit, auditable, and automatically updated when CodeRabbit (`coderabbitai[bot]`) submits or dismisses reviews. | Once the `main` ruleset requires the status check, PRs to `main` are blocked until CodeRabbit has approved the current head commit; dismissing the bot’s review or pushing new commits re-blocks merges until re-approval. | -| 2026-01-02 | Project management: issue dependency workflow | Use GitHub’s native issue dependency graph for “blocked by” sequencing, and document the REST/CLI workflow (GraphQL has no direct dependency mutation; add/remove uses the blocking issue’s numeric `issue_id`). Seed a cross-repo AIΩN Project to track issues spanning Echo/Wesley/AIΩN. | Dependencies are higher-signal than “blocked-by” text and can be visualized across boards; documenting the correct API avoids thrash caused by `issue_number` vs `issue_id` confusion and keeps automation deterministic. A cross-repo Project is the simplest durable place to track shared work. | Maintainers can safely wire/inspect dependencies using a single documented procedure; cross-repo items can be viewed in one place without duplicating issues. | -| 2026-01-03 | TT0 TimeStreams spec: admission decision id derivation | Define `StreamAdmissionDecision.decision_id` as a derived, domain-separated digest over the canonical per-decision record bytes used in `admission_digest` (and clarify `worldline_ref` is pinned by snapshot ancestry). | The admission digest encoding must remain stable and non-circular. A derived id allows deterministic cross-references (facts/receipts/tools) without adding extra fields to the canonical digest encoding. Domain separation avoids confusion with other digest lanes. | Tools can compute and validate `decision_id` from history; `spec-time-streams-and-wormholes` and `spec-merkle-commit` stay consistent and replay-safe. | -| 2026-01-01 | PR hygiene: review-loop determinism | Standardize CodeRabbitAI review triage by pinning “actionable comments” extraction to the PR head commit, and automate it with a script that groups stale vs fresh feedback. | GitHub carries review comments forward, which makes it easy to waste time on already-fixed feedback. Treating comment extraction as a procedure (and generating a report) keeps review loops deterministic and prevents ambiguity across rounds. | PR fix batches start from a clean, reproducible actionables list; stale comments are verified against current code before any work is done; review iteration cost drops. | -| 2026-01-01 | Paper VI + roadmap hygiene (issue #180) | Add and maintain a Capability Ownership Matrix doc as a first-class artifact, alongside time-travel determinism notes, to keep ownership/determinism/provenance boundaries explicit. | As Echo grows, “who owns what” drifts silently; writing it down early prevents nondeterminism from leaking into the kernel and keeps tool/replay requirements grounded. | Future design and implementation work references the matrix for boundary decisions; specs and tooling tasks can be triaged against explicit determinism/provenance expectations. | -| 2026-01-01 | WVP demo hardening: review nits follow-up | Demo 1 — Tighten loopback/publish tests to be defensively correct (overflow-safe packet sizing and explicit non-empty graph assertion) in response to CodeRabbit review feedback. | Even test-only code should not encode obvious footguns (unchecked `len + 32`) or rely on undocumented invariants without asserting them. | The test suite remains robust under adversarial framing and continues to document WVP demo assumptions explicitly. | -| 2026-01-01 | WVP demo hardening (issue #169) | Demo 1 — Add loopback tests (service + viewer) that pin snapshot-first publish behavior, gapless epoch monotonicity, and forbidden/epoch-gap rejection. | The WVP “happy path” is demo-critical and easy to regress during refactors. Loopback tests provide fast feedback without requiring a real Unix socket listener or a browser/GUI harness. | `cargo test` now catches protocol regressions (authority, snapshot required, epoch gaps) and viewer publish-state bugs before they make the demo path flaky. | -| 2026-01-01 | Demo tooling: session dashboard hardening (PR #176 review) | Demo 1 — Harden the embedded session dashboard and its smoke tests (a11y drawer `inert`, metrics fetch timeout/guard, observer backoff, and “fail on no activity” e2e assertions). | Demo-grade tooling must degrade gracefully when upstream is unavailable and must be accessible by default; the Playwright smoke test should fail loudly on timeouts and provide useful diagnostics. | Dashboard no longer risks hanging indefinitely on `/api/metrics`; keyboard users can’t tab into a hidden drawer; hub observer reconnects with backoff on handshake/subscribe failures; e2e fails deterministically when metrics never reflect traffic. | -| 2026-01-01 | Demo tooling: keep the Session Dashboard smoke test self-contained | Demo 1 — Add a minimal `echo-session-client` example (`publish_pulse`) so Playwright can generate deterministic snapshot+diff traffic without depending on the full GUI viewer. | The dashboard smoke test should be runnable from a clean checkout. A tiny CLI publisher makes the test deterministic (gapless epochs) and removes hidden local state (stashes/untracked files) from the workflow. | The e2e dashboard test can build `publish_pulse` and use it as its traffic generator; future contributors can reproduce “hub + gateway + dashboard metrics” locally with a single command. | -| 2026-01-01 | Tooling UI: unify embedded dashboards without a web build | Demo 1 — Adopt Open Props v1.7.17 (vendored CSS tokens + theme switch) as the baseline styling system for embedded dashboards served by Rust binaries. | We want consistent spacing/typography/color decisions across tooling, but we can’t afford bundler/build friction for demo-grade dashboards. Token-based CSS gets most of the benefits of a component library while staying offline and easy to embed. | The session dashboard now uses Open Props tokens + theme vars; future embedded dashboards can share the same vendor CSS under `/vendor/*.css` without introducing a separate web build step. | -| 2025-12-31 | Demo tooling: keep demos honest in CI/local runs | Demo 1 — Add Playwright smoke tests: (1) `Session Dashboard` boots hub + gateway and asserts `/dashboard` + `/api/metrics` reflect observed WVP traffic; (2) fix the collision tour PiP `.hidden` behavior so the existing docs tests match reality. | Demo work tends to rot fastest; a black-box browser-level test catches regressions in routing, embedded assets, and observer wiring without requiring the full viewer. The collision tour failure was a CSS specificity mismatch (`.pip img` overriding `.hidden`). | Running `pnpm exec playwright test` validates both the docs tour interactions and the session dashboard/metrics flow; PiP world/graph tabs correctly hide/show images across browsers. | -| 2025-12-31 | Demo tooling: “look, Echo works” visibility for the WVP stack | Demo 1 — Serve a lightweight session dashboard from `echo-session-ws-gateway` (`/dashboard` + `/api/metrics`) and include a built-in hub observer (UDS subscriber) so it shows activity even when the gateway is not in the data path. | We need immediate, zero-setup observability while iterating on WVP framing and publish/subscribe semantics. Serving UI + JSON from the existing gateway avoids CORS/build complexity and keeps the demo entrypoint as “run a binary, open a page.” | Local browser dashboard can display hub observer status, connection counts, byte/frame rates, decode errors, and per-warp last epoch/hash progress; metrics remain explicitly non-authoritative (observed traffic only). | -| 2026-01-01 | Motion payload determinism (v2) | Promote the motion demo payload to a deterministic Q32.32 fixed-point v2 encoding (`payload/motion/v2`, 6×i64 LE) while preserving v0 decode compatibility (`payload/motion/v0`, 6×f32 LE). Port the motion executor to `Scalar` and upgrade v0 payloads to v2 on write. | Motion payload bytes participate in deterministic state hashing and cross-platform replay; raw `f32` bytes reintroduce NaN payload and subnormal drift risks. Upgrading v0-on-write keeps backwards compatibility while converging stores on a stable canonical encoding. | Motion rule remains usable with legacy fixtures, but resulting state always converges to the deterministic v2 payload after the first write; non-finite values are mapped deterministically (NaN→0; ±∞ saturate). | -| 2026-01-01 | Deterministic math: fixed-point lane (`DFix64`) | Introduce a deterministic fixed-point scalar (`DFix64`, Q32.32 in `i64`) behind a `det_fixed` feature flag, with saturating arithmetic and ties-to-even rounding for mul/div; add `warp-core` tests and CI lanes that run `cargo test -p warp-core --features det_fixed` (glibc + musl). | Fixed-point is a key alternative backend for determinism audits and “no-float” environments; keeping it feature-gated and continuously tested lets us evolve it without destabilizing the default `F32Scalar` lane. | The repo gains a continuously exercised fixed-point backend; CI will catch regressions in the fixed-point surface and cross-platform compilation early (including musl). | -| 2026-01-01 | Deterministic math guardrails | Forbid raw trig calls in `warp-core` math modules (outside the deterministic trig backend + scalar wrapper surface) via a dedicated CI/script guard. | Prevents regressions where platform/libm transcendentals sneak back into runtime math, breaking cross-platform determinism. | CI fails fast if `mat4`, `quat`, etc. reintroduce `.sin/.cos/.sin_cos` calls; trig stays centralized in `warp_core::math::trig` and surfaced through scalar wrappers. | -| 2026-01-01 | Deterministic math: transcendental stability (`F32Scalar`) | Implement LUT-backed deterministic `F32Scalar::{sin,cos,sin_cos}` via a checked-in quarter-wave table + linear interpolation; add golden-vector trig tests to pin exact output bits. | Removes platform/libm variance while preserving `F32Scalar` canonicalization invariants (no `-0.0`, no subnormals, canonical NaNs). The existing “error budget” test remains opt-in because its reference uses platform libm. | Trig is now deterministic across supported platforms; CI can enforce stable bits via golden vectors; developers can still audit approximation error by running ignored tests explicitly. | -| 2026-01-01 | PR #167 review cleanup (deterministic math) | Tighten determinism invariants surfaced by review: enforce exact odd symmetry for sine by range-reducing `abs(angle)` and applying sign at the end; centralize signed-zero canonicalization via `trig::canonicalize_zero`; remove magic endpoint bits in the LUT generator; add tests asserting the motion executor accepts v0 payloads and upgrades to v2; document deterministic `0/0 → 0` policy; add `// TODO(#177)` tracking for the ignored trig error-budget audit. | Code review found a 1‑ULP mismatch between `sin(±π/8)` golden vectors and flagged duplicated signed-zero handling / implicit invariants. Making symmetry and canonicalization explicit prevents bit drift and reduces the chance of reintroducing nondeterministic edge behavior. | Golden vectors for `sin(-x)` are bit-stable with `sin(x)`; canonicalization logic is centralized and reused by Mat4/Quat; motion v0 support is executable (not just documented); remaining work: convert the ignored audit test into a deterministic oracle once budgets are pinned (#177). | -| 2025-12-30 | PR #164 review follow-ups (WVP demo robustness; related: WVP demo path entry below) | Reset per-connection publish state on successful connect (force snapshot before diffs), harden handshake/outbound encoding failures (log + bail instead of writing empty packets), and add missing rustdoc/warn logs surfaced by review. | Review comments flagged a reconnect publish dead-end and “silent failure” risks; making failures visible and forcing snapshot-on-reconnect keeps the demo deterministic, debuggable, and aligned with missing-docs policy. | Publishing resumes after reconnect without manual toggles; protocol encode failures no longer silently write empty packets; workspace `-D warnings -D missing_docs` remains green. | -| 2025-12-30 | WARP View Protocol demo path (hub + 2 viewers) | Add a bidirectional tool connection (`connect_channels_for_bidir`) so tools can publish `warp_stream` frames, and wire `warp-viewer` with publish/receive toggles plus a deterministic demo mutation (“pulse”) that emits gapless snapshot+diff streams. Document the workflow in `docs/guide/wvp-demo.md` and mark WVP checklist items complete in `docs/tasks.md`. | WVP needs an end-to-end “real” demo (not just a spec): tools must be able to publish without dragging async runtimes into UI code, and publisher/subscriber roles must be testable locally. A deterministic, viewer-native mutation is the smallest way to exercise the protocol without coupling to the engine rewrite loop yet. | You can run a local hub and watch two viewers share WARP changes (publisher pulses, subscriber updates). Client and hub error surfaces are explicit via notifications/toasts; epoch gaps and forbidden publishes fail fast. Remaining follow-up: a dedicated integration test harness for multi-client loopback if/when we formalize resync/acks. | -| 2025-12-30 | Stage B1 usability/docs: make descent-chain law and portal-chain slicing concrete | Expand `docs/spec-warp-core.md` with worked Stage B1 examples (descent-chain reads → `Footprint.a_read` → patch `in_slots`, and portal-chain inclusion in Paper III slicing). Add `Engine::with_state(...) -> Result<Engine, EngineError>` so tools/tests can initialize an engine from an externally constructed multi-instance `WarpState` without exposing internal maps. | Stage B1 correctness depends on subtle invariants that are easy to “get almost right” without a concrete example; documentation must prevent “looks right, doesn’t slice.” A state-based engine constructor is the smallest ergonomic primitive that enables multi-instance fixtures and patch-replay workflows while keeping the rewrite hot path unchanged. | Contributors can reproduce and reason about B1 behavior quickly; multi-instance demos/tests can be built via patch replay + `Engine::with_state` rather than internal mutation; the portal-chain dependency is explicit in both docs and slice behavior. | -| 2025-12-30 | Review follow-ups: receipts + wasm/ffi ergonomics + GraphStore deletes | Keep tick receipts self-describing and instance-scoped: document `TickReceiptEntry.scope` as a `NodeKey` (`warp_id`, `local_id`) and include both components in the receipt digest encoding. Add `Engine::insert_node_with_attachment` for atomic demo/bootstrap insertion and use it from WASM/FFI boundaries. Remove `delete_node_cascade`’s inbound `O(total_edges)` scan by maintaining reverse inbound edge indexes (`edges_to` + `edge_to_index`). | Receipts are used as deterministic audit artifacts: scope must be unambiguous once WARP is multi-instance. WASM/FFI callers need deterministic failure behavior and acceptable debugging signals. Delete-heavy workloads should not accidentally hit `O(total_edges)` traps when the store already maintains reverse indexes. | Receipt docs and digest encoding are clearer and stable; WASM spawn logs errors behind `console-panic` and returns a clear sentinel; FFI spawn avoids partial init; node deletion now scales with the number of incident edges rather than total graph size. | -| 2025-12-30 | Post-merge touch-ups (Stage B1): API encapsulation + semantics | Add stable id byte accessors (`NodeId::as_bytes`, `WarpId::as_bytes`), add `GraphStore::has_edge`, and tighten `Engine` accessors so “unknown warp store” is distinguished from “missing node/attachment” (`Result<Option<_>, EngineError>`). Make root-store mutation helpers (`insert_node`, `set_node_attachment`) return `Result` (no silent mutation drops). | Prevent callers from depending on tuple-field internals (`.0`) and reduce coupling to internal map fields. The engine must be deterministic and explicit about failure modes; “drop writes in release” is a correctness hazard. Returning `Result` keeps clippy/CI gates strict without using panics/`expect`. | Call sites in wasm/ffi/benches/tests were updated to handle `Result`; patch/receipt hashing uses the stable accessors; docs now use explicit “node-attachment plane / edge-attachment plane” terminology. CI remains green under `-D warnings -D missing_docs`. | -| 2025-12-29 | Paper I/II two-plane semantics (SkeletonGraph + attachment plane) | Define Echo’s `warp-core` store as the **SkeletonGraph** (`π(U)`), and represent attachment-plane payloads as **typed atoms**: `AtomPayload { type_id: TypeId, bytes: Bytes }` stored on `NodeRecord`/`EdgeRecord`. Update canonical hashing and tick patch encoding so payload `type_id` participates in `state_root`/`patch_digest`, and codify project laws: “skeleton rewrites never decode attachments” + “no hidden edges in payload bytes.” | Echo must be a faithful reference implementation of WARP’s two-plane semantics: the skeleton plane is rewrite-visible and provenance/slicing-relevant, while attachments are separate. Untyped payload bytes create safety hazards and allow “same bytes, different meaning” collisions at the boundary. Keeping matching/indexing skeleton-only preserves performance and prevents provenance bugs caused by hidden structure. | Terminology is clarified (SkeletonGraph vs. WarpState); payload meaning is explicit and hash-committed; decode failure is deterministic and conservative (“rule does not apply” by default); Stage B1 descended attachments are unblocked via explicit indirection rather than nested structs or payload-smuggled edges. | -| 2025-12-29 | Paper III boundary semantics (delta patches + slicing) | Implement `WarpTickPatchV1` as a **delta patch** (canonical `ops` + conservative `in_slots`/`out_slots` over unversioned slots); define value-versioning *by interpretation* along a worldline (`ValueVersionId := (slot_id, tick_index)`), not embedded in the patch; upgrade commit hashing to **v2** so `commit_id` commits only to `(parents, state_root, patch_digest, policy_id, version)` and treats plan/decision/rewrites digests + receipts as diagnostics. | Paper III’s slice/holography results require a replayable boundary artifact; recipe patches bind replay to executor semantics and do not archive cleanly across languages or evolution. Unversioned slots keep patch hashes position-independent; SSA-style value versioning is recovered from patch position in `P`. Committing only to the patch delta prevents consensus from being coupled to scheduler/planner quirks. | Commit ids become “this transformation” (delta) rather than “this scheduling narrative”; slicing becomes trivial once `producer(slot@i)` is defined by patch position; planner/receipt metadata remains available for debugging without becoming consensus-critical. | -| 2025-12-29 | Paper II tick receipts: blocking causality | Extend `TickReceipt` to record a blocking-causality witness for `Rejected(FootprintConflict)` entries (indices of the applied candidates that blocked it, in canonical plan order); keep `decision_digest` encoding stable (digest commits only to accept/reject outcomes, not blocker metadata). | Paper II’s receipts are meant to explain *why* candidates were rejected and support deterministic debugging/provenance. Blocker indices provide a minimal poset edge list without destabilizing commit hashes as explanations evolve. | Tools/tests can surface “blocked by …” explanations; receipts now carry poset edges for footprint conflicts; commit ids remain stable for identical accept/reject outcomes. | -| 2025-12-29 | warp-core follow-ups (#151/#152) | Remove “TODO-vibes” by making `policy_id` an explicit `Engine` parameter (default `POLICY_ID_NO_POLICY_V0`) and add a reverse edge index (`EdgeId -> from`) so tick patch replay no longer scans all edge buckets for `UpsertEdge`/migration. | `policy_id` is part of the deterministic boundary and must be configurable, not a hardcoded magic number; patch replay must scale beyond toy graphs. Maintaining the reverse index keeps replay `O(bucket_edges)` and avoids history-sensitive state roots caused by empty edge buckets. | Callers can set policy semantics explicitly; tick patch replay is no longer `O(total_edges)` per migrated edge; state hashing remains a function of graph content, not mutation history. | -| 2025-12-29 | warp-core follow-up: edge record equality | Derive `PartialEq, Eq` for `EdgeRecord` and remove the local `edge_record_eq` helper from tick patch diffing, switching to idiomatic `==`/`!=`. | Helper-based equality duplicates type semantics and can drift. Deriving equality makes record comparisons precise, portable, and single-source-of-truth. | Diffing code is smaller and less fragile; future `EdgeRecord` field changes will naturally participate in equality comparisons. | -| 2025-12-29 | warp-core follow-up: tick patch hygiene | Harden `tick_patch` boundary code: remove double map lookups in `diff_store`, expand rustdoc to document invariants and semantics, use `thiserror` for `TickPatchError`, dedupe `ops` in `WarpTickPatchV1::new`, and introduce `ContentHash` (alias for `crate::ident::Hash`) to avoid confusion with `derive(Hash)`. | Tick patches are a consensus-critical boundary artifact: code should be clear, deterministic, and resistant to misuse. Removing boilerplate and ambiguity reduces drift risk and prevents subtle replay errors (duplicate ops, mismatched tags vs sort order). | Patch construction/diffing is clearer and cheaper; errors are less boilerplate; duplicate ops cannot cause replay failures; type naming is less confusing for readers. | -| 2025-12-28 | AIΩN bridge doc + Paper II tick receipts | Promote the AIΩN Foundations bridge from `docs/notes/` into a canonical doc (`docs/aion-papers-bridge.md`, keeping a stub for historical links); implement `TickReceipt` + `Engine::commit_with_receipt` in `warp-core` and commit the receipt digest via `Snapshot.decision_digest` (encoding defined in `docs/spec-merkle-commit.md`). | The bridge is a long-lived architectural dependency and should not be buried in dated notes; Paper II tick receipts are required for deterministic debugging and provenance. Reusing `decision_digest` commits to accepted/rejected outcomes without changing the commit header shape. | A stable bridge doc is indexed in `docs/docs-index.md`; tools/tests can obtain receipts from `commit_with_receipt`. Commit ids now incorporate receipt outcomes whenever the candidate set is non-empty (empty receipts still use the canonical empty digest). | -| 2025-12-28 | Brand asset variants | Add `assets/echo-white-radial.svg`: an `echo-white.svg`-derived logo variant with a shared radial gradient fill (`#F5E6AD` center → `#F13C77` edges) and an interior stroke emulated via clipped stroke paths for broad SVG renderer support. | Provide a ready-to-use, style-forward logo asset while keeping the original `echo-white.svg` unchanged; prefer a clip-path “inner stroke” technique over `stroke-alignment` to avoid uneven renderer support. | Consumers can pick the original flat-white logo or the gradient+inner-stroke variant without changing code; the new SVG renders consistently in common SVG engines (rsvg/InkScape). | -| 2025-12-28 | Repo tour + onboarding refresh | Replace the root README with a reality-aligned overview, link Echo to the AIΩN Framework + the Foundations series papers, and add durable tour/bridge notes; fix `spec-merkle-commit` empty-digest semantics to match `warp-core`. | Keep onboarding and determinism specs consistent with the implemented engine contracts and the motivating research lineage; reduce future churn caused by doc drift. | New `README.md`, `docs/notes/project-tour-2025-12-28.md`, and `docs/notes/aion-papers-bridge.md` provide a single entry path; the commit digest spec now matches code behavior. | -| 2025-12-28 | WARP alignment (rename) | Mechanically rename legacy RMG identifiers to WARP equivalents across the workspace (crates, types, protocol, docs): `rmg-*` → `warp-*`, `Rmg*` → `Warp*`, and update the book/specs accordingly; keep wire decode aliases for legacy `subscribe_rmg`/`rmg_stream` and `rmg_id` during transition. | Align the repo with the AIΩN Foundations Series terminology so paper readers can map concepts directly; reduce long-term “two vocabularies” drift while keeping a short compatibility window. | Workspace compiles + tests pass under WARP naming; docs now speak WARP-first; legacy clients/notes can still decode via explicit compatibility aliases. | -| 2025-12-28 | PR #141 follow-up (new CodeRabbit nits @ `4469b9e`) | Add `[workspace.package]` metadata and migrate `spec-000-rewrite` to inherit `license`, `repository`, and `rust-version`; update the rust-version guard to accept `rust-version.workspace = true` and avoid `sed` for output formatting. | Reduce duplication/drift across manifests and keep the guard compatible with Cargo workspace inheritance while staying bash-idiomatic. | Spec-000 rewrite no longer duplicates common package metadata (`e4e5c19`); the rust-version guard is more robust + test-backed (`7e84b16`). | -| 2025-12-28 | PR #141 follow-up (new CodeRabbit review @ `639235b`) | Re-affirm `"wasm"` as a valid crates.io category (verify via API) and restore it on WASM crates for discoverability; fix Spec-000 doc examples so they’re internally consistent (serializable `WarpGraph`, non-noop `Disconnect`); tighten CI guard semantics and harden the rust-version checker script with recursion + self-tests. | Avoid churn from contradictory review assertions by checking the source-of-truth; keep spec examples honest/compilable; ensure CI guards are robust and self-documenting. | WASM crates regain the canonical category (`84e63d3`); Spec-000 docs examples compile/behave as claimed (`922553f`); echo-session-client uses workspace deps (`dfa938a`); rust-version guard is future-proof + tested and `cargo-audit` self-tests live with dependency policy (`56a37f8`). | -| 2025-12-28 | PR #141 follow-up (new CodeRabbit review @ `b563359`) | Standardize the workspace MSRV to match the pinned toolchain (Rust 1.90.0) and enforce it via CI; de-duplicate security audit ignores by sourcing them from `deny.toml`; remove stale advisory ignores and simplify publish metadata to reduce churn. | Prevent “works on my toolchain” drift, keep policy single-sourced, and avoid future review loops caused by duplicated ignore lists or stale configuration. | CI now guards rust-version consistency; `scripts/run_cargo_audit.sh` can’t drift from `deny.toml`; publishing metadata avoids disputed categories; follow-ups landed in `0f8e95d`, `150415b`, `2ee0a07`, `3570069`, `3e5b52d`, `3ccaf47`, `1bf90d3`, `8db8ac6`, `82fce3f`, and `e5954e4`. | -| 2025-12-28 | PR #141 follow-up (SPDX + deny housekeeping) | Align SPDX enforcement with the repo license split by treating build/config files as code (Apache-2.0) and repairing headers accordingly; document required cargo-deny allowlist licenses (OpenSSL, Ubuntu-font-1.0) with explicit dependency justification; move warp-viewer local crate pins into `[workspace.dependencies]` for DRY version management. | Reduce licensing ambiguity and prevent drift between file headers and crate metadata; keep deny policy explicit and justified; centralize path+version pins to avoid repeated edits across crates. | SPDX CI now enforces the intended split; cargo-deny license policy remains green and understandable; viewer crate updates become a single root bump. Fixes in `042ec2b`, `5086881`, and `17687f2`. | -| 2025-12-28 | PR #141 follow-up (new CodeRabbit review @ `c8111ec`) | Harden CI and docs hygiene: pin `cargo-deny` GitHub Action to a verified commit SHA; remove duplicate `cargo-audit` job from `ci.yml` and centralize audit args in `scripts/run_cargo_audit.sh`; harden the task-list guard by making it case-insensitive, failing when no task lists exist, and adding self-tests. | Keep CI supply-chain deterministic and reduce drift; make task-list enforcement reliable (and test-backed) so docs can’t silently contradict themselves. | CI logs become clearer and more deterministic; task-list guard now detects case-only near-duplicates and has regression tests; security audit policy is single-sourced. Fixes in `602ba1e` (plus doc cleanup commit in this follow-up). | -| 2025-12-28 | PR #141 follow-up (new CodeRabbit round) | Fix Spec-000 rewrite history semantics by adding `Rewrite.subject` (field name) and storing actual prior field values in `old_value` for `Set`; bump Spec-000 Leptos dependency to `0.8.15` and adjust the CSR scaffold to match the new API. | Align docs and implementation so rewrite history supports replay/undo semantics; keep the living spec scaffold on maintained Leptos/WASM patch releases. | New regression test locks the semantics; Spec-000 builds on Leptos 0.8.15; fixes land in `1f36f77` and `1a0c870`. | -| 2025-12-28 | PR #141 follow-up (new review batch + CI repairs) | Address CodeRabbit’s follow-up round: extract constants, strengthen tests (including negative framing cases), tighten WASM ABI/bindings docs and no-op semantics, and add automated task list consistency checks (pre-commit + CI). Repair CI policy failures by allowing required transitive licenses (OpenSSL, Ubuntu-font) and version-pinning workspace path deps to satisfy cargo-deny wildcard bans; ignore RUSTSEC-2024-0370 until upstream replaces `proc-macro-error`. | Keep boundaries deterministic and panic-free while preventing regression churn; ensure CI remains a reliable gate even when upstream marks dependencies as “unmaintained” without a drop-in replacement. | Follow-up review comments map cleanly to fixing SHAs; `cargo test`/clippy gates stay green; CI audit/deny jobs pass and task lists can’t drift into contradictory “checked + unchecked” duplicates. | -| 2025-12-28 | PR #141 CodeRabbit burn-down (Spec-000 scaffold) | Resolve all actionable PR review items: align living-spec methodology/tasks docs with current repo reality; add tests for session-client error scoping; harden WS gateway framing/teardown and upgrade to axum 0.8; declare MSRV for edition-2024 WASM crates. | Prevent review churn and regressions by moving expectations into tests/docs; ensure the WS gateway fails fast and is debuggable; make toolchain requirements explicit for 2024-edition crates. | Workspace is green (`cargo test`, clippy w/ `-D missing_docs`); review comments can be cleared with referenced SHAs; living-spec docs now distinguish implemented scaffolding from planned certification. | -| 2025-12-13 | WS gateway lifecycle + Spec-000 WASM | Gate Spec-000’s `wasm_bindgen` entrypoint behind `cfg(all(feature = "wasm", target_arch = "wasm32"))`; on the WS gateway, treat upstream (UDS) disconnect as terminal by sending a best-effort Close frame and cancelling the ping loop + sibling tasks. | Keeps default host builds green while still supporting Trunk/wasm32; prevents hanging `join!`/leaked ping tasks that keep browser connections alive when the backend is gone. | Workspace builds no longer trip wasm-only entrypoints; browser clients get a clean disconnect on upstream failure instead of a zombie connection receiving only pings. | -| 2025-12-11 | Browser WS gateway | Added `echo-session-ws-gateway`: axum-based WebSocket→Unix bridge with optional rustls TLS, origin allowlist, 8 MiB frame guard, and JS-ABI frame parsing to keep gapless WARP framing intact for browser clients. | Browsers need WS; TCP proxies can’t handle WS framing. A thin Rust gateway keeps protocol identical while enforcing size/origin limits and retaining Unix-socket hub. | Browser clients can subscribe/publish WARP streams over WS; no protocol change to hub; TLS/origin filters available for internet exposure. | -| 2025-12-11 | Scripting runtime pivot | Replace the prior scripting embedding with Rhai as the deterministic scripting layer; update docs/roadmaps/backlogs accordingly. warp-ffi stays for host/native integrations; scripting embeds Rhai directly. | Rhai runs in-process, is Rust-native, and gives tighter control over deterministic sandboxes without C FFI complexity; aligns with deterministic host modules and reduces attack surface. | All design documents, plans, and demo targets now refer to Rhai; future scripting work focuses on Rhai modules and hot-reload, not legacy bindings. | -| 2025-12-11 | WARP authority enforcement | Session-service now pins a single producer per WarpId, rejects other publishers with structured errors (`E_FORBIDDEN_PUBLISH`, `E_WARP_SNAPSHOT_REQUIRED`, `E_WARP_EPOCH_GAP`), and preserves snapshot/diff gap checks; session-client surfaces Error frames as local error notifications. | Prevents competing writers from corrupting WARP streams and makes protocol violations visible to tools instead of silent drops or disconnects. | Unauthorized publishes fail fast without fanout; clients see explicit error notifications; sets the stage for dirty-loop/resync work. | -| 2025-12-10 | CI cargo-audit warnings | Keep `cargo audit --deny warnings` green by ignoring RUSTSEC-2024-0436 (paste unmaintained via wgpu/metal) and RUSTSEC-2021-0127 (serde_cbor legacy advisory) in both `security-audit.yml` and `ci.yml`. | Paste is an unmaintained transitive dep of wgpu with no downstream replacement yet; serde_cbor was removed from the codebase but may reappear through cached lockfiles—ignoring prevents CI flakiness while we track upstream updates. | Security audit jobs now ignore these advisories in both workflows; advisories remain documented and will surface again once upstreams ship replacements or if serde_cbor re-enters the tree. | -| 2025-12-10 | CI cargo-deny index warnings | Prime crates.io index with `cargo fetch --locked` in the `deny` job before running `cargo-deny` (ci.yml). | `cargo deny` was emitting `warning[index-failure]` when it tried to query yanked status without a local index; warming the index prevents network flakiness from spamming logs and fails early if fetch cannot reach crates.io. | CI is quieter and deterministically fails on real connectivity issues; yank checks now use the warmed cache instead of best-effort network lookups during the deny step. | -| 2025-12-10 | WARP View Protocol plan | Added `docs/tasks.md` with a checklist to deliver the WARP View Protocol (pub/sub authority, dirty-loop publishing, demo path, tests) and logged intent in execution-plan. | Centralizes the work needed to demo multi-viewer shared WARPs and sets commit-by-slice expectations. | Provides a single progress list; future slices will check off items and keep docs aligned. | -| 2025-12-10 | CBOR migration + viewer gating | Replaced archived `serde_cbor` with `ciborium` in proto/graph (canonical encoder/decoder with serde_value bridge), marked graph enums `#[non_exhaustive]`, made canonical hashing return `Result`, and aligned egui deps/input handling (pointer & WASDQE gated to View, escape only on keydown, snapshot hash mismatch desyncs). | Keeps wire format on a maintained crate, avoids namespace pollution, and prevents silent desyncs or UI mutation outside the View screen; removes raw pointer aliasing in app_events. | Canonical framing stays deterministic on supported MSRV, oversized ints surface errors, viewer input is safer/debuggable, and CI dependency alignment avoids egui patch skew. | -| 2025-12-10 | Viewer timing + session buffering | Capture frame `dt` once per frame and reuse for camera/layout/arcball; compute angular velocity using `angle/dt` with epsilon and zero-angle guard; session client now buffers header/payload/checksum across reads, decodes only when a full packet is present, and never drops partial data. | Prior code reset `last_frame` before `elapsed()` uses, producing zero dt and runaway angular velocities; arcball used a bogus constant divisor; poll_message dropped partial headers and over-allocated payloads. | Viewer motion/decay uses correct per-frame delta and stable spin; angular velocity matches actual drag speed; session client keeps stream in sync and surfaces `Ok(None)` only when truly no data. | -| 2025-12-10 | Config + docs alignment | README points to `ConfigStore` and correct doc path; proto reexports are explicit with serde-renamed `AckStatus::{Ok,Error}`; constellation figure labeled/cross-referenced with anchored legend. | Aligns docs with actual APIs and keeps figure references stable. | Less namespace pollution, accurate docs, and reliable LaTeX figure placement. | -| 2025-12-10 | Session client framing/polling | poll_message is now non-blocking, buffered across calls, enforces an 8 MiB max payload with checked arithmetic, and respects header+payload+checksum sizing; poll_notifications drains buffered notifications only. Added partial-header test earlier. | Previous blocking loop could spin forever and trust unbounded lengths; partial reads risked desync and OOM. | Session client won’t block while idle, rejects oversized frames safely, and keeps framing consistent across partial reads. | -| 2025-12-10 | Viewer timing & safety | Frame dt captured once and reused for motion/decay; angular velocity uses angle/dt with epsilon; viewport access no longer unwraps; helper window lifetimes relaxed; single aspect computation. | Prevents zero-dt spins, stabilizes arcball momentum, and avoids panics when no viewport exists. | Viewer motion is stable and safe even if viewports haven’t been created yet. | -| 2025-12-06 | Tool crate docs | Added a crate map for tool-related crates in the Echo Tools booklet and gave each tool crate (echo-app-core, echo-config-fs, echo-session-proto, echo-session-service, echo-session-client, warp-viewer) a local README with a “What this crate does” + “Documentation” section pointing back to the booklets/ADR/ARCH specs. | Keeps the booklets as the canonical source of truth while giving each crate a concise, local summary of its role and where to find the full design docs. | New contributors can navigate from any tool crate directly to the relevant sections of the Echo book and ADR/ARCH documents; Cargo `readme` fields now point at crate-local READMEs instead of the repo root. | -| 2025-12-06 | JS-ABI + WARP docs | Aligned Echo’s LaTeX booklets with JS-ABI v1.0 (deterministic encoding + framing) and the new WARP streaming stack by adding Core sections for the wire protocol and generic WARP consumer contract, plus cross-links from Session Service and WARP Viewer. | Keeps the implementation (canonical CBOR encoder, packet framing, gapless WARP streams) in lockstep with durable docs; avoids re-specifying the same contract in multiple places. | Core booklet now explains JS-ABI framing and the per-WarpId snapshot/diff consumer algorithm (with a role summary table); Tools booklet’s Session Service and WARP Viewer sections link back to that contract instead of drifting copies. | -| 2025-12-04 | WARP streaming stack | Landed shared `echo-graph` crate (canonical RenderGraph + `WarpFrame`/`WarpOp`/Snapshot/Diff) and rewired proto/viewer to use gapless structural diffs with hash checks. | Avoids viewer-owned graph types and keeps the wire contract deterministic; lets tools share one graph representation. | Viewer now enforces no-gap epoch sequence and hashes when applying diffs; remaining work is engine emitter + extracting IO into adapters. | -| 2025-12-04 | Session hub IO | Moved socket/CBOR IO into `echo-session-client` helpers and wired `warp-viewer` to consume frames/notifications from the client; session service now emits snapshot + gapless diffs (stub engine). | Restores hex boundaries: viewer is render-only; hub owns transport; provides a live gapless WARP stream for development. | Roadmap P1 items for client wiring and engine emitter are checked; next up: real engine integration and better scene mapping from payloads. | -| 2025-12-03 | `warp-viewer/ROADMAP` | Marked all P0 items as completed after code review (arrow offsets, HUD VSync toggle, controls overlay, watermark, MSAA). | Features are implemented in `src/main.rs` and shaders (VSync toggle, help overlay, watermark image, arrowhead offset via `head`, MSAA sample_count=4). | Roadmap now reflects shipped P0 scope; next focus shifts to P1 items (persistence, perf overlay, tunable arrowheads, camera auto-fit). | -| 2025-12-03 | `warp-viewer` config | Added JSON config persisted under OS config dir; camera pose + HUD toggles + vsync/watermark now survive restarts. | Serialize viewer state with serde; load on startup, save on close; uses `directories` for platform paths. | Roadmap P1 “persist camera + HUD debug settings” checked off; future sessions start with last-used view. | -| 2025-12-03 | `warp-viewer` roadmap | Reprioritized viewer roadmap around hexagonal boundaries: core/services hold session/config/toasts; viewer becomes a rendering adapter; editor service is a separate adapter. | Avoids “tiny editor” creep in viewer; aligns upcoming work to extract core (`echo-app-core`, config/toast services), add session service/client, then resume UX/perf polish. | Roadmap now sequences architecture first, then service/client, then UX/visual layers. | -| 2025-12-03 | `echo-app-core` + `echo-config-fs` | Added core app crate (ConfigService/ToastService/ViewerPrefs) and filesystem ConfigStore adapter; warp-viewer now loads/saves prefs via the new services (no serde/directories in viewer). | Reclaims hex boundaries: persistence + prefs live in shared core/adapters; viewer stays render-focused and emits prefs on exit. | Roadmap P0 items for core + fs adapter + viewer refactor are checked off; toast rendering still pending. | -| 2025-12-03 | `warp-viewer` HUD toasts | Wired viewer HUD to render toasts supplied by core ToastService; config save/load errors surface as toasts. | Keeps viewer dumb: rendering only; toast lifecycle lives in core; colors/progress bar show TTL. | P0 toast renderer item done; next up: session service/client slice. | -| 2025-12-04 | Session proto/service/client skeleton | Added crates `echo-session-proto`, `echo-session-service` (stub hub), and `echo-session-client` (stub API) to prep distributed session architecture. | Establishes shared wire schema (Handshake, SubscribeWarp, WarpStream (snapshot/diff), Notification) and stub entrypoints; transport to be added next. | Roadmap P1 proto/service/client items checked (skeleton); viewer still needs to bind to client. | -| 2025-11-29 | LICENSE | Add SPDX headers to all files | LEGAL PROTECTION 🛡️✨ | -| 2025-11-30 | `F32Scalar` | Canonicalize subnormal values to zero | Now, they're zero. | -| 2025-12-01 | Docs LaTeX skeleton | Added shared preamble/title/legal pages, parts + per-shelf booklet drivers, logo PDFs; seeded onboarding roadmap and glossary chapters. | Enable one master PDF and four shelf booklets with consistent legal/branding; Orientation now has a guided entry path. | Makefile builds master + booklets; future content/diagrams can slot into parts without restructuring. | -| 2025-12-01 | `package.json` package manager | Set `packageManager` to `pnpm@10.23.0`. | Aligns repo tooling with pnpm lockfile and preferred workflow; avoids accidental npm/yarn installs. | Editors and CI will default to pnpm; prevents mismatched lockfiles or node_modules layout. | -| 2025-12-01 | `Cargo.toml` bench profile | Remove `panic = "abort"` from `[profile.bench]`. | Silence cargo warning as this setting is ignored for bench profiles; rely on release profile inheritance or default behavior. | Cleaner build output; no behavior change for benches (which default to unwind or abort based on target/inheritance). | -| 2025-11-30 | `F32Scalar` NaN reflexivity | Update `PartialEq` implementation to use `total_cmp` (via `Ord`) instead of `f32::eq`. | Ensures `Eq` reflexivity holds even for NaN (`NaN == NaN`), consistent with `Ord`. Prevents violations of the `Eq` contract in collections. | `F32Scalar` now behaves as a totally ordered type; NaN values are considered equal to themselves and comparable. | -| 2025-11-30 | `F32Scalar` NaN canonicalization | Canonicalize all NaNs to `0x7fc00000` in `F32Scalar::new`. Update MSRV to 1.83.0 for `const` `is_nan`/`from_bits`. | Guarantees bit-level determinism for all float values including error states. Unifies representation across platforms. MSRV bump enables safe, readable `const` implementation. | All NaNs are now bitwise identical; `const fn` constructors remain available; `warp-core` requires Rust 1.83.0+. | -| 2025-11-30 | `F32Scalar` canonicalization | Enforce bitwise determinism by canonicalizing `-0.0` to `+0.0` for all `F32Scalar` instances; implement `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Display`. Make `value` field private. | Essential for bit-perfect cross-platform determinism in math operations and comparisons, especially for hashing and serialization. Prevents accidental introduction of `-0.0` by direct field access. | Guarantees consistent numerical behavior for `F32Scalar`; all public API methods and constructors now ensure canonical zero. | -| 2025-11-29 | `F32Scalar` | Add `warp-core::math::scalar::F32Scalar` type | Now we have it. | -| 2025-11-03 | Scalar foundation | Add `warp-core::math::Scalar` trait (operator supertraits + sin/cos) | Arithmetic via `Add/Sub/Mul/Div/Neg` supertraits for ergonomic `+ - * /`; `sin/cos` methods declared; canonicalization/LUTs deferred | Unblocks F32Scalar and DFix64 implementations; math code can target a stable trait | -| 2025-10-23 | Repo reset | Adopt pnpm + TS skeleton | Monorepo scaffolding for Echo | Phase 0 tasks established | -| 2025-10-24 | Branch tree spec | Integrate roaring bitmaps and chunk epochs | Deterministic merges & diffs | Snapshot policy updated | -| 2025-10-24 | Codex’s Baby spec | Event envelopes, temporal bridge integration | Align with causality layer | Security envelopes + inspector updates | -| 2025-10-25 | Serialization protocol | Canonical encoding using BLAKE3 | Cross-platform determinism | Replay tooling groundwork | -| 2025-10-25 | Temporal bridge doc | Formalized retro delivery & paradox guard | Ensure cross-branch consistency | Entropy hooks refined | -| 2025-10-25 | Replay plan | Golden hashes + CLI contract | Ensure reproducibility | Phase 1 test suite scope | -| 2025-10-25 | Math validation harness | Landed Rust fixture suite & tolerance checks for deterministic math | Keep scalar/vector/matrix/quaternion results stable across environments | Extend coverage to browser + fixed-point modes | -| 2025-10-26 | EPI bundle | Adopt entropy, plugin, inspector, runtime config specs (Phase 0.75) | Close causality & extensibility gap | Phase 1 implementation backlog defined | -| 2025-10-26 | WARP + Confluence | Adopt WARP v2 (typed DPOi engine) and Confluence synchronization as core architecture | Unify runtime/persistence/tooling on deterministic rewrites | Launch Rust workspace (warp-core/ffi/wasm/cli), port ECS rules, set up Confluence networking | -| 2025-10-27 | Core math split | Split `warp-core` math into focused submodules (`vec3`, `mat4`, `quat`, `prng`) replacing monolithic `math.rs`. | Improves readability, testability, and aligns with strict linting. | Update imports; no behavior changes intended; follow-up determinism docs in snapshot hashing. | -| 2025-10-27 | PR #7 prep | Extracted math + engine spike into `warp-core` (split-core-math-engine); added inline rustdoc on canonical snapshot hashing (node/edge order, payload encoding). | Land the isolated, reviewable portion now; keep larger geometry/broad‑phase work split for follow-ups. | After docs update, run fmt/clippy/tests; merge is a fast‑forward over `origin/main`. | - -## Recent Decisions (2025-10-28 onward) - -The following entries use a heading + bullets format for richer context. - -## 2026-01-03 — Planning hygiene + #206 kickoff (DPO concurrency litmus) - -- Decision: treat the execution plan as a live artifact again (update “Today’s Intent” + “Next Up”), then start issue #206 with a minimal spec note + deterministic litmus tests in `warp-core`. -- Rationale: plan drift causes duplicate triage, and concurrency/determinism claims need executable, reviewable evidence (commuting vs conflicting vs overlapping-but-safe cases). -- Next: land a small DPO concurrency note and a new litmus test file that compares terminal digests across enqueue orders and pins conflict outcomes. -| 2025-12-01 | `docs/guides/how-do-echo-work` PDF build | Escaped bare `&` tokens, fixed TikZ bidirectional arrows, added a minimal Rust listing language, and made the LaTeX Makefile run in non-interactive `-halt-on-error` mode (three passes). | Prevents TikZ parse failures and listings errors, avoids TeX prompting in CI/automation, and keeps code samples readable. | `make` in `docs/guides/how-do-echo-work` now produces `main.pdf` without interaction; remaining output is cosmetic overfull hbox warnings. | -| 2025-12-01 | `docs/guides/how-do-echo-work` accuracy + visuals | Synced guide content to current code: clarified scheduler kinds (Radix/Legacy), footprint conflicts, sandbox determinism helper, and `F32Scalar` behavior (canonical zero only; NaNs passthrough for now). Added timeline tree TikZ, resized hex diagram, refreshed comparison table, and Rust listings. Removed layout warnings. | Keep the guide truthful to warp-core as implemented; improves reader clarity and CI reliability. | `main.pdf` builds non-interactively; visuals/tables reflect actual APIs and invariants; remaining determinism work (LUT sin/cos, NaN canonicalization) is called out as future work. | -| 2025-12-01 | SPDX appendix + CI check | Added `legal-appendix.tex` with dual-license explainer and included it in the guide. Introduced `spdx-header-check.yml` workflow that runs `scripts/check_spdx.sh --check --all` to enforce SPDX headers. | Ensures licensing terms are visible in the book and keeps automated enforcement in CI. | New appendix renders in PDF; workflow will fail PRs missing SPDX headers. | -| 2025-11-06 | warp-core scheduler Clippy cleanup | Make pre-commit pass without `--no-verify`: fix `doc_markdown`, `similar_names`, `if_not_else`, `option_if_let_else`, `explicit_iter_loop`; change `RewriteThin.handle` to `usize`; keep radix `counts16` as `Vec<u32>` (low bandwidth) with safe prefix-sum/scatter; fail fast in drain with `unreachable!` instead of `expect()` or silent drop; make `pending` field private (keep `PendingTx` private). | Preserve determinism and ordering while satisfying strict `clippy::pedantic` and `-D warnings`. Avoid truncation casts and private interface exposure. | Determinism preserved; panic on invariant violation; histogram remains 256 KiB on 64‑bit; pre-commit unblocked. -| 2025-11-06 | warp-core test + benches lint fixes | Clean up `clippy::pedantic` failures blocking commit: (1) add backticks to doc comments for `b_in`/`b_out` and `GenSet(s)`; (2) refactor `DeterministicScheduler::reserve` into helpers to satisfy `too_many_lines`; (3) move inner test function `pack_port` above statements to satisfy `items_after_statements`; (4) remove `println!` and avoid `unwrap()`/`panic!` in tests; (5) use captured format args and `u64::from(...)`/`u32::from(...)` idioms; (6) fix `warp-benches/benches/reserve_scaling.rs` imports (drop unused `CompactRuleId` et al.) and silence placeholder warnings. | Align tests/benches with workspace lint policy while preserving behavior; ensure CI and pre-commit hooks pass uniformly. | Clippy clean on lib + tests; benches compile; commit hook no longer blocks. -| 2025-11-06 | CI fix | Expose `PortSet::iter()` (no behavior change) to satisfy scheduler iteration in CI. | Unblocks Clippy/build on GH; purely additive API. | CI gates resume. -| 2025-10-30 | warp-core determinism hardening | Added reachability-only snapshot hashing; closed tx lifecycle; duplicate rule detection; deterministic scheduler drain order; expanded motion payload docs; tests for duplicate rule name/id and no‑op commit. | Locks determinism contract and surfaces API invariants; prepares PR #7 for a safe merge train. | Clippy clean for warp-core; workspace push withheld pending further feedback. | -| 2025-10-30 | Tests | Add golden motion fixtures (JSON) + minimal harness validating motion rule bytes/values | Establishes deterministic test baseline for motion; supports future benches and tooling | No runtime impact; PR-01 linked to umbrella and milestone | -| 2025-10-30 | Templates PR scope | Clean `echo/pr-templates-and-project` to contain only templates + docs notes; remove unrelated files pulled in by merge; fix YAML lint (trailing blanks; quote placeholder) | Keep PRs reviewable and single-purpose; satisfy CI Docs Guard | Easier review; no runtime impact | -| 2025-10-30 | Docs lint | Fix MD022 (blank line after headings) in `docs/spec-deterministic-math.md` on branch `echo/docs-math-harness-notes` | Keep markdown lint clean; improve readability | No content change; unblock future docs PRs | -| 2025-10-30 | Bug template triage | Add optional `stack_trace` and `version` fields to `.github/ISSUE_TEMPLATE/bug.yml` | Capture logs and version/SHA up front to speed debugging | Better triage signal without burdening reporters | -| 2025-10-30 | Bug template wording | Standardize bug template descriptions to imperative capitalization ("Provide …") | Consistent style and clearer prompts | Improved reporter guidance | -| 2025-10-30 | Proptest seed pinning | Add dev‑dep `proptest` and a pinned‑seed property test for motion rule (`proptest_seed_pinning.rs`) | Establish deterministic, reproducible property tests and document seed‑pinning pattern | Tests‑only; no runtime impact | -| 2025-10-30 | CI matrix | Add musl tests job (warp-core; x86_64-unknown-linux-musl) and a manual macOS workflow for local runs | Cover glibc + musl in CI while keeping macOS optional to control costs | Determinism coverage improves; CI footprint remains lean | -| 2025-10-30 | Motion negative tests (PR-06) | Add tests documenting NaN/Infinity propagation and invalid payload size NoMatch in motion rule | Clarify expected behavior without changing runtime; improves determinism docs via tests | Tests-only; no runtime impact | -| 2025-10-30 | BLAKE3 header tests (PR-09) | Add unit tests to verify commit header encoding order/endianness and hash equivalence | Codifies checklist guarantees in tests; prevents regressions | Tests-only; no runtime impact | -| 2025-10-30 | README CI tips (PR-10) | Document manual macOS workflow and how to reproduce CI locally | Lowers barriers to contributor validation | Docs-only | -| 2025-10-28 | PR #7 merged | Reachability-only snapshot hashing; ports demo registers rule; guarded ports footprint; scheduler `finalize_tx()` clears `pending`; `PortKey` u30 mask; hooks+CI hardened (toolchain pin, rustdoc fixes). | Determinism + memory hygiene; remove test footguns; pass CI with stable toolchain while keeping warp-core MSRV=1.68. | Queued follow-ups: #13 (Mat4 canonical zero + MulAssign), #14 (geom train), #15 (devcontainer). | -| 2025-10-27 | MWMR reserve gate | Engine calls `scheduler.finalize_tx()` at commit; compact rule id used on execute path; per‑tx telemetry summary behind feature. | Enforce independence and clear active frontier deterministically; keep ordering stable with `(scope_hash, family_id)`. | Toolchain pinned to Rust 1.68; add design note for telemetry graph snapshot replay. | - - -## 2025-10-28 — Mat4 canonical zero + MulAssign (PR #13) - -- Decision: Normalize -0.0 from trig constructors in Mat4 and add MulAssign for in-place multiplication. -- Rationale: Avoid bitwise drift in snapshot/matrix comparisons across platforms; improve ergonomics in hot loops. -- Impact: No API breaks. New tests assert no -0.0 in rotation matrices at key angles; added `MulAssign` for owned/&rhs. -- Next: Review feedback; if accepted, apply same canonicalization policy to other math where applicable. - -## 2025-10-28 — Geometry merge train (PR #14) - -- Decision: Use an integration branch to validate #8 (geom foundation) + #9 (broad-phase AABB) together. -- Rationale: Surface cross-PR interactions early and avoid rebase/force push; adhere to merge-only policy. -- Impact: New crate `warp-geom` (AABB, Transform, TemporalTransform) and baseline broad-phase with tests. No public API breaks in core. -- Next: If green, merge train PR; close individual PRs as merged-via-train. - -## 2025-10-28 — warp-geom foundation (PR #8) compile + clippy fixes - -- Decision: Keep PR #8 scoped to geometry foundations; defer `broad` module to its own PR to avoid E0583. -- Changes: Use `Quat::to_mat4` + `Mat4::new` in `Transform::to_mat4`; replace `Vec3::ZERO` with `Vec3::new(0,0,0)` for MSRV; rename variables to satisfy `similar_names`. -- CI: Merged latest `main` to pick up stable-toolchain overrides for workspace clippy/test; crate-level clippy gates relaxed (drop `nursery`/`cargo`) to avoid workspace metadata lints. -- Next: Land PR #9 for broad-phase on top; revisit clippy gates once workspace has uniform metadata. -## 2025-10-28 — Devcontainer added - -- Decision: Provide a reproducible local environment matching CI runners. -- Details: VS Code devcontainer (Ubuntu 24.04) with Rust stable + MSRV toolchains, clippy/rustfmt, Node 20, gh CLI; post-create script installs 1.68.0 and wasm target. -- Outcome: Faster feedback loops; easier reproduction of CI issues (clippy, rustdoc, Docs Guard). - -## 2025-10-28 — Pre-commit formatting flag renamed - -- Decision: Use an Echo-scoped env var for auto-format on commit. -- Change: `AUTO_FMT` → `ECHO_AUTO_FMT` in `.githooks/pre-commit`. -- Docs: README, AGENTS, CONTRIBUTING updated with hook install and usage. - -## 2025-10-29 — Snapshot header v1 + tx/rule hardening (warp-core) - -- Context: PR #9 base work on top of PR #8; integrate deterministic provenance into snapshots without changing reachable‑only state hashing. -- Decision: Model snapshots as commit headers with explicit `parents` and metadata digests (`plan`, `decision`, `rewrites`). Keep `decision_digest = blake3(len=0_u64)` (canonical empty list digest) until Aion/agency lands. -- Changes: - - `Snapshot { parents: Vec<Hash>, plan_digest, decision_digest, rewrites_digest, policy_id }`. - - `Engine::commit()` computes `state_root`, canonical empty/non‑empty digests, and final commit hash. - - `Engine::snapshot()` produces a header‑shaped view with canonical empty digests so a no‑op commit equals a pre‑tx snapshot. - - Enforce tx lifecycle (`live_txs` set; deny ops on closed/zero tx); `begin()` is `#[must_use]` and wraps on `u64::MAX` skipping zero. - - Rule registration now rejects duplicate names and duplicate ids; assigns compact rule ids for execution hot path. - - Scheduler is crate‑private; ordering invariant documented (ascending `(scope_hash, rule_id)`). -- Tests: Added/updated motion tests (velocity preserved; commit after `NoMatch` is a no‑op), math tests (relative tolerances; negative scalar multiplies; extra mul order). -- Consequence: Deterministic provenance is now explicit; future Aion inputs can populate `decision_digest` without reworking the header. No behavior changes for state hashing. - -## 2025-10-29 — Toolchain strategy: floor raised to 1.71.1 - -- Decision: Raise the workspace floor (MSRV) to Rust 1.71.1. All crates and CI jobs target 1.71.1. -- Implementation: Updated `rust-toolchain.toml` to 1.71.1; bumped `rust-version` in crate manifests; CI jobs pin 1.71.1; devcontainer installs only 1.71.1. - -## 2025-10-29 — Docs E2E carousel init (PR #10) - -- Context: Playwright tour test clicks Next to enter carousel from "all" mode. -- Decision: Do not disable Prev/Next in "all" mode; allow navigation buttons to toggle into carousel. -- Change: docs/public/assets/collision/animate.js leaves Prev/Next enabled in 'all'; boundary disabling still applies in single-slide mode. -- Consequence: Users can initiate the carousel via navigation controls; E2E tour test passes deterministically. - -## 2025-10-29 — Docs make open (PR #11) - -- Context: Make dev docs open automatically; fix routing and dead-link noise. -- Decisions: - - Use a precise dead-link ignore for `/collision-dpo-tour.html` (exact regex) until the page is always present. - - Convert tour/spec links to root‑relative paths to work site‑wide under VitePress routing. - - Make the dev server polling loop portable (`sleep 1`). -- Consequence: Docs dev flow is consistent across environments; CI Docs Guard happy; links resolve from any page. - -## 2025-10-29 — Hooks formatting gate (PR #12) - -- Context: Enforce consistent formatting before commit; avoid CI/docs drift when non-doc files change. -- Decision: Pre-commit runs `cargo fmt --all -- --check` whenever staged Rust files are detected. Retain the PRNG coupling guard but remove the unconditional early exit so formatting still runs when the PRNG file isn’t staged. -- EditorConfig: normalize line endings (LF), ensure final newline, trim trailing whitespace, set 2-space indent for JS/TS/JSON and 4-space for Rust. -- Consequence: Developers get immediate feedback on formatting; cleaner diffs and fewer CI round-trips. - -## 2025-10-29 — Geom fat AABB bounds mid-rotation - -- Context: Broad-phase must not miss overlaps when a shape rotates about an off‑centre pivot; union of endpoint AABBs can under‑approximate mid‑tick extents. -- Decision: `Timespan::fat_aabb` now unions AABBs at start, mid (t=0.5 via nlerp for rotation, lerp for translation/scale), and end. Sampling count is fixed (3) for determinism. -- Change: Implement midpoint sampling in `crates/warp-geom/src/temporal/timespan.rs`; add test `fat_aabb_covers_mid_rotation_with_offset` to ensure mid‑pose is enclosed. -- Consequence: Deterministic and more conservative broad‑phase bounds for typical rotation cases without introducing policy/config surface yet; future work may expose a configurable sampling policy. - -## 2025-10-29 — Pre-commit auto-format policy - -- Decision: When `ECHO_AUTO_FMT=1` (default), the pre-commit hook first checks formatting. If changes are needed, it runs `cargo fmt` to update files, then aborts the commit. This preserves index integrity for partially staged files and prevents unintended staging of unrelated hunks. -- Rationale: `rustfmt` formats entire files; auto-restaging could silently defeat partial staging. Aborting makes the workflow explicit: review, restage, retry. -- Consequence: One extra commit attempt in cases where formatting is needed, but safer staging semantics and fewer surprises. Message includes guidance (`git add -p` or `git add -A`). - -## 2025-10-29 — CI + Security hardening - -- Decision: Add `cargo audit` and `cargo-deny` to CI; expand rustdoc warnings gate to all public crates. -- Rationale: Catch vulnerable/deprecated crates and doc regressions early; keep public surface clean. -- Consequence: Faster failures on dependency or doc issues; small CI time increase. -- Notes: - - Use `rustsec/audit-check@v1` for the audit step; avoid pinning to non-existent tags. - - Add `deny.toml` with an explicit license allowlist to prevent false positives on permissive licenses (Apache-2.0, MIT, BSD-2/3, CC0-1.0, MIT-0, Unlicense, Unicode-3.0, BSL-1.0, Apache-2.0 WITH LLVM-exception). - - Run cargo-audit on Rust 1.75.0 (via `RUSTUP_TOOLCHAIN=1.75.0`) to meet its MSRV; this does not change the workspace MSRV (1.71.1). - -## 2025-10-29 — Snapshot commit spec (v1) - -- Decision: Introduce `docs/spec-merkle-commit.md` describing `state_root` vs `commit_id` encodings and invariants. -- Rationale: Make provenance explicit and discoverable; align code comments with a durable spec. -- Changes: Linked spec from `crates/warp-core/src/snapshot.rs` and README. - -| 2025-10-30 | CI toolchain simplification | Standardize on Rust `@stable` across CI (fmt, clippy, tests, security audit); remove MSRV job; set `rust-toolchain.toml` to `stable`. | Reduce toolchain drift and recurring audit/MSRV mismatches. | Future MSRV tracking can move to release notes when needed. | -| 2025-10-30 | Rustdoc pedantic cleanup | Snapshot docs clarify `state_root` with code formatting to satisfy `clippy::doc_markdown`. | Keep strict lint gates green; no behavior change. | None. | -| 2025-10-30 | Spec + lint hygiene | Removed duplicate `clippy::module_name_repetitions` allow in `warp-core/src/lib.rs`. Clarified `docs/spec-merkle-commit.md`: `edge_count` is u64 LE and may be 0; genesis commits have length=0 parents; “empty digest” explicitly defined as `blake3(b"")`; v1 mandates empty `decision_digest` until Aion lands. | Codifies intent; prevents ambiguity for implementers. | No code behavior changes; spec is clearer. | -| 2025-10-30 | Templates & Project | Added issue/PR/RFC templates and configured Echo Project (Status: Blocked/Ready/Done); fixed YAML lint nits | Streamlines review process and Kanban tracking | No runtime impact; CI docs guard satisfied | - -## 2025-11-02 — M1: benches crate skeleton (PR-11) - -- Decision: Add `crates/warp-benches` with a minimal Criterion harness and a motion-throughput benchmark using public `warp-core` APIs. -- Rationale: Establish a place for performance microbenches; keep PR small and focused before adding JSON artifacts/regression gates in follow-ups. -- Consequence: Benches run locally via `cargo bench -p warp-benches`; no runtime changes. - -## 2025-11-01 — PR-10 scope hygiene (tests split) - -- Context: PR‑10 (README/CI/docs) accidentally included commit header tests in `snapshot.rs`, overlapping with PR‑09 (tests‑only). -- Decision: Remove the test module from PR‑10 to keep it strictly docs/CI/tooling; keep all BLAKE3 commit header tests in PR‑09 (`echo/pr-09-blake3-header-tests`). -- Consequence: Clear PR boundaries; no runtime behavior change in PR‑10. - - -## 2025-11-02 — CI hotfix: cargo-deny (benches) - -- Context: CI `cargo-deny` job failed on PR-11 due to `warp-benches` lacking a license and a prior wildcard dependency reference reported by CI logs. -- Decision: Add `license = "Apache-2.0"` to `crates/warp-benches/Cargo.toml` and ensure `warp-core` is referenced via a path dev-dependency (no wildcard). -- Rationale: Keep workspace policy consistent with other crates (Apache-2.0) and satisfy bans (wildcards = deny) and licenses checks. -- Consequence: `cargo-deny` bans/licenses should pass; remaining warnings are deprecations in `deny.toml` to be addressed in a later sweep. - -## 2025-11-02 — cargo-deny modernization - -- Context: CI emitted deprecation warnings for `copyleft` and `unlicensed` keys in `deny.toml` (cargo-deny PR #611). -- Decision: Remove deprecated keys; rely on the explicit permissive `allow = [...]` list to exclude copyleft licenses; ensure all workspace crates declare a license (benches fixed earlier). -- Rationale: Keep CI quiet and align with current cargo-deny schema without weakening enforcement. -- Consequence: Same effective policy, no deprecation warnings; future license exceptions remain possible via standard cargo-deny mechanisms. -- CI Note: Use `cargo-deny >= 0.14.21` in CI (workflow/container) to avoid schema drift and deprecation surprises. Pin the action/image or the downloaded binary version accordingly. - -## 2025-11-02 — PR-12: benches pin + micro-optimizations - -- Context: CI cargo-deny flagged wildcard policy and benches had minor inefficiencies. -- Decision: - - Pin `blake3` in `crates/warp-benches/Cargo.toml` to exact patch `=1.8.2` and - disable default features (`default-features = false, features = ["std"]`) to - avoid rayon/parallelism in microbenches. - - `snapshot_hash`: compute `link` type id once; label edges as `e-i-(i+1)` (no `e-0-0`). - - `scheduler_drain`: builder returns `Vec<NodeId>`; `apply` loop uses precomputed ids to avoid re-hashing. -- Rationale: Enforce deterministic, single-threaded hashing in benches and satisfy - cargo-deny wildcard bans; reduce noise from dependency updates. -- Consequence: Cleaner dependency audit and slightly leaner bench setup without - affecting runtime code. - -## 2025-11-02 — PR-12: benches constants + documentation - -- Context: Pedantic review flagged magic strings, ambiguous labels, and unclear throughput semantics in benches. -- Decision: Extract constants for ids/types; clarify edge ids as `<from>-to-<to>`; switch `snapshot_hash` to `iter_batched`; add module-level docs and comments on throughput and BatchSize; retain blake3 exact patch pin `=1.8.2` with trimmed features to stay consistent with CI policy. -- Rationale: Improve maintainability and readability while keeping dependency policy coherent and deterministic. -- Consequence: Benches read as executable docs; CI docs guard updated accordingly. - -## 2025-11-02 — PR-12: benches README + main link - -- Context: Missing documentation for how to run/interpret Criterion benches. -- Decision: Add `crates/warp-benches/benches/README.md` and link from the top-level `README.md`. -- Rationale: Improve discoverability and ensure new contributors can reproduce measurements. -- Consequence: Docs Guard satisfied; single-source guidance for bench usage and outputs. - -## 2025-11-02 — PR-12: Sync with main + merge conflict resolution - -- Context: GitHub continued to show a merge conflict on PR #113 (`echo/pr-12-snapshot-bench`). -- Decision: Merge `origin/main` into the branch (merge commit; no rebase) and resolve the conflict in `crates/warp-benches/Cargo.toml`. -- Resolution kept: - - `license = "Apache-2.0"`, `blake3 = { version = "=1.8.2", default-features = false, features = ["std"] }` in dev-dependencies. - - `warp-core = { version = "0.1.0", path = "../warp-core" }` (version-pinned path dep per cargo-deny bans). - - Bench targets: `motion_throughput`, `snapshot_hash`, `scheduler_drain`. -- Rationale: Preserve history with a merge, align benches metadata with workspace policy, and clear PR conflict status. -- Consequence: Branch synced with `main`; local hooks (fmt, clippy, tests, rustdoc) passed; CI Docs Guard satisfied via this log and execution-plan update. - -## 2025-11-02 — Benches DX: offline report + server reliability - -- Context: `make bench-report` started a background HTTP server that sometimes exited immediately; opening the dashboard via `file://` failed because the page fetched JSON from `target/criterion` which browsers block over `file://`. -- Decision: - - Add `nohup` to the `bench-report` server spawn and provide `bench-status`/`bench-stop` make targets. - - Add `scripts/bench_bake.py` and `make bench-bake` to generate `docs/benchmarks/report-inline.html` with Criterion results injected as `window.__CRITERION_DATA__`. - - Teach `docs/benchmarks/index.html` to prefer inline data when present, skipping network fetches. -- Rationale: Remove friction for local perf reviews and allow sharing a single HTML artifact with no server. -- Consequence: Two paths now exist—live server dashboard and an offline baked report. Documentation updated in main README and benches README. `bench-report` now waits for server readiness and supports `BENCH_PORT`. -## 2025-11-30 — PR #121 CodeRabbit batch fixes (scheduler/bench/misc) - -- Context: Address first review batch for `perf/scheduler` (PR #121) covering radix drain, benches, and tooling hygiene. -- Decisions: - - Removed placeholder `crates/warp-benches/benches/reserve_scaling.rs` (never ran meaningful work; duplicated hash helper). - - Added `PortSet::keys()` and switched scheduler boundary-port conflict/mark loops to use it, clarifying traversal API. - - Bumped `rustc-hash` to `2.1.1` for latest fixes/perf; updated `Cargo.lock`. - - Relaxed benches `blake3` pin to `~1.8.2` with explicit rationale to allow patch security fixes while keeping rayon disabled. - - Cleaned bench dashboards: removed dead `fileBanner` script blocks, fixed fetch fallback logic, and added vendor/.gitignore guard. - - Hardened `warp-math/build.sh` with bash shebang and `set -euo pipefail`. -- Rationale: Clean CI noise, make API usage explicit for ports, keep hashing dep current, and ensure math build fails fast. -- Consequence: Bench suite sheds a no-op target; scheduler code compiles against explicit port iteration; dependency audit reflects new rustc-hash and bench pin policy; dashboard JS is consistent; math build is safer. Docs guard satisfied via this log and execution-plan update. - -## 2025-12-01 — PR #121 follow-ups (portability, collision bench stub, doc clarifications) - -- Context: Second batch of CodeRabbit feedback for scheduler/bench docs. -- Decisions: - - Makefile: portable opener detection (open/xdg-open/powershell) for `bench-open`/`bench-report`. - - Added `scheduler_adversarial` Criterion bench exercising FxHashMap under forced collisions vs random keys; added `rustc-hash` to benches dev-deps. - - Introduced pluggable scheduler selection (`SchedulerKind`: Radix vs Legacy) with Radix default; Legacy path retains BTreeMap drain + Vec<Footprint> independence for apples-to-apples comparisons. - - Added sandbox helpers (`EchoConfig`, `build_engine`, `run_pair_determinism`) for spinning up isolated Echo instances and per-step Radix vs Legacy determinism checks. - - Documentation clarifications: collision-risk assumption and follow-up note in `docs/scheduler-reserve-complexity.md`; softened reserve validation claims and merge gating for the “10–100x” claim in `docs/scheduler-reserve-validation.md`; fixed radix note fences and `RewriteThin.handle` doc to `usize`. - - warp-math: documented \DPO macro parameters; fixed `warp-rulial-distance.tex` date to be deterministic. - - scripts/bench_bake.py: executable bit, narrower exception handling, f-string output. -- Consequence: Bench portability and collision stress coverage improved; sandbox enables A/B determinism tests; docs no longer overclaim; LaTeX artifacts become reproducible. Remaining follow-ups: adversarial hasher evaluation, markdown lint sweep, IdSet/PortSet IntoIterator ergonomics. - -## 2025-12-29 — warp-core follow-ups: policy id plumbing + edge replay index (#151, #152) - -- Context: Two “minor” follow-ups were at risk of compounding into boundary drift: - - Policy id was previously hardcoded/magic (`0u32`), even though commit header v2 and tick patch digests commit to `policy_id`. - - Tick patch replay used an `O(total_edges)` scan (`remove_edge_by_id`) to migrate/remove edges by id. -- Decisions: - - Make `policy_id` an explicit `Engine` configuration parameter, defaulting to `POLICY_ID_NO_POLICY_V0` (`b"NOP0"` as little‑endian `u32`), and commit it consistently into `patch_digest` and `commit_id` v2. - - Add a `GraphStore` reverse edge index (`EdgeId -> from NodeId`) and use it during tick patch replay so `UpsertEdge`/migration and `DeleteEdge` are `O(bucket_edges)` instead of scanning every bucket. - - Maintain a store invariant that `edges_from` contains no empty buckets (prune buckets when the last edge is removed) so `state_root` remains a function of graph content, not mutation history. -- Rationale: Paper III boundary artifacts (delta patches) must stay replayable and scalable; committing to `policy_id` only makes sense if callers can set it explicitly; and replay must not devolve into quadratic behavior for large graphs. -- Consequence: Patch replay performance scales with local bucket size; policy semantics are explicit at the boundary; and identical graph states hash identically regardless of whether they were reached via add/remove edge sequences. - -## 2025-12-29 — warp-core Stage B0: two-plane attachments as typed atoms - -- Context: Echo needed to align with the WARP papers’ two-plane semantics (SkeletonGraph + attachment plane) without slowing rewrite matching/indexing. -- Decisions: - - Define attachment payloads as typed, opaque atoms: `AtomPayload { type_id: TypeId, bytes: Bytes }`. - - Keep node/edge records skeleton-only (`NodeRecord`, `EdgeRecord`); store attachment values separately in an attachment plane. - - Introduce a strict codec boundary (`Codec<T>`, `DecodeError`, `CodecRegistry`) for rules/views/tooling; core matching/indexing does not decode. - - Deterministic decode failure semantics (v0): type mismatch or strict decode failure ⇒ “rule does not apply”. - - Commit `type_id` into canonical encodings/digests (state_root and patch_digest) to prevent “same bytes, different meaning” collisions. - - Lock the law: no hidden edges in payload bytes (dependencies must be skeleton-visible). -- Rationale: preserve determinism and performance while making attachment semantics explicit and safe. -- Consequence: payload typing becomes a first-class boundary artifact; the rewrite hot path remains skeleton-only; future recursion can be layered without reinterpreting bytes as structure. - - Docs: `docs/spec/SPEC-0001-attachment-plane-v0-atoms.md` (spec) and `docs/warp-two-plane-law.md` (project law). - -## 2025-12-30 — warp-core Stage B1: WarpInstances + descended attachments (flattened indirection) - -- Context: Depth-0 atoms alone are “WARP graphs of depth 0”; to model “WARPs all the way down” we needed explicit recursion without recursive hot-path traversal. -- Decisions: - - Introduce instance-scoped identity (`WarpId`, `NodeKey`, `EdgeKey`) and multi-instance state (`WarpState`, `WarpInstance`). - - Extend the attachment plane with explicit indirection: `AttachmentValue::Descend(WarpId)` (no encoding of structure inside bytes). - - Make attachment slots first-class resources for causality: - - `AttachmentKey` is a stable slot identity. - - Footprints/scheduler treat attachment reads/writes as conflict resources. - - Execution inside a descended instance must READ the descent-chain attachment keys. - - Update the deterministic boundary: - - `state_root` hashes reachable instance content across `Descend` links (skeleton + attachments + instance headers). - - tick patch ops include instance metadata and attachment edits; patch digest encoding bumped to v2. - - Add a minimal slice helper test validating that descendant work pulls in the portal producer (Paper III slice closure). -- Rationale: make recursion explicit, hashable, sliceable, and scheduler-visible while keeping matching/indexing skeleton-only within an instance. -- Consequence: Echo can represent recursive WARP state via flattened indirection; descended-instance rewriting stays fast and deterministic; slicing remains trivial over `in_slots`/`out_slots` without decoding atoms. - - Docs: `docs/spec/SPEC-0002-descended-attachments-v1.md` (spec), `docs/spec-merkle-commit.md` (state_root reachability), and `docs/spec-warp-tick-patch.md` (patch digest v2 / portal ops). - -## 2025-12-30 — warp-core Stage B1.1: Atomic portals (OpenPortal) + merge/DAG slicing semantics - -- Context: Stage B1 made descended attachments explicit, but portal authoring could still be expressed as multiple independent edits across ticks (create instance vs set `Descend`), which risks slices that include the portal write but omit the instance creation history. -- Decisions: - - Add a single canonical tick patch operation for portal authoring: `WarpOp::OpenPortal { key, child_warp, child_root, init }`. - - `OpenPortal` atomically establishes instance metadata and sets the `Descend(child_warp)` attachment slot. - - Patch replay validates “no dangling portal” and “no orphan instance” invariants. - - Bump `TickReceipt` digest format version to `2u16` after switching scope encoding to instance-scoped `NodeKey` so receipts remain self-describing across format evolution. - - Update the patch diff constructor (`diff_state`) to emit `OpenPortal` for newly created descended instances when the `parent` slot is set to `Descend(child_warp)`, avoiding separate instance + attachment ops. - - Specify multi-parent merge semantics explicitly: - - merge commits must include an explicit merge patch that resolves all conflicting slot writes (including attachment slots); no implicit “parent order wins”. - - DAG slicing generalizes worldline slicing by treating merge patches as first-class resolution events. - - Add a canonical terminology doc to pin “instances/portals vs wormholes” and prevent future term collisions. -- Rationale: Keep the Paper III replay boundary honest: the patch must be the boundary artifact, slices must not be able to omit required portal/instance creation history, and merges must be deterministically authored. -- Consequence: Portal authoring becomes clean and inevitable instead of a long-term swamp; tooling can build an honest instance-zoom view over explicit portals; merge/DAG slicing semantics are specified (implementation work remains). - - Docs: `docs/spec/SPEC-0002-descended-attachments-v1.md` (merge commits + DAG slicing), `docs/architecture/TERMS_WARP_STATE_INSTANCES_PORTALS_WORMHOLES.md` (terminology lock), and `docs/branch-merge-playbook.md` (conflict resolution playbook). - - Tests: `crates/warp-core/src/tick_patch.rs` (patch replay rejects dangling portals and orphan instances). - -## 2025-12-30 — THEORY.md: Foundations paraphrase + explicit divergence callouts - -- Context: Echo’s implementation is driven by the published AIΩN Foundations papers, but Echo also makes pragmatic runtime trade-offs (performance, determinism boundaries, cross-language replay). -- Decisions: - - Add `docs/THEORY.md` as a paraphrase/tour of Papers I–IV (WARPs, deterministic ticks, tick patches/slicing, observer geometry). - - Whenever Echo intentionally diverges from the paper definitions, add an inline callout in the form: - - `> [!note]` - - `> Echo does this differently. It works like X because Y.` - - Commit the `assets/echo-white-radial.svg` brand asset and remove the local-only `assets/readme/` scratch directory. -- Rationale: Readers should be able to move between the papers and the repo without guessing which parts are “paper law” vs “engine law,” and any deviation should be documented with rationale to keep the repo backlog honest. -- Consequence: THEORY becomes an onboarding bridge and drift detector; future changes that diverge from the papers have a clear place to document rationale. - - Evidence: `c029c82` - -## 2025-12-30 — PR #162: CodeRabbit doc nits (prose/markdown) - -- Context: CodeRabbit review requested small readability tweaks and markdownlint-sensitive cleanup in `docs/THEORY.md`. -- Decisions: - - Tighten minor wordiness/repetition without changing meaning. - - Keep formatting conservative (no trailing whitespace; blank lines around headings). -- Rationale: Keep the “paper bridge” doc readable and reduce review noise without churn. -- Consequence: Same technical content, slightly tighter prose; markdownlint nits avoided. - - Evidence: `22ba855` - -## 2026-01-09 — append-only guardrails for onboarding docs - -- Context: AGENTS, docs/decision-log, TASKS-DAG, and docs/execution-plan capture chronological intent and must never delete or reorder existing entries. -- Decision: Replace `merge=union` automation with a documented CI guarantee: `scripts/check-append-only.js` (c.f. `docs/append-only-invariants.md`) runs before merges to reject deletions, and new documentation keeps the append-only contract visible wherever these artifacts are declared. -- Rationale: Line-based `merge=union` silently reintroduces deletions and duplicates; an explicit append-only check surfaces violations and keeps the invariant enforcement central and auditable. -- Compliance: Keep the policy referenced in `.gitattributes`, AGENTS, decision logs, and execution plans so future contributors can see the script + doc that gate these files; CI should fail any merge that removes or mutates existing lines in the tracked paths. diff --git a/docs/diagrams.md b/docs/diagrams.md index 98d959b7..87b175c4 100644 --- a/docs/diagrams.md +++ b/docs/diagrams.md @@ -21,7 +21,7 @@ graph LR subgraph Core["Echo Core"] ECS["@EntityComponentStore"] Scheduler["Scheduler\n(DAG + Branch Orchestrator)"] - Codex["Codex's Baby\n(Event Bus)"] + Codex["Event Bus\n(MaterializationBus)"] Timeline["Timeline Tree\n(Chronos/Kairos/Aion)"] Math["Deterministic Math\n(Vector, PRNG, Metrics)"] ECS --> Scheduler @@ -103,7 +103,7 @@ flowchart TD Clock -->|dt| SchedulerPre["Phase 1: Pre-Update"]:::stage SchedulerPre --> InputAssim["Assimilate Input\n(InputPort flush)"]:::op - InputAssim --> CodexPre["Codex's Baby\nPre-Flush"]:::op + InputAssim --> CodexPre["Event Bus\nPre-Flush"]:::op CodexPre --> TimelineIntake["Timeline Tree\nRegister Branch Jobs"]:::op TimelineIntake --> UpdatePhase["Phase 2: Update Systems"]:::stage diff --git a/docs/dind-harness.md b/docs/dind-harness.md new file mode 100644 index 00000000..28aadd27 --- /dev/null +++ b/docs/dind-harness.md @@ -0,0 +1,77 @@ + + +# DIND Harness (Deterministic Ironclad Nightmare Drills) + +The DIND harness is the deterministic verification runner for Echo/WARP. It replays canonical intent transcripts and asserts that state hashes and intermediate outputs are identical across runs, platforms, and build profiles. + +Location: +- `crates/echo-dind-harness` +- `crates/echo-dind-tests` (stable test app used by the harness) +- `testdata/dind` (scenarios + goldens) + +## Quickstart + +```bash +cargo run -p echo-dind-harness -- help +``` + +Examples (commands depend on the harness CLI): + +```bash +cargo run -p echo-dind-harness -- torture +cargo run -p echo-dind-harness -- converge +cargo run -p echo-dind-harness -- repro +``` + +## Determinism Guardrails + +Echo ships guard scripts to enforce determinism in core crates: + +- `scripts/ban-globals.sh` +- `scripts/ban-nondeterminism.sh` +- `scripts/ban-unordered-abi.sh` + +## Convergence scope (Invariant B) + +For commutative scenarios, `MANIFEST.json` can specify a `converge_scope` +node label (e.g., `sim/state`). The `converge` command compares the +projected hash of the subgraph reachable from that node, while still +printing full hashes for visibility. + +### Converge scope semantics (short spec) + +**What scopes exist today (DIND test app):** +- `sim/state` — the authoritative state root for the test app (includes theme/nav/route + kv). +- `sim/state/kv` (not currently used) — a narrower root for KV-only projections. + +**What is included in the projected hash:** +- All nodes reachable by following **outbound edges** from the scope root. +- All edges where both endpoints are reachable. +- All node and edge attachments for the included nodes/edges. + +**What is excluded:** +- Anything not reachable from the scope root (e.g., `sim/inbox`, event history, sequence sidecars). +- Inbound edges from outside the scope. + +**What “commutative” means here:** +- The operations are order-independent with respect to the **projected subgraph**. +- Either they touch disjoint footprints or they are semantically commutative + (e.g., set union on disjoint keys). + +**When you must NOT use projection:** +- When event history is semantically meaningful (auditing, causality, timelines). +- When last-write-wins behavior or ordered effects are part of the contract. +- When differences in inbox/order should be observable by the consumer. + +### CLI override (debug only) + +`converge` accepts an override for ad‑hoc debugging: + +```bash +cargo run -p echo-dind-harness -- converge --scope sim/state --i-know-what-im-doing +``` + +This bypasses `MANIFEST.json` and emits a warning. Do not use it for canonical +test results. + +Run them locally or wire them into CI for strict enforcement. diff --git a/docs/docs-index.md b/docs/docs-index.md index 4a6f7c97..ba0cfce7 100644 --- a/docs/docs-index.md +++ b/docs/docs-index.md @@ -7,15 +7,14 @@ If you want the full inventory, use repo search (`rg`) and follow links outward | Document | Purpose | | -------- | ------- | | `architecture-outline.md` | High-level architecture vision and principles | -| `execution-plan.md` | Living plan of tasks, intent, and progress | | `workflows.md` | Contributor workflows, policies, and blessed repo entry points | | `guide/warp-primer.md` | Start here: newcomer-friendly primer for WARP in Echo | | `guide/wvp-demo.md` | Demo: run the session hub + 2 viewers (publisher/subscriber) | | `guide/tumble-tower.md` | Demo 3 scenario: deterministic physics ladder (“Tumble Tower”) | | `spec-branch-tree.md` | Branch tree, diffs, and timeline persistence | -| `spec-codex-baby.md` | Event bus, bridges, backpressure, security | | `spec-temporal-bridge.md` | Cross-branch event lifecycle | | `spec-serialization-protocol.md` | Canonical encoding and hashing | +| `spec-canonical-inbox-sequencing.md` | Canonical inbox sequencing, idempotent ingress, and deterministic tie-breaks | | `spec-capabilities-and-security.md` | Capability tokens and signatures | | `spec-world-api.md` | Stable public façade for external modules | | `spec-entropy-and-paradox.md` | Entropy metrics and paradox handling | @@ -50,13 +49,11 @@ If you want the full inventory, use repo search (`rg`) and follow links outward | `scheduler-reserve-complexity.md` | Redirect: merged into `scheduler-warp-core.md` | | `testing-and-replay-plan.md` | Replay, golden hashes, entropy tests | | `runtime-diagnostics-plan.md` | Logging, tracing, inspector streams | -| `codex-instrumentation.md` | CB metrics and telemetry hooks | | `docs-audit.md` | Docs hygiene memo: purge/merge/splurge candidates | | `docs-index.md` | This index | | `hash-graph.md` | Hash relationships across subsystems | | `legacy-excavation.md` | Historical artifact log | | `memorial.md` | Tribute to Caverns | -| `decision-log.md` | Chronological design decisions | | `release-criteria.md` | Phase transition checklist | ## Start Here @@ -95,11 +92,6 @@ If you want the full inventory, use repo search (`rg`) and follow links outward - Current claims / error budgets: [/warp-math-claims](/warp-math-claims) - Validation plan (may lag behind implementation): [/math-validation-plan](/math-validation-plan) -## Project Log (Read This Before Starting Big Work) - -- Execution plan (living intent, “Today’s Intent”): [/execution-plan](/execution-plan) -- Decision log (chronological record): [/decision-log](/decision-log) - ## Reference / Deep Dives - Two-plane law (“no hidden edges”): [/warp-two-plane-law](/warp-two-plane-law) diff --git a/docs/execution-plan.md b/docs/execution-plan.md deleted file mode 100644 index 51b90637..00000000 --- a/docs/execution-plan.md +++ /dev/null @@ -1,1108 +0,0 @@ - - -# Echo Execution Plan (Living Document) - -This is Codex’s working map for building Echo. Update it relentlessly—each session, checkpoint what moved, what’s blocked, and what future-Codex must know. - ---- - -## Operating Rhythm - -- **Before Starting** - 1. Ensure `git status` is clean. If not, capture the state in `docs/decision-log.md` and wait for human guidance. - 2. Skim the latest updates in this document and `docs/decision-log.md` to synchronize with the active timeline. - 3. Update the *Today’s Intent* section below. -- **During Work** - - Record major decisions, blockers, or epiphanies in `docs/decision-log.md` (canonical log) and copy a concise summary into the Decision Log table below for quick reference. - - Keep this document current: mark completed tasks, add new sub-items, refine specs. -- **After Work** - 1. Summarize outcomes, next steps, and open questions in the Decision Log section below and ensure the full entry is captured in `docs/decision-log.md`. - 2. Update the “Next Up” queue. - 3. Push branches / PRs or leave explicit instructions for future Codex. - ---- - -## Phase Overview - -| Phase | Codename | Goal | Status | Notes | -| ----- | -------- | ---- | ------ | ----- | -| 0 | **Spec Forge** | Finalize ECS storage, scheduler, event bus, and timeline designs with diagrams + pseudo-code. | In Progress | Implement roaring bitmaps, chunk epochs, deterministic hashing, LCA binary lifting. | -| 1 | **Core Ignition** | Implement `@echo/core` MVP: entity manager, component archetypes, scheduler, Codex’s Baby basics, deterministic math utilities, tests. | Backlog | Needs dirty-index integration and branch tree core. | -| 2 | **Double-Jump** | Deliver reference adapters (Pixi/WebGL renderer, browser input), seed playground app, timeline inspector scaffolding. | Backlog | Depends on Phase 1 stability. | -| 3 | **Temporal Bloom** | Advanced ports (physics, audio, network), branch merging tools, debugging overlays. | Backlog | Long-term horizon. | - ---- - -## Today’s Intent - -> 2026-01-09 — DAG automation cleanup (IN PROGRESS) - -- Goal: make the tasks DAG generator and docs behave consistently by emitting into `docs/assets/dags`, consolidating the DAG documentation, and wiring the generator into the refresh workflow. -- Scope: - - Switch `scripts/generate-tasks-dag.js` (and its outputs) to the canonical `docs/assets/dags/` directory and keep the DOT/SVG names stable. - - Remove the misleading "Auto-generated" header from `TASKS-DAG.md`, fold `docs/dependency-dags-2.md` into `docs/dependency-dags.md`, and document how to rerun the task graph. - - Run the generator as part of `.github/workflows/refresh-dependency-dags.yml`, and capture the decision/rationale in `docs/decision-log.md`. -- Exit criteria: canonical assets + docs live under `docs/assets/dags` / `docs/dependency-dags.md`, the workflow runs the task generator, and log + plan entries describe the change. - -> 2026-01-09 — Append-only guardrails (IN PROGRESS) - -- Goal: make AGENTS.md, docs/decision-log.md, TASKS-DAG.md, and docs/execution-plan.md truly append-only by documenting the policy and running `scripts/check-append-only.js` from `docs/append-only-invariants.md`. -- Scope: - - Reference the append-only doc wherever the onboarding artifacts are asserted (AGENTS, `.gitattributes`, decision log, execution plan). - - Add a CI gate (`Append-only Guard` workflow) that runs `node scripts/check-append-only.js` (with `APPEND_ONLY_BASE` pointing at the merge base) before merges touching the tracked files. - - Educate contributors on how to update the invariants and keep the decision log entry in sync when the policy evolves. -- Exit criteria: the new doc describes the policy, `.gitattributes` and AGENTS highlight it, and the check script is wired into contributors’ workflows and CI (see `.github/workflows/append-only-check.yml`). - -> 2026-01-03 — Merge-train: oldest open PRs (#220 → #227 → #242) (IN PROGRESS) -> 2026-01-03 — Planning hygiene + start #206 (DPO concurrency litmus) (DONE) - -- Goal: refresh the execution plan so it matches current GitHub state, then begin issue #206 with a minimal “DPO concurrency litmus” suite (spec note + tests). -- Scope: - - Planning hygiene: - - Update this document’s *Today’s Intent* and “Next Up” queue to reflect the current work frontier. - - Record the pivot in `docs/decision-log.md` so the next session doesn’t re-triage already-closed threads. - - #206: - - Add a spec note documenting which parts of the DPO concurrency story Echo relies on (and what we enforce mechanically via footprints + scheduler independence checks). - - Add litmus tests that pin commuting/overlapping/conflicting rule-pair outcomes deterministically. -- Outcome: - - Planning hygiene: execution-plan “Today’s Intent” + “Next Up” refreshed to match GitHub state. - - #206 started with a minimal spec note + litmus tests (commuting vs conflicting vs overlapping-but-safe). -- Evidence: - - `docs/spec/SPEC-0003-dpo-concurrency-litmus-v0.md` - - `crates/warp-core/tests/dpo_concurrency_litmus.rs` - -> 2026-01-03 — Merge-train: oldest open PRs (#220 → #227 → #242) (DONE) - -- Outcome: no open PRs remained by the end of the merge-train; the last merges landed on `main` (see merge commit `8082bbb`). -- Next: treat subsequent work as issue-driven (one branch per issue), starting with #206. - -> 2026-01-03 — PR #213: merge `origin/main` + resolve review feedback (DONE) - -- Goal: land PR #213 cleanly by merging the latest `origin/main`, resolving conflicts deterministically, and addressing all review comments. -- Scope: - - Merge `origin/main` into the PR branch (no rebase) and resolve conflicts. - - Pull PR comments/review threads and implement requested fixes (docs + tooling as needed). - - Verify `pnpm docs:build` is green before pushing. -- Exit criteria: conflicts resolved, review feedback addressed, and the updated branch pushed to the PR remote. - -> 2026-01-02 — Docs consolidation: scheduler doc map (issue #210) (IN PROGRESS) - -- Goal: reduce confusion between the implemented `warp-core` rewrite scheduler and the planned Echo ECS/system scheduler. -- Scope: - - Add a scheduler landing doc (`docs/scheduler.md`) that maps “which doc should I read?” - - Update scheduler docs to clearly label scope/status and link back to the landing page. - - Merge the `reserve()` validation/complexity satellites into a single canonical warp-core scheduler doc (`docs/scheduler-warp-core.md`) and leave redirects behind. - - Consolidate scheduler benchmark notes into a single canonical warp-core performance doc (`docs/scheduler-performance-warp-core.md`) and leave a redirect behind. - - Keep changes documentation-only. -- Exit criteria: scheduler docs are self-consistent and discoverable from `docs/docs-index.md`. - -> 2026-01-02 — Dependency DAG sketches (issues + milestones) (IN PROGRESS) - -- Goal: produce a durable “do X before Y” visual map across a subset of open GitHub Issues + Milestones so we can sequence work intentionally (especially around TT0/TT1/TT2/TT3 and S1 dependencies). -- Scope: - - Add confidence-styled dependency graphs (DOT sources + rendered SVGs) under `docs/assets/dags/`. - - Add a small explainer doc (`docs/dependency-dags.md`) that defines edge direction + confidence styling and links the rendered artifacts. - - Add a repo generator (`scripts/generate-dependency-dags.js`) plus a config file (`docs/assets/dags/deps-config.json`) so the diagrams can be regenerated and extended deterministically. - - Expose the generator via `cargo xtask` for a consistent repo tooling entrypoint. - - Add a scheduled GitHub Action that refreshes the DAGs (PR only if outputs change). - - Add `docs/workflows.md` and link it from README + AGENTS so contributors can discover the official entrypoints (`make`, `cargo xtask`, CI automation). -- Keep the diagrams explicitly “planning sketches” (not a replacement for GitHub Project state or native dependency edges). -- Exit criteria: both DAGs render locally via Graphviz (`dot -Tsvg …`) and the doc index links to `docs/dependency-dags.md`. - -> 2026-01-03 — PR #178: TT0 TimeStreams + wormholes spec lock (IN PROGRESS) - -- Goal: merge `origin/main` into `echo/time-streams-wormholes-166`, resolve review feedback, and push updates to unblock CodeRabbit re-review. -- Scope: - - Merge `origin/main` into the branch (no rebase). - - Address all actionable CodeRabbit review items (correctness + doc lint). - - Ensure all “explicitly deferred” sections are linked to tracking issues. -- Exit criteria: actionable review list is empty and the branch pushes cleanly. - -> 2026-01-02 — Issue #219: enforce CodeRabbit approval as a merge gate (IN PROGRESS) - -> 2026-01-03 — Issue #248: remove the CodeRabbit approval-gate CI workflow (DONE) - -- Goal: remove the GitHub Actions merge gate workflow that fails before CodeRabbit has had a chance to review, leaving stale red checks even after approval. -- Scope: - - Remove the workflow file from `.github/workflows/`. - - Treat “wait for CodeRabbit approval” as a procedural requirement (human + bot workflow), not a status-check requirement that can race the bot. -- Exit criteria: the approval gate workflow is removed and docs no longer reference it. -- Tracking: GitHub issue #248. - -> 2026-01-02 — Issue #219: enforce CodeRabbit approval as a merge gate (DEPRECATED) - -- Outcome: the merge-gate workflow approach proved noisy in practice (it commonly runs before CodeRabbit can review and can leave stale failures). It was removed under issue #248. -- Replacement: keep CodeRabbit approval as a “merge only when approved” policy, enforced socially and via review procedures (`docs/procedures/PR-SUBMISSION-REVIEW-LOOP.md`). - -> 2026-01-02 — Docs audit: purge/merge/splurge pass (IN PROGRESS) - -- Goal: audit Echo docs for staleness and overlap, then decide which docs should be purged, merged, or expanded (starting with `docs/math-validation-plan.md`). -- Scope: - - Refresh `docs/math-validation-plan.md` to match the current deterministic math implementation and CI coverage. - - Produce a short audit memo listing candidate docs to purge/merge/splurge with rationale. - - Fix a concrete dead-link cluster by making the collision tour build-visible (`docs/public/collision-dpo-tour.html`) and adding a `docs/spec-geom-collision.md` stub. - - Keep changes single-purpose: documentation only (no runtime changes). -- Exit criteria: audit memo committed + updated math validation plan; PR opened (tracked under issue #208). - -> 2026-01-02 — Issue #214: strict Origin allowlist semantics (IN PROGRESS) - -- Goal: keep `echo-session-ws-gateway`’s `--allow-origin` behavior strict and make that policy obvious to operators and contributors. -- Scope: - - Document “strict allowlist” behavior in `crates/echo-session-ws-gateway/README.md` (missing `Origin` is rejected when `--allow-origin` is configured). - - Add a unit test in `crates/echo-session-ws-gateway/src/main.rs` that locks the behavior (missing `Origin` rejected only when allowlist is present). -- Exit criteria: issue #214 is closed by a small PR; `cargo test -p echo-session-ws-gateway` is green. -- Tracking: GitHub issue #214. - -> 2026-01-02 — Issue #215: CI Playwright dashboard smoke job (IN PROGRESS) - -- Goal: add a GitHub Actions job that runs the Playwright Session Dashboard smoke test on PRs/pushes and publishes artifacts so embedded-tooling regressions can’t silently land. -- Scope: - - Extend `.github/workflows/ci.yml` with a Playwright job that: - - installs Node + pnpm (pinned to `package.json`), - - installs Chromium via `playwright install`, - - runs `pnpm exec playwright test e2e/session-dashboard.spec.ts`, - - uploads `playwright-report/` and `test-results/` as artifacts (even on failure). - - Keep `ECHO_CAPTURE_DASHBOARD_SCREENSHOT` disabled in CI (CI should not mutate tracked files); rely on Playwright attachments/artifacts for visual inspection. -- Exit criteria: PR is open + green (new CI job passes); artifacts are present on failure; `ci.yml` change is documented in `docs/decision-log.md`. -- Tracking: GitHub issue #215. - -> 2026-01-02 — PR triage pipeline: start with PR #179 (IN PROGRESS) - -- Goal: sync to PR #179 and validate the tooling for pulling PR review comments and extracting actionable issues (including human reviewer notes), so we can systematically close review feedback across the open PR queue and merge cleanly once approved. -- Scope: - - Checkout PR #179’s branch locally. - - Identify where the tool pulls PR comments from (GitHub API / `gh` CLI / local refs) and what comment types it includes (issue comments, review comments, review summaries). - - Ensure the report is attributable (comment author is included) so non-CodeRabbit actionables are not lost. - - Ensure “✅ Addressed in commit …” ack markers cannot be spoofed by templated bot text (require a human-authored ack with a real PR commit SHA). - - Run the tool against at least one PR to confirm output format and any required auth/config. -- Exit criteria: documented “how to run” steps for the tool; confidence that we can repeatably extract issues from PR comments for subsequent PRs. - -> 2026-01-02 — Issue #228: PR review ack “one comment per round” (IN PROGRESS) - -- Goal: stop notification floods during PR burn-down by acknowledging a whole fix batch with **one** PR timeline comment (per round), instead of replying on every review thread. -- Scope: - - Extend `.github/scripts/extract-actionable-comments.sh` to treat a single PR conversation “round ack” comment as acks for multiple review-thread items (`discussion_r`). - - Keep ack validation deterministic: require human authorship and a commit SHA that exists in the PR. - - Update `docs/procedures/*` so the default loop is “fix batch → push → post one round ack comment”. -- Exit criteria: extractor recognizes round acks; procedures updated; CI green. - -> 2026-01-02 — Issue #177: deterministic trig audit oracle + pinned error budgets (IN PROGRESS) - -- Goal: un-ignore the trig “error budget” test by replacing its platform-libm reference with a deterministic oracle, then pin explicit accuracy thresholds so CI can catch regressions in the LUT-backed trig backend. -- Scope: - - Use a pure-Rust oracle (`libm`) so the reference is not host libc/libm dependent. - - Measure both: - - absolute error vs the f64 oracle (robust near zero), - - ULP distance vs the f32-rounded oracle (applied only when |ref| ≥ 0.25 so ULPs remain meaningful). - - Remove the repo-root scratchpad `freaky_numbers.rs` if it is not used by any crate/tests. -- Exit criteria: `cargo test -p warp-core --test deterministic_sin_cos_tests` is green with the audit test enabled by default; budgets are documented in `docs/decision-log.md`. -- Tracking: GitHub issue #177. - -> 2026-01-01 — Issue #180: Paper VI notes + capability matrix (IN PROGRESS) - -- Goal: turn “Pulse” time/determinism/tooling insights into durable artifacts (Paper VI notes + a crisp ownership matrix for Echo). -- Scope: - - Add `docs/capability-ownership-matrix.md` (template + first pass). - - Extend Paper VI notes in `aion-paper-06` (HostTime/HistoryTime, decision records, multi-clock streams, replay integrity hooks). -- Exit criteria: matrix + notes are concrete enough to guide near-term implementation choices and future tool UX. -- Tracking: GitHub issue #180. - -> 2026-01-01 — PR hygiene: standardize CodeRabbitAI review triage (COMPLETED) - -- Goal: make CodeRabbitAI review loops cheap and unambiguous by codifying how we extract actionable comments from the current PR head diff. -- Scope: - - Add mandatory procedures under `docs/procedures/` for PR submission and review comment extraction. - - Add a helper script `.github/scripts/extract-actionable-comments.sh` to automate review comment bucketing and produce a Markdown report. -- Exit criteria: a contributor can run one command and get a clean actionables list without re-reading the entire PR history. - -> 2026-01-01 — T2 (#168): embedded session dashboard baseline (COMPLETED) - -- Goal: keep the “run a binary, open a page” dashboard workflow stable while standardizing styling and keeping docs screenshots honest. -- Scope: - - Serve a static dashboard from `echo-session-ws-gateway` (`/dashboard`) plus `/api/metrics`. - - Vendor Open Props CSS into the gateway and serve it under `/vendor/*.css` for offline use. - - Add Playwright smoke tests that exercise the dashboard and optionally regenerate the screenshot used in `docs/guide/wvp-demo.md`. -- Exit criteria: `cargo clippy -p echo-session-ws-gateway --all-targets -- -D warnings` green; `pnpm exec playwright test` green; updated screenshot checked in. -- Evidence: - - PR #176 (session dashboard + Playwright smoke + screenshot regen) - - Dashboard: `crates/echo-session-ws-gateway/assets/dashboard.html` - - Routes: `crates/echo-session-ws-gateway/src/main.rs` - - e2e: `e2e/session-dashboard.spec.ts` - - Docs screenshot: `docs/assets/wvp/session-dashboard.png` - -> 2026-01-01 — T2 (#168): make dashboard smoke tests self-contained (COMPLETED) - -- Goal: ensure the Playwright “Session Dashboard” smoke test can build and run all required binaries from a clean checkout. -- Scope: - - Add a tiny `echo-session-client` example (`publish_pulse`) used by the e2e test to generate deterministic, gapless snapshot+diff traffic. -- Exit criteria: `pnpm exec playwright test` no longer depends on local stashes / untracked artifacts. -- Evidence: - - `pnpm exec playwright test e2e/session-dashboard.spec.ts` (green) - -> 2026-01-01 — Issue #169: harden WVP demo with loopback tests (COMPLETED) - -- Goal: prevent WVP demo regressions by pinning protocol invariants (snapshot-first, gapless epochs, authority enforcement) in automated tests. -- Scope: - - Add a `UnixStream::pair()` loopback test for `echo-session-service` that exercises handshake, subscribe, and publish error cases without binding a real UDS path. - - Add `warp-viewer` unit tests for publish gating and publish-state transitions (snapshot-first, epoch monotonicity, pending ops clearing). -- Exit criteria: `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green; tests document the demo invariants. -- Tracking: GitHub issue #169. -- Evidence: - - PR #175 (loopback tests + publish behavior pinned; follow-up hardening for defensive test checks) - -> 2026-01-01 — PR #167: deterministic math follow-ups + merge `main` (COMPLETED) - -- Goal: address all CodeRabbit review comments on PR #167 with minimal churn, keep the PR tightly scoped to deterministic math + warp-core motion payload work, and restore mergeability by merging `main` and resolving docs guard conflicts. -- Scope: - - Resolve merge conflicts from `origin/main` in `docs/decision-log.md` and `docs/execution-plan.md` while preserving both the WVP hardening timeline and the deterministic math timeline. - - Keep deterministic trig guardrails stable (`scripts/check_no_raw_trig.sh`) so raw platform trig calls cannot sneak back into runtime math code. -- Exit criteria: `cargo test -p warp-core` and `cargo test -p warp-core --features det_fixed` are green; `cargo clippy -p warp-core --all-targets -- -D warnings -D missing_docs` is green; PR is mergeable and CI stays green. -- Evidence: merged to `main` as PR #167 (merge commit `54d7626`; closes #165). - -> 2026-01-01 — Motion payload v2 (Q32.32) + `Scalar` port (COMPLETED) - -- Goal: move the motion demo payload to a deterministic Q32.32 fixed-point encoding (v2) while preserving decode compatibility for the legacy v0 `f32` payload; port the motion executor to use the `Scalar` abstraction and upgrade v0 payloads to v2 on write. -- Evidence: `cargo test -p warp-core` green; `cargo test -p warp-core --features det_fixed` green; `cargo clippy -p warp-core --all-targets -- -D warnings -D missing_docs` green; `cargo clippy -p warp-core --all-targets --features det_fixed -- -D warnings -D missing_docs` green. - -> 2026-01-01 — Deterministic fixed-point lane (`DFix64`) + CI coverage (COMPLETED) - -- Goal: land a deterministic fixed-point scalar backend (`DFix64`, Q32.32) behind a `det_fixed` feature flag, add a dedicated test suite, and extend CI with explicit `--features det_fixed` lanes (including MUSL) so we continuously exercise cross-platform behavior. -- Evidence: commit `57d2ec3` plus the above motion work continues to validate the det_fixed lane in CI. - -> 2026-01-01 — Implement deterministic `F32Scalar` trig (COMPLETED) - -- Goal: replace `F32Scalar::{sin,cos,sin_cos}`’s platform transcendentals with a deterministic LUT-backed backend, check in the LUT, and promote the existing trig test scaffold into a cross-platform golden-vector suite. -- Evidence: `cargo test -p warp-core` green; `cargo test -p warp-core --test deterministic_sin_cos_tests` green (error-budget audit test remains `#[ignore]`); `cargo clippy -p warp-core --all-targets -- -D warnings -D missing_docs` green. - -> 2025-12-30 — Branch maintenance: resurrect `F32Scalar/sin-cos` (COMPLETED) - -- Goal: merge `main` into the legacy deterministic trig test branch, resolve the `rmg-core`→`warp-core` rename conflict, and leave the WIP test compiling (ignored by default). -- Evidence: merge commit `6cfa64d` (“Merge branch 'main' into F32Scalar/sin-cos”); `cargo test -p warp-core --test deterministic_sin_cos_tests` passes (ignored test remains opt-in). - -> 2025-12-30 — Issue #163: WVP demo path (IN PROGRESS) - -- Goal: complete the WARP View Protocol demo path (publisher + subscriber) by adding outbound publish support to `echo-session-client` and wiring publish/subscribe toggles + a dirty publish loop in `warp-viewer`. -- Scope: - - `echo-session-client`: bidirectional tool connection (receive + publish `warp_stream`). - - `warp-viewer`: publish/subscribe toggles, deterministic local mutation to generate diffs, gapless epoch publish, surface hub errors as notifications/toasts. - - Review follow-ups (PR #164): resolve CodeRabbit/Codex actionables (rustdoc coverage, reconnect snapshot reset, and no-silent-encode-failure logging). - - Docs: update `docs/tasks.md` and add a short “two viewers + hub” demo walkthrough. -- Exit criteria: `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green; demo is reproducible locally; PR opened. -- Tracking: GitHub issue #163. - -> 2025-12-30 — PR #162: Address CodeRabbit doc nits (COMPLETED) - -- Goal: close out CodeRabbit review comments on the THEORY doc with minimal churn. -- Scope: - - Tighten small prose redundancies in `docs/THEORY.md` (wordiness/repetition). - - Double-check markdownlint-sensitive formatting (no trailing whitespace; headings surrounded by blank lines). -- Exit criteria: CodeRabbit review comments are resolved; hooks remain green. -- Evidence: - - Commit: `22ba855` (THEORY prose tightening + session logging) - -> 2025-12-30 — THEORY.md paper alignment callouts (COMPLETED) - -- Goal: annotate the local THEORY paraphrase so readers can see where Echo intentionally diverges from the published papers. -- Scope: - - Add `[!note]` callouts explaining “Echo does this differently … because …” where implementation differs. - - Commit `docs/THEORY.md` + the `assets/echo-white-radial.svg` brand asset; remove the local-only `assets/readme/` image scratchpad. -- Exit criteria: THEORY doc is self-aware (differences + rationale); files pass hooks (SPDX guard, fmt/clippy/tests). -- Evidence: - - Commit: `c029c82` (`docs/THEORY.md` + `assets/echo-white-radial.svg`) - -> 2025-12-30 — Post-merge housekeeping: sync roadmap/checklists (COMPLETED) - -- Goal: keep the living roadmap accurate now that WARP Stage B1 landed and merged. -- Scope: - - Mark completed items in the “Immediate Backlog” that are now delivered in `main`. - - Remove/replace references to non-existent paths (e.g. `packages/…`) with current Rust workspace paths. - - Update `docs/docs-index.md` “Getting Started” so it points at the current WARP entry path (`docs/guide/warp-primer.md`). -- Exit criteria: roadmap reflects current repo structure; no “(IN PROGRESS)” entries for merged work. -- Evidence: - - Commit: `a0e908a` (roadmap housekeeping) - -> 2025-12-30 — PR #159: Address CodeRabbit actionables (COMPLETED) - -- Goal: close the remaining review items on PR #159 (docs + demo hygiene). -- Scope: - - Add Evidence blocks (commit SHAs) for completed 2025-12-30 entries below. - - Clarify “no hidden edges” enforcement references in `docs/guide/warp-primer.md`. - - Clarify Paper I notation context (`U`, `π(U)`) in `docs/spec-warp-core.md`. - - Minor demo hygiene: document init/update behavior in the port demo executor. -- Exit criteria: `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green; CodeRabbit re-review clean. -- Evidence: - - Merge: `f2d6a68` (PR #159 merged to `main`) - -> 2025-12-30 — Add WARP primer + wire into “Start here” (COMPLETED) - -- Goal: make WARP approachable to newcomers and pin canonical “start here” docs order. -- Scope: - - Add `docs/guide/warp-primer.md`. - - Link it as step 1 from README + `docs/spec-warp-core.md`. - - Keep formatting markdownlint-friendly (esp. MD022 heading spacing). -- Exit criteria: docs read cleanly; CodeRabbit markdown nits avoided. -- Evidence: - - Commit: `1b40f66` (docs primer + links) - - Docs: `docs/guide/warp-primer.md`, `docs/spec-warp-core.md` - - README: `README.md` (Start here list) - -> 2025-12-30 — PR #159: Address CodeRabbit majors (warp-core tour examples) (COMPLETED) - -- Goal: close remaining “Major” review gaps by making Stage B1 behavior concrete and easy to adopt. -- Scope: - - Expand `docs/spec-warp-core.md` with a worked Stage B1 example showing: - - portal authoring (`OpenPortal`) + `Descend(WarpId)` semantics, - - `Engine::apply_in_warp` usage with a `descent_stack`, - - how descent-chain reads become `Footprint.a_read` and therefore `in_slots`, - - Paper III worldline slicing that includes the portal chain. - - Add a minimal `Engine` constructor for initializing from an existing `WarpState` (needed to make examples concrete without exposing internals). - - Close remaining review gaps on portal invariants by enforcing and testing: - - no dangling `Descend(WarpId)` portals without a corresponding `WarpInstance`, - - no orphan instances (`WarpInstance.parent` must be realized by an attachment slot). -- Exit criteria: `cargo fmt --all`, `cargo test --workspace`, and `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green. -- Evidence: - - Commits: `875690c`, `7a02123`, `846723d`, `27e490a` - - Docs: `docs/spec-warp-core.md` (Stage B1 quickstart + worked examples) - - Implementation: `crates/warp-core/src/engine_impl.rs`, `crates/warp-core/src/tick_patch.rs` - - Tests: portal invariant tests in `crates/warp-core/src/tick_patch.rs` - -> 2025-12-30 — Touch-ups (receipts/docs + wasm/ffi ergonomics + graph delete perf) (COMPLETED) - -- Goal: address follow-up review nits and a concrete graph-store performance trap without changing deterministic semantics. -- Scope: - - Clarify `TickReceiptEntry.scope` semantics (it is a `NodeKey`, not a `NodeId`) and keep receipt digest encoding stable. - - Make the WASM boundary less opaque: replace `ok()??` with explicit matches; log spawn failures behind `console-panic` and return a clear sentinel (`Uint8Array` length 0). - - Add `Engine::insert_node_with_attachment` for atomic bootstrapping of demo nodes (avoids partial init if an invariant is violated). - - Eliminate `delete_node_cascade`’s `O(total_edges)` inbound scan by maintaining a reverse inbound index (`edges_to`) plus `EdgeId -> to` index. - - Clarify `docs/spec-warp-tick-patch.md` that the section 1.2 op list is semantic and does not imply encoding tag order. -- Exit criteria: `cargo fmt --all`, `cargo test --workspace`, and `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green. -- Evidence: - - Commits: `8282a29`, `ef89bc7`, `a070374`, `31e0836`, `889932d` - - Implementation: `crates/warp-core/src/receipt.rs`, `crates/warp-wasm/src/lib.rs`, `crates/warp-ffi/src/lib.rs`, `crates/warp-core/src/graph.rs` - - Docs: `docs/spec-warp-tick-patch.md` - -> 2025-12-30 — Touch-ups: encapsulation + ergonomics + micro-perf (COMPLETED) - -- Goal: address review touch-ups to keep public APIs stable, avoid internal field coupling, and remove sharp edges in docs/tests/benchmarks. -- Scope: - - Clarify attachment-plane terminology in `docs/warp-two-plane-law.md`. - - Improve worldline slice work-queue allocation (`Vec::with_capacity`) for large patch sets. - - Add small encapsulation helpers (`GraphStore::has_edge`, `WarpId::as_bytes`, `NodeId::as_bytes`) and update callers to avoid `.0` tuple-field indexing. - - Tighten `Engine` accessors: distinguish “unknown warp store” vs “missing node/attachment” via `Result, EngineError>`. - - Make root-store mutation methods (`insert_node`, `set_node_attachment`) return `Result` and surface invariant violation (`UnknownWarp`) rather than silently dropping writes. - - Improve diagnostics in motion benchmarks’ panic paths. -- Exit criteria: `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green; docs guard updated. -- Evidence: - - Commits: `31e0836`, `889932d` - - Implementation: `crates/warp-core/src/engine_impl.rs`, `crates/warp-core/src/graph.rs`, `crates/warp-core/src/tick_patch.rs`, `crates/warp-core/src/ident.rs` - - Benches: `crates/warp-benches/benches/motion_throughput.rs` - -> 2025-12-30 — Stage B1.1: Atomic portals + merge/DAG slicing semantics (COMPLETED) - -- Goal: make descended attachments “slice-safe” by introducing an atomic portal authoring op (`OpenPortal`), then lock down merge semantics and terminology to prevent long-term drift. -- Scope: - - Add `WarpOp::OpenPortal { key, child_warp, child_root, init }` as the canonical portal authoring operation. - - Update patch replay validation to forbid dangling portals / orphan instances. - - Update `diff_state` to emit `OpenPortal` for new descended instances, preventing portal/instance creation from being separable across ticks. - - Document merge semantics (explicit conflict resolution) and DAG slicing algorithm. - - Add a terminology law doc to pin “instance zoom vs wormholes”. -- Exit criteria: `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green; docs guard updated. -- Evidence: - - Implementation: - - `crates/warp-core/src/tick_patch.rs` (`WarpOp::OpenPortal`, replay validation, diff_state portal canonicalization) - - Docs: - - `docs/adr/ADR-0002-warp-instances-descended-attachments.md` (atomic portals + merge law) - - `docs/spec/SPEC-0002-descended-attachments-v1.md` (OpenPortal + merge/DAG slicing + zoom tooling note) - - `docs/spec-warp-tick-patch.md` (OpenPortal op encoding) - - `docs/architecture/TERMS_WARP_STATE_INSTANCES_PORTALS_WORMHOLES.md` (terminology law) - -> 2025-12-30 — Stage B1: WarpInstances + descended attachments (COMPLETED) - -- Goal: implement “WARPs all the way down” without recursive traversal in the rewrite hot path by modeling descent as flattened indirection (WarpInstances). -- Scope: - - Introduce WarpInstances: - - `WarpId` - - `WarpInstance { warp_id, root_node, parent: Option }` - - Make ids instance-scoped: - - `NodeKey { warp_id, local_id }` - - `EdgeKey { warp_id, local_id }` - - Upgrade attachments from depth-0 atoms to `AttachmentValue = Atom(AtomPayload) | Descend(WarpId)` and make attachment slots first-class: - - `AttachmentKey { owner: NodeKey|EdgeKey, plane: Alpha|Beta }` - - `SlotId::Attachment(AttachmentKey)` (tick patches + slicing) - - Enforce the Paper I/II “no hidden edges” and descent-chain correctness laws: - - Matching/indexing stays skeleton-only within an instance. - - Any match/exec within a descended instance must READ every `AttachmentKey` in the descent stack (so changing a descent pointer deterministically invalidates the match). - - Slicing integration: a demanded value in instance `W` must pull in the attachment chain (root → W) producers via `SetAttachment(...Descend...)` ops, with no decoding of atoms. -- Exit criteria: `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green; new ADR + SPEC for Stage B1; decision log updated. -- Evidence: - - Implementation: - - `crates/warp-core/src/warp_state.rs` (WarpState/WarpInstance) - - `crates/warp-core/src/attachment.rs` (AttachmentKey/Value incl. `Descend`) - - `crates/warp-core/src/snapshot.rs` (state_root reachability across instances via `Descend`) - - `crates/warp-core/src/engine_impl.rs` (apply_in_warp + descent_chain reads) - - `crates/warp-core/src/footprint.rs` + `crates/warp-core/src/scheduler.rs` (attachment conflicts) - - `crates/warp-core/src/tick_patch.rs` (SlotId::Attachment, instance ops, patch_digest v2) - - Docs: - - `docs/adr/ADR-0002-warp-instances-descended-attachments.md` - - `docs/spec/SPEC-0002-descended-attachments-v1.md` - - `docs/spec-warp-tick-patch.md` (updated to v2 encoding) - - `docs/spec-merkle-commit.md` (updated state_root encoding) - - `docs/warp-two-plane-law.md` (updated to reflect B1 reality) - - Tests: - - `crates/warp-core/src/tick_patch.rs` (portal-chain slice test) - - `crates/warp-core/src/scheduler.rs` (descent-chain conflict test) - -> 2025-12-29 — WARP two-plane semantics: typed atom attachments (COMPLETED) - -- Goal: align Echo’s `warp-core` implementation with Paper I/II “two-plane” semantics without slowing the rewrite hot path. -- Scope: - - Treat `GraphStore` as the **SkeletonGraph** (π(U)): the structure used for matching, rewriting, scheduling, slicing, and hashing. - - Model attachment-plane payloads as **typed atoms** (depth-0): `AtomPayload { type_id: TypeId, bytes: Bytes }`. - - Update snapshot hashing + tick patch canonical encoding so payload `type_id` participates in digests (no “same bytes, different meaning” collisions). - - Introduce a minimal codec boundary (`Codec` + registry concept) for typed decode/encode at rule/view boundaries; core matching/indexing remains skeleton-only unless a rule explicitly decodes. - - Document the project laws (“no hidden edges in payload bytes”, “skeleton rewrites never decode attachments”) and record the decision in ADR + SPEC form. -- Exit criteria: `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green; docs guard updated (`docs/decision-log.md` + new ADR/SPEC + law doc). -- Evidence: - - Implementation: `crates/warp-core/src/attachment.rs`, `crates/warp-core/src/record.rs`, `crates/warp-core/src/snapshot.rs`, `crates/warp-core/src/tick_patch.rs`. - - Docs: `docs/warp-two-plane-law.md`, `docs/adr/ADR-0001-warp-two-plane-skeleton-and-attachments.md`, `docs/spec/SPEC-0001-attachment-plane-v0-atoms.md`. - - Tests: new digest/type-id assertions in `crates/warp-core/tests/atom_payload_digest_tests.rs`; workspace tests + clippy rerun green. - -> 2025-12-29 — Follow-up: tick patch hygiene (COMPLETED) - -- Goal: clean up `tick_patch` sharp edges so the patch boundary stays deterministic, well-documented, and resistant to misuse. -- Scope: - - `diff_store`: avoid double map lookups and expand rustdoc (intent, invariants, semantics, edge cases, perf). - - `TickPatchError`: switch to `thiserror` derive (remove boilerplate). - - `encode_ops`: document that digest tag bytes are distinct from replay sort ordering. - - `WarpTickPatchV1::new`: dedupe duplicate ops by sort key to avoid replay errors. - - `Hash` naming: alias `crate::ident::Hash` to `ContentHash` to avoid confusion with `derive(Hash)`. -- Exit criteria: `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green. -- Evidence: Issue `#156` / PR `#157` (commits `793322f`, `615b9e9`, `5e1e502`) — `crates/warp-core/src/tick_patch.rs` implements the changes; tests/clippy rerun green. - -> 2025-12-29 — Follow-up: `EdgeRecord` equality (COMPLETED) - -- Goal: remove the ad-hoc `edge_record_eq` helper so `EdgeRecord` equality is defined by the type, not by duplicated helper logic in `tick_patch`. -- Scope: derive `PartialEq` + `Eq` on `EdgeRecord`; replace `edge_record_eq(a, b)` call sites with idiomatic `a == b`; delete the helper from `tick_patch.rs`. -- Exit criteria: `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green. -- Evidence: `EdgeRecord` derives `PartialEq, Eq` and `tick_patch` uses `==` directly; helper removed; tests/clippy rerun green. - -> 2025-12-29 — Follow-ups: policy_id plumbing + edge replay index (COMPLETED) - -- Goal: eliminate “TODO-vibes” follow-ups by making policy id handling explicit/configurable and by removing the O(total_edges) edge scan from tick patch replay. -- Scope: thread `policy_id` through `Engine` as an explicit engine parameter (defaulting to `POLICY_ID_NO_POLICY_V0`); add a `GraphStore` reverse index (`EdgeId -> from`) to support O(bucket) edge migration/removal during patch replay; keep commit hash semantics unchanged (still commits to `state_root` + `patch_digest` + policy id). -- Exit criteria: `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` green; no remaining local-only scope hash clones; no edge replay full-scan. -- Evidence: `Engine::{with_policy_id, with_scheduler_and_policy_id}` added; tick patch replay now uses `GraphStore` reverse index; strict clippy/tests gates rerun green. - -> 2025-12-29 — Tick receipts: blocking causality (COMPLETED) - -- Goal: finish the Paper II tick receipt slice by recording *blocking causality* for rejected candidates (a poset edge list) in `warp-core`. -- Scope: extend `TickReceipt` to expose the applied candidates that blocked a `Rejected(FootprintConflict)` entry; keep `decision_digest` stable (digest commits only to accept/reject outcomes, not blocker metadata). -- Exit criteria: new tests cover multi-blocker cases; `cargo test --workspace` and `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` are green; bridge docs updated to match implementation; decision log entry recorded. -- Evidence: implemented `TickReceipt::blocked_by` and blocker attribution in `Engine::commit_with_receipt`; added multi-blocker tests; updated `docs/aion-papers-bridge.md` and `docs/spec-merkle-commit.md`; validated via `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs`. - -> 2025-12-29 — Delta tick patches + commit hash v2 (COMPLETED) - -- Goal: implement Paper III-aligned **delta tick patches** (`WarpTickPatchV1`) in `warp-core` and switch commit hashing to **v2** so `commit_id` commits only to the replayable delta (`patch_digest`). -- Scope: define canonical patch ops + slot sets (`in_slots`/`out_slots` as *unversioned* slots); compute `patch_digest` from canonical patch encoding; update `Snapshot` and commit hashing so v2 commits to `(parents, state_root, patch_digest, policy_id, version)` and treats planner/scheduler digests + receipts as diagnostics only. -- Exit criteria: new spec doc for `WarpTickPatchV1` + commit hash v2; tests cover deterministic patch generation + patch replay; `cargo test --workspace` and `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` are green; decision log entry recorded. -- Evidence: added `docs/spec-warp-tick-patch.md`; upgraded `docs/spec-merkle-commit.md` to v2; implemented `crates/warp-core/src/tick_patch.rs` + wired patch generation into `Engine::commit_with_receipt`; added patch replay test (`Engine` unit test) + updated receipt tests; validated via `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs`. - -> 2025-12-28 — Promote AIΩN bridge doc + add tick receipts (COMPLETED) - -- Goal: promote the AIΩN Foundations ↔ Echo bridge from a dated note into a canonical doc, then implement Paper II “tick receipts” in `warp-core`. -- Scope: move the bridge into `docs/` (keep a stub for historical links); add `TickReceipt` + `Engine::commit_with_receipt`; commit receipt outcomes via `decision_digest`; update `docs/spec-merkle-commit.md` to define the receipt digest encoding. -- Exit criteria: `cargo test --workspace` and `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` are green; the bridge is indexed in `docs/docs-index.md`; the decision log records rationale. -- Evidence: added `docs/aion-papers-bridge.md` and indexed it; implemented `crates/warp-core/src/receipt.rs` + `Engine::commit_with_receipt`; updated `docs/spec-merkle-commit.md`; validated via `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs`. - -> 2025-12-28 — WARP rename + AIΩN docs sanity pass (COMPLETED) - -- Goal: confirm the WARP-first terminology sweep and AIΩN linkage are consistent and build-clean after the rename. -- Scope: rerun `cargo test --workspace` and `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs`; verify README links and ensure the only remaining `rmg_*` mentions are explicit compatibility/historical references. -- Exit criteria: tests + clippy green; no stray “RMG / recursive metagraph” naming remains in code/docs beyond the bridge note and wire-compat aliases. -- Evidence: workspace tests + clippy rerun green; wire decoder keeps a short transition window for `subscribe_rmg`/`rmg_stream` and `rmg_id` aliases (documented in `docs/spec-warp-view-protocol.md`). - -> 2025-12-28 — Repo survey + briefing capture (COMPLETED) - -- Goal: refresh a full-stack mental model of Echo as it exists *today* (Rust workspace + session tooling) and capture a durable briefing for future work. -- Scope: read architecture + determinism specs; map crate boundaries; trace the rewrite/commit hashing pipeline and the session wire/protocol pipeline; review AIΩN/WARP paper sources and map them onto the current repo; note any doc/code drift. -- Exit criteria: publish a concise repo map + invariants note in `docs/notes/` and repair any obvious spec drift discovered during the survey. -- Evidence: added `docs/notes/project-tour-2025-12-28.md` and `docs/notes/aion-papers-bridge.md`; corrected the canonical empty digest semantics in `docs/spec-merkle-commit.md` to match `warp-core`; refreshed `README.md` to link the AIΩN Framework repo + Foundations series. - -> 2025-12-28 — RMG → WARP rename sweep (COMPLETED) - -- Goal: eliminate the legacy “RMG / recursive metagraph” naming drift by renaming the workspace to WARP-first terminology. -- Scope: rename crates (`rmg-*` → `warp-*`), update Rust identifiers (`Rmg*` → `Warp*`), align the session proto/service/client/viewer, and sweep docs/specs/book text to match. -- Exit criteria: `cargo test --workspace` passes; the only remaining `rmg_*` strings are explicit compatibility aliases and historical bridge-note references. -- Evidence: workspace builds and tests pass under WARP naming; the wire decoder accepts legacy `subscribe_rmg` / `rmg_stream` and `rmg_id` as transition aliases; docs/specs/book now describe WARP streams and WarpIds. - -> 2025-12-28 — PR #141 follow-up (new CodeRabbit nits @ `4469b9e`) (COMPLETED) - -- Goal: address the two newly posted CodeRabbit nitpicks on PR #141 (latest review on commit `4469b9e`). -- Scope: bucket new actionable threads; implement fixes (incl. rust-version guard + workspace package metadata inheritance) with tests where applicable; update burn-down index + decision log; reply on each thread with fix SHAs; land PR. -- Exit criteria: pre-push hooks green; `gh pr checks 141` green; explicit PR comment posted mapping issue IDs → fixing SHAs; PR merged. -- Evidence: rust-version guard now supports `rust-version.workspace = true` + avoids `sed` (`7e84b16`); Spec-000 rewrite inherits shared package metadata from `[workspace.package]` (`e4e5c19`). - -> 2025-12-28 — PR #141 follow-up (new CodeRabbit review @ `639235b`) (COMPLETED) - -- Goal: address newly posted CodeRabbit review comments on PR #141 (latest review on commit `639235b`), including any high-priority blockers. -- Scope: re-extract paginated PR comments; bucket new actionable threads; implement fixes with tests + doc alignment; update burn-down index + consolidated PR comment with fix SHAs. -- Exit criteria: `cargo test --workspace` + `cargo clippy --all-targets -- -D warnings -D missing_docs` green; PR checks green; explicit PR comment posted mapping issue IDs → fixing SHAs. -- Evidence: restore `"wasm"` categories in `84e63d3`; Spec-000 docs fixes in `922553f`; workspace dep cleanup in `dfa938a`; CI + rust-version guard hardening in `56a37f8`. - -> 2025-12-28 — PR #141 follow-up (new CodeRabbit review @ `b563359`) (COMPLETED) - -- Goal: address newly posted CodeRabbit review comments on PR #141 (including minor/nitpick) and repair any newly failing CI jobs. -- Scope: re-extract paginated PR comments; bucket by severity; implement fixes with tests + doc alignment; update burn-down index + consolidated PR comment with fix SHAs. -- Exit criteria: `cargo test` + `cargo clippy --all-targets -- -D warnings -D missing_docs` green; PR checks green; consolidated summary comment updated with fix SHAs. -- Evidence: MSRV standardization + CI guard in `0f8e95d`; workspace deps fixes in `150415b` + `2ee0a07`; audit ignore DRY in `3570069` + `e5954e4`; deny license justification in `3e5b52d`; remove `"wasm"` categories in `3ccaf47`; stale advisory ignore removed in `1bf90d3`; Makefile guard rails in `8db8ac6`; doc style tweaks in `82fce3f`. - -> 2025-12-28 — PR #141 follow-up (new CodeRabbit comments after `c8111ec`) (COMPLETED) - -- Goal: address newly posted CodeRabbit review comments on PR #141 (including minor/nitpick) and ship a clean follow-up push. -- Scope: re-extract paginated PR comments; bucket by severity; implement fixes with tests + doc alignment; update burn-down index + consolidated PR comment with fix SHAs. -- Exit criteria: `cargo test` + `cargo clippy --all-targets -- -D warnings -D missing_docs` green; PR checks green; consolidated summary comment updated with new SHAs. -- Evidence: task-list/CI hardening in `602ba1e`, SPDX policy alignment in `042ec2b`, follow-up nits in `5086881`, docs fixes in `6ee8811` + `a55e1e0`, deny justification in `17687f2`. - -> 2025-12-28 — PR #141 follow-up (new CodeRabbit round: Leptos bump + Rewrite semantics) (COMPLETED) - -- Goal: address newly posted PR #141 review comments (Leptos 0.8.15 bump + fix `Rewrite` semantics around `old_value`) and ship a clean follow-up push. -- Scope: re-extract review comments with pagination; implement fixes with tests + doc alignment; re-check CI and repair any failing jobs; post one consolidated PR summary comment with fix SHAs. -- Exit criteria: `cargo test` + `cargo clippy --all-targets -- -D warnings -D missing_docs` green; PR checks green; summary comment updated with new fix SHAs. -- Evidence: `Rewrite` semantics fix in `1f36f77`, Leptos bump in `1a0c870`, and the refreshed burn-down index in `docs/notes/pr-141-comment-burn-down.md`. - -> 2025-12-28 — PR #141 follow-up (new review comments + CI fixes) (COMPLETED) - -- Goal: resolve newly posted PR review comments on #141, fix failing CI jobs, and ship a clean follow-up push. -- Scope: re-extract review comments with pagination; bucket by severity; implement fixes with tests + docs; inspect the latest GitHub Actions run and repair failing jobs/workflows if needed; post one consolidated PR summary comment with fix SHAs. -- Exit criteria: PR checks green; new review comments addressed; `cargo test` + `cargo clippy --all-targets -- -D missing_docs` green; follow-up summary comment posted. -- Evidence: follow-up fixes landed in `46bc079` (see `docs/notes/pr-141-comment-burn-down.md`). - -> 2025-12-28 — PR #141 review comment burn-down (COMPLETED) - -- Goal: extract, bucket, and resolve every PR comment on #141 with tests, fixes, and doc alignment. -- Scope: use `gh` + API to enumerate review + issue comments; verify stale vs actionable; implement fixes with minimal deterministic surface changes; update `docs/decision-log.md` and any impacted specs. -- Exit criteria: `cargo test` + `cargo clippy --all-targets -- -D missing_docs` green; PR thread includes fix SHAs; branch is pushable. (See `docs/notes/pr-141-comment-burn-down.md` @ `933239a`, PR comment: ) - -> 2025-12-13 — WS gateway disconnect hygiene + Spec-000 WASM gating (COMPLETED) - -- Goal: keep `cargo build`/`cargo test` green for the host target while still supporting `trunk serve` (wasm32) builds. -- Scope: gate `spec-000-rewrite` WASM entry points correctly; ensure `echo-session-ws-gateway` closes WS + stops ping task when upstream UDS disconnects. -- Status: completed; Spec-000 entrypoint is wasm32-gated and the WS gateway now closes + cancels ping on upstream disconnect. (PR #141: commits `2fec335`, `970a4b5`) - -> 2025-12-11 — WebSocket gateway for session hub (COMPLETED) - -- Goal: allow browser clients to connect to the Unix-socket session bus via a secure WS bridge. -- Scope: new `echo-session-ws-gateway` crate with WS→UDS forwarding, frame guards, origin allowlist, optional TLS. -- Status: completed; gateway parses JS-ABI frame lengths, enforces 8 MiB cap, and proxies binary frames over WS. (PR #141: commit `785c14e`; hardening in `89c2bb1`) - -> 2025-12-11 — Scripting pivot to Rhai (COMPLETED) - -- Goal: cement Rhai as the scripting layer across design/docs, update scripting backlog items, and log the pivot. -- Scope: execution plan, decision log, scripting/spec docs, FFI descriptions. -- Status: completed; scripting plans now target Rhai with deterministic sandboxing, prior scripting references removed. (commit `30b3b82`) - -> 2025-12-11 — WARP authority enforcement (IMPLEMENTED; PENDING MERGE) - -- Goal: Reject non-owner publishes on WARP channels and surface explicit errors to clients. -- Scope: `echo-session-service` (producer lock + error frames), `echo-session-client` (map error frames to notifications), protocol tasks checklist. -- Status: implemented on branch `echo/warp-view-protocol-spec` (commit `237460e`); not yet merged to `main`. - -> 2025-12-10 — CI cargo-deny index failures (COMPLETED) - -- Goal: stop noisy `warning[index-failure]: unable to check for yanked crates` in GitHub Actions by ensuring `cargo-deny` has a warm crates.io index. -- Scope: `.github/workflows/ci.yml` deny job (prime cargo index before running `cargo deny`). -- Status: completed; deny job now runs `cargo fetch --locked` before `cargo deny`. - -> 2025-12-10 — CI cargo-audit unmaintained warnings (COMPLETED) - -- Goal: keep `cargo audit --deny warnings` green despite unavoidable unmaintained transitive `paste` (via wgpu) and legacy `serde_cbor` advisory. -- Scope: `.github/workflows/security-audit.yml` and `.github/workflows/ci.yml` (add `--ignore RUSTSEC-2024-0436` and `--ignore RUSTSEC-2021-0127`). -- Status: completed; audit steps now ignore these advisories explicitly until upstreams replace them. - -> 2025-12-10 — WARP View Protocol tasks (IN PROGRESS) - -- Goal: land the WARP View Protocol/EIP checklist and execute slices toward multi-viewer sharing demo. -- Scope: tracked in `docs/tasks.md` with stepwise commits as items complete. -- Status: checklist drafted. - -> 2025-12-10 — CBOR migration + viewer input gating (COMPLETED) - -- Goal: swap serde_cbor for maintained ciborium, harden canonical encoding/decoding, and keep viewer input/render stacks consistent. -- Scope: `crates/echo-session-proto` (ciborium + serde_value bridge, canonical encoder/decoder), `crates/echo-graph` (ciborium canonical bytes + non_exhaustive enums), `crates/warp-viewer` (egui patch alignment, input/app events/session_logic gating, hash mismatch desync), dependency lockfile. -- Status: completed; wire encoding now uses ciborium with checked integer handling and canonical ordering, graph hashing returns Result, viewer controls are gated to View screen with safer event handling and consistent egui versions. - -> 2025-12-10 — Session client framing & non-blocking polling (COMPLETED) - -- Goal: make session client polling non-blocking, bounded, and checksum-aligned. -- Scope: `crates/echo-session-client/src/lib.rs` (buffered try_read polling, MAX_PAYLOAD guard, checksum-respecting frame sizing, notification drain, tests). -- Status: completed; poll_message is now non-blocking, enforces an 8 MiB cap with checked arithmetic, preserves buffered partials, and poll_notifications drains buffered notifications only. - -> 2025-12-10 — Viewer timing & viewport safety (COMPLETED) - -- Goal: stabilize per-frame timing and prevent viewport unwrap panics. -- Scope: `crates/warp-viewer/src/app_frame.rs` (dt reuse, angular velocity with dt, safe viewport access, single aspect computation, window lifetime). -- Status: completed; dt is captured once per frame, spins/decay use that dt, viewport access is guarded, and helper signatures no longer require 'static windows. - -> 2025-12-10 — Config + docs alignment (COMPLETED) - -- Goal: keep docs aligned with code and maintained deps. -- Scope: `crates/echo-config-fs/README.md` (ConfigStore naming, doc path), `crates/echo-session-proto/src/lib.rs` (explicit reexports, AckStatus casing), `docs/book/echo/sections/06-editor-constellation.tex` + TikZ legend/label tweaks. -- Status: completed; README references correct traits/paths, proto surface is explicit with serde renames, figure labeled/cross-referenced with anchored legend. - -> 2025-12-06 — Tool crate docs + crate map (COMPLETED) - -- Goal: tighten docs around the tool hexagon pattern and make crate-level READMEs point at the Echo booklets as the canonical source of truth. -- Scope: `docs/book/echo/sections/09-tool-hex-pattern.tex` (crate map), READMEs and `Cargo.toml` `readme` fields for `echo-app-core`, `echo-config-fs`, `echo-session-proto`, `echo-session-service`, `echo-session-client`, and `warp-viewer`. -- Status: completed; Tools booklet now includes a crate map, and each tool-related crate README has a “What this crate does” + “Documentation” section pointing back to the relevant booklets/ADR/ARCH specs. - -> 2025-12-06 — JS-ABI + WARP streaming docs alignment (COMPLETED) - -- Goal: Align Echo’s book-level docs with the JS-ABI v1.0 deterministic encoding + framing decisions (ADR-0013 / ARCH-0013) and the new WARP streaming stack. -- Scope: `docs/book/echo/sections/{13-networking-wire-protocol,14-warp-stream-consumers,07-session-service,08-warp-viewer-spec}.tex` (cross-links, diagrams, tables). -- Status: completed; Core booklet now documents JS-ABI framing + generic WARP consumer contract (with role summary), and Tools booklet’s Session Service + WARP Viewer sections cross-reference that contract instead of re-specifying it. - -> 2025-12-04 — Sync roadmap with session streaming progress (COMPLETED) - -- Goal: capture the new canonical `echo-graph` crate + gapless WARP streaming path, and queue remaining engine/viewer wiring tasks. -- Scope: update `crates/warp-viewer/ROADMAP.md`, note outstanding engine emitter + client extraction; log decisions. -- Status: completed. - -> 2025-12-03 — Recover warp-viewer ROADMAP after VSCode crash - -- Goal: confirm whether roadmap edits were lost and restore the latest saved state. -- Scope: `crates/warp-viewer/ROADMAP.md` sanity check vs git. -- Status: completed; file matches last commit (no recovery needed). - -> 2025-12-03 — Persist warp-viewer camera + HUD settings between runs (COMPLETED) - -- Goal: write config load/save so camera + HUD toggles survive restarts. -- Scope: `crates/warp-viewer/src/main.rs`, add serde/directories deps; update roadmap/docs. -- Status: completed; config saved to OS config dir `warp-viewer.json`, loads on startup, saves on close. - -> 2025-12-03 — Extract core app services and refactor viewer (COMPLETED) - -- Goal: stop config/toast creep in warp-viewer; introduce shared core + fs adapter; make viewer consume injected prefs. -- Scope: new crates `echo-app-core` (ConfigService/ToastService/ViewerPrefs) and `echo-config-fs`; rewire `warp-viewer` to use them and drop serde/directories. -- Status: completed; prefs load/save via ConfigService+FsConfigStore; viewer owns only rendering + HUD state; toast rendering pending. - -> 2025-12-04 — Session proto/service/client skeleton (COMPLETED) - -- Goal: set up the distributed session slice with shared wire types and stub endpoints. -- Scope: new crates `echo-session-proto` (messages), `echo-session-service` (stub hub), `echo-session-client` (stub API); roadmap/docs updates. -- Status: completed; schema covers Handshake/SubscribeWarp/WarpStream (snapshot/diff)/Notification; transport and viewer binding are next. - -> 2025-12-01 — LaTeX skeleton + booklets + onboarding/glossary (COMPLETED) - -- Goal: scaffold reusable LaTeX parts (master + per-shelf booklets), wire logos, and seed onboarding + glossary content for Orientation. -- Scope: `docs/book/echo` (preamble, title/legal pages, parts/booklets, Makefile) and new Orientation chapters. -- Status: completed; master + booklets build, onboarding/glossary live. - -> 2025-12-01 — Set canonical package manager to pnpm in `package.json` - -- Goal: declare pnpm as the repo’s package manager via the `packageManager` field. -- Scope: `package.json` only. -- Status: completed; set to `pnpm@10.23.0` to match local toolchain. - -> 2025-12-01 — Fix cargo panic warning in bench profile (COMPLETED) - -- Goal: Silence the `warning: panic setting is ignored for bench profile` message during `cargo test`. -- Scope: `Cargo.toml`. -- Changes: Removed `panic = "abort"` from `[profile.bench]`. -- Status: Completed; warning no longer appears. - -> 2025-11-30 – Handle subnormal f32 values in F32Scalar - -- Goal: Canonicalize subnormal f32 values to zero. -- Scope: subnormals, F32Scalars. -- Plan: Make 'em zero. - -> 2025-12-01 — Fix “How Echo Works” LaTeX build (non-interactive PDF) - -- Goal: unblock `docs/guides/how-do-echo-work` PDF generation without interactive TeX prompts. -- Scope: tidy TikZ arrows/ampersands, add Rust listing language, harden LaTeX Makefile to fail fast. -- Plan: clean artifacts, adjust TeX sources, re-run `make` until `main.pdf` builds cleanly. - -> 2025-12-01 — Book accuracy + visuals refresh - -- Goal: align the “How Echo Works” guide with the current code (scheduler kinds, sandbox, math invariants) and add clearer visuals/tables. -- Scope: scan `warp-core` for scheduler, sandbox, and math implementations; update prose, tables, and TikZ diagrams; remove layout warnings. -- Status: completed; PDF now builds cleanly with updated figures and code snippets. - -> 2025-12-01 — License appendix + SPDX CI - -- Goal: add a LaTeX license appendix and wire CI to enforce SPDX headers. -- Scope: new `legal-appendix.tex` included in the guide; GitHub Action `spdx-header-check.yml` runs `scripts/check_spdx.sh --check --all`. -- Status: added appendix and workflow. - -> 2025-11-30 — PR #121 feedback (perf/scheduler) - -- Goal: triage and address CodeRabbit review feedback on scheduler radix drain/footprint changes; ensure determinism and docs guard stay green. -- Scope: `crates/warp-core/src/scheduler.rs`, related engine wiring, and any doc/bench fallout; keep PendingTx private and fail-fast drain semantics intact. -- Plan: classify feedback (P0–P3), implement required fixes on `perf/scheduler`, update Decision Log + docs guard, run `cargo clippy --all-targets` and relevant tests. -- Added: pluggable scheduler kind (Radix default, Legacy BTreeMap option) via `SchedulerKind`; legacy path kept for side-by-side comparisons. -- Risks: regress deterministic ordering or footprint conflict semantics; ensure histogram O(n) performance and radix counts remain u32 without overflow. - -> 2025-12-01 — Sandbox harness for deterministic A/B tests - -- Goal: enable spawning isolated Echo instances (Engine + GraphStore) from configs to compare schedulers and determinism. -- Scope: `warp-core::sandbox` with `EchoConfig`, `build_engine`, `run_pair_determinism`; public `SchedulerKind` (Radix/Legacy). -- Behavior: seed + rules provided as factories per instance; synchronous per-step determinism check helper; threaded runs left to callers. - -> 2025-11-06 — Unblock commit: warp-core scheduler Clippy fixes (follow-up) - -- Goal: make pre-commit Clippy pass without `--no-verify`, preserving determinism. -- Scope: `crates/warp-core/src/scheduler.rs` only; no API surface changes intended. -- Changes: - - Doc lint: add backticks in `scheduler.rs` docs for `b_in`/`b_out` and `GenSet(s)`. - - Reserve refactor: split `DeterministicScheduler::reserve` into `has_conflict`, `mark_all`, `on_conflict`, `on_reserved` (fix `too_many_lines`). - - Tests hygiene: move inner `pack_port` helper above statements (`items_after_statements`), remove `println!`, avoid `unwrap()`/`panic!`, use captured format args. - - Numeric idioms: replace boolean→int and lossless casts with `u64::from(...)` / `u32::from(...)`. - - Benches: drop unused imports in `reserve_scaling.rs` to avoid workspace clippy failures when checking all targets. -- Expected behavior: identical drain order and semantics; minor memory increase for counts on 64‑bit. -- Next: run full workspace Clippy + tests, then commit. - - CI follow-up: add `PortSet::iter()` (additive API) to satisfy scheduler iteration on GH runners. -> 2025-11-30 – F32Scalar canonicalization and trait implementations (COMPLETED) - -- Goal: Ensure bit-level deterministic handling of zero for `F32Scalar` and implement necessary traits for comprehensive numerical behavior. -- Scope: `crates/warp-core/src/math/scalar.rs` and `crates/warp-core/tests/math_scalar_tests.rs`. -- Changes: - - `F32Scalar` canonicalizes `-0.0` to `+0.0` on construction. - - `F32Scalar` canonicalizes all NaNs to `0x7fc00000` on construction (new). - - `value` field made private. - - `PartialEq` implemented via `Ord` (total_cmp) to ensure `NaN == NaN` (reflexivity). - - `Eq`, `PartialOrd`, `Ord`, `Display` traits implemented. -- Added: Tests for zero canonicalization, trait behavior, and NaN reflexivity. -- Risks: Introducing unexpected performance overhead or subtly breaking existing math operations; mitigated by unit tests and focused changes. - -> 2025-11-29 – Finish off `F32Scalar` implementation - -- Added `warp-core::math::scalar::F32Scalar` type. - -> 2025-11-03 — Issue #115: Scalar trait scaffold - -- Added `warp-core::math::scalar::Scalar` trait declaring deterministic scalar operations. -- Arithmetic is required via operator supertraits: `Add/Sub/Mul/Div/Neg` with `Output = Self` for ergonomic `+ - * / -` use in generics. -- Explicit APIs included: `zero`, `one`, `sin`, `cos`, `sin_cos` (default), `from_f32`, `to_f32`. -- No implementations yet (F32Scalar/DFix64 follow); no canonicalization or LUTs in this change. -- Exported via `warp-core::math::Scalar` for consumers. - -> 2025-11-02 — PR-12: benches updates (CI docs guard) - -- Dependency policy: pin `blake3` in `warp-benches` to exact patch `=1.8.2` with - `default-features = false, features = ["std"]` (no rayon; deterministic, lean). -- snapshot_hash bench: precompute `link` type id once; fix edge labels to `e-i-(i+1)`. -- scheduler_drain bench: builder returns `Vec` to avoid re-hashing labels; bench loop uses the precomputed ids. - -> 2025-11-02 — PR-12: benches polish (constants + docs) - -- snapshot_hash: extract all magic strings to constants; clearer edge ids using `<from>-to-<to>` labels; use `iter_batched` to avoid redundant inputs; explicit throughput semantics. -- scheduler_drain: DRY rule name/id prefix constants; use `debug_assert!` inside hot path; black_box the post-commit snapshot; added module docs and clarified BatchSize rationale. -- blake3 policy: keep exact patch `=1.8.2` and disable default features to avoid - rayon/parallel hashing in benches. - -> 2025-11-02 — PR-12: benches README - -- Added `crates/warp-benches/benches/README.md` documenting how to run and interpret - benchmarks, report locations, and optional flamegraph usage. -- Linked it from the main `README.md`. - -> 2025-11-02 — PR-12: benches polish and rollup refresh - -- Pin `blake3` in benches to `=1.8.2` and disable defaults to satisfy cargo-deny - wildcard bans while keeping benches single-threaded. -- snapshot_hash bench: precompute `link` type id and fix edge labels to `e-i-(i+1)`. -- scheduler_drain bench: return `Vec` from builder and avoid re-hashing node ids in the apply loop. - -> 2025-11-02 — Benches DX: offline report + server fix - -- Fix `Makefile` `bench-report` recipe to keep the background HTTP server alive using `nohup`; add `bench-status` and `bench-stop` helpers. -- Add offline path: `scripts/bench_bake.py` injects Criterion results into `docs/benchmarks/index.html` to produce `docs/benchmarks/report-inline.html` that works over `file://`. -- Update dashboard to prefer inline data when present (skips fetch). Update READMEs with `make bench-bake` instructions. - - Improve `bench-report`: add `BENCH_PORT` var, kill stale server, wait-for-ready loop with curl before opening the browser; update `bench-serve/bench-open/bench-status` to honor `BENCH_PORT`. - -> 2025-11-02 — PR-12: Sync with main + benches metadata - -- Target: `echo/pr-12-snapshot-bench` (PR #113). -- Merged `origin/main` into the branch (merge commit, no rebase) to clear GitHub conflict status. -- Resolved `crates/warp-benches/Cargo.toml` conflict by keeping: - - `license = "Apache-2.0"` and `blake3 = { version = "=1.8.2", default-features = false, features = ["std"] }` in dev-dependencies. - - Version-pinned path dep: `warp-core = { version = "0.1.0", path = "../warp-core" }`. - - Bench entries: `motion_throughput`, `snapshot_hash`, `scheduler_drain`. -- Benches code present/updated: `crates/warp-benches/benches/snapshot_hash.rs`, `crates/warp-benches/benches/scheduler_drain.rs`. -- Scope: benches + metadata only; no runtime changes. Hooks (fmt, clippy, tests, rustdoc) were green locally before push. - -> 2025-11-02 — PR-11 hotfix-deterministic-rollup-check - -- Switch to `echo/hotfix-deterministic-rollup-check`, fetch and merge `origin/main` (merge commit; no rebase). -- Fix CI cargo-deny failures: - - Add `license = "Apache-2.0"` to `crates/warp-benches/Cargo.toml`. - - Ensure no wildcard dependency remains in benches (use workspace path dep for `warp-core`). -- Modernize `deny.toml` (remove deprecated `copyleft` and `unlicensed` keys per cargo-deny PR #611); enforcement still via explicit allowlist. - -> 2025-10-30 — PR-01: Golden motion fixtures (tests-only) - -- Add JSON golden fixtures and a minimal harness for the motion rule under `crates/warp-core/tests/`. -- Scope: tests-only; no runtime changes. -- Links: PR-01 and tracking issue are associated for visibility. - -> 2025-10-30 — Templates + Project board (PR: templates) - -- Added GitHub templates (Bug, Feature, Task), PR template, and RFC discussion template. -- Configured Echo Project (Projects v2) Status options to include Blocked/Ready/Done. -- YAML lint nits fixed (no trailing blank lines; quoted placeholders). - -> 2025-10-30 — Templates PR cleanup (scope hygiene) - -- Cleaned branch `echo/pr-templates-and-project` to keep "one thing" policy: restored unrelated files to match `origin/main` so this PR only contains templates and the minimal Docs Guard notes. -- Verified YAML lint feedback: removed trailing blank lines and quoted the `#22` placeholder in Task template. -- Updated `docs/execution-plan.md` and `docs/decision-log.md` to satisfy Docs Guard for non-doc file changes. - -> 2025-12-01 — Docs rollup retired - -- Cleaned SPDX checker skip list now that the rollup no longer exists. - -> 2025-10-30 — Deterministic math spec (MD022) - -- On branch `echo/docs-math-harness-notes`, fixed Markdown lint MD022 by inserting a blank line after subheadings (e.g., `### Mat3 / Mat4`, `### Quat`, `### Vec2 / Vec3 / Vec4`). No content changes. - -> 2025-10-30 — Bug template triage fields - -- Enhanced `.github/ISSUE_TEMPLATE/bug.yml` with optional fields for `Stack Trace / Error Logs` and `Version / Commit` to improve first‑pass triage quality. - -> 2025-10-30 — Bug template wording consistency - -- Standardized description capitalization in bug template to imperative form ("Provide …") for consistency with existing fields. - -> 2025-10-30 — PR-03: proptest seed pinning (tests-only) - -- Added `proptest` as a dev‑dependency in `warp-core` and a single example test `proptest_seed_pinning.rs` that pins a deterministic RNG seed and validates the motion rule under generated inputs. This demonstrates how to reproduce failures via a fixed seed across CI and local runs (no runtime changes). - -> 2025-10-30 — PR-04: CI matrix (glibc + musl; macOS manual) - -- CI: Added a musl job (`Tests (musl)`) that installs `musl-tools`, adds target `x86_64-unknown-linux-musl`, and runs `cargo test -p warp-core --target x86_64-unknown-linux-musl`. -- CI: Added a separate macOS workflow (`CI (macOS — manual)`) triggered via `workflow_dispatch` to run fmt/clippy/tests on `macos-latest` when needed, avoiding default macOS runner costs. - -> 2025-10-30 — PR-06: Motion negative tests (opened) - -- Added tests in `warp-core` covering NaN/Infinity propagation and invalid payload size returning `NoMatch`. Tests-only; documents expected behavior; no runtime changes. - -> 2025-10-30 — PR-09: BLAKE3 header tests (tests-only) - -- Added unit tests under `warp-core` (in `snapshot.rs`) that: - - Build canonical commit header bytes and assert `compute_commit_hash` equals `blake3(header)`. - - Spot-check LE encoding (version u16 = 1, parents length as u64 LE). -- Assert that reversing parent order changes the hash. No runtime changes. - -> 2025-10-30 — PR-10: README (macOS manual + local CI tips) - -- Added a short CI Tips section to README covering how to trigger the manual macOS workflow and reproduce CI locally (fmt, clippy, tests, rustdoc, audit, deny). - -> 2025-11-01 — PR-10 scope hygiene - -- Removed commit‑header tests from `crates/warp-core/src/snapshot.rs` on this branch to keep PR‑10 strictly docs/CI/tooling. Those tests live in PR‑09 (`echo/pr-09-blake3-header-tests`). No runtime changes here. - - -> 2025-10-29 — Geom fat AABB midpoint sampling (merge-train) - -- Update `warp-geom::temporal::Timespan::fat_aabb` to union AABBs at start, mid (t=0.5), and end to conservatively bound rotations about off‑centre pivots. -- Add test `fat_aabb_covers_mid_rotation_with_offset` to verify the fat box encloses the mid‑pose AABB. - -> 2025-10-29 — Pre-commit format policy - -- Change auto-format behavior: when `cargo fmt` would modify files, the hook now applies formatting then aborts the commit with guidance to review and restage. This preserves partial-staging semantics and avoids accidentally staging unrelated hunks. - -> 2025-10-29 — CI/security hardening - -- CI now includes `cargo audit` and `cargo-deny` jobs to catch vulnerable/deprecated dependencies early. -- Rustdoc warnings gate covers warp-core, warp-geom, warp-ffi, and warp-wasm. -- Devcontainer runs `make hooks` post-create to install repo hooks by default. -- Note: switched audit action to `rustsec/audit-check@v1` (previous attempt to pin a non-existent tag failed). -- Added `deny.toml` with an explicit permissive-license allowlist (Apache-2.0, MIT, BSD-2/3, CC0-1.0, MIT-0, Unlicense, Unicode-3.0, BSL-1.0, Apache-2.0 WITH LLVM-exception) to align cargo-deny with our dependency set. - - Audit job runs `cargo audit` on Rust 1.75.0 (explicit `RUSTUP_TOOLCHAIN=1.75.0`) to satisfy tool MSRV; workspace MSRV remains 1.71.1. - -> 2025-10-29 — Snapshot commit spec - -- Added `docs/spec-merkle-commit.md` defining `state_root` vs `commit_id` encoding and invariants. -- Linked the spec from `crates/warp-core/src/snapshot.rs` and README. - -> 2025-10-28 — PR #13 (math polish) opened - -- Focus: canonicalize -0.0 in Mat4 trig constructors and add MulAssign ergonomics. -- Outcome: Opened PR echo/core-math-canonical-zero with tests; gather feedback before merge. - -> 2025-10-29 — Hooks formatting gate (PR #12) - -- Pre-commit: add rustfmt check for staged Rust files (`cargo fmt --all -- --check`). -- Keep PRNG coupling guard, but avoid early exit so formatting still runs when PRNG file isn't staged. -- .editorconfig: unify whitespace rules (LF, trailing newline, 2-space for JS/TS, 4-space for Rust). - -> 2025-10-29 — Docs make open (PR #11) - -- VitePress dev: keep auto-open; polling loop uses portable `sleep 1`. -- Fix links and dead-link ignore: root-relative URLs; precise regex for `/collision-dpo-tour.html`; corrected comment typo. - -> 2025-10-29 — Docs E2E (PR #10) - -- Collision DPO tour carousel: keep Prev/Next enabled in "all" mode so users and tests can enter carousel via navigation. Fixes Playwright tour test. -- Updated Makefile by merging hooks target with docs targets. -- CI Docs Guard satisfied with this entry; Decision Log updated. - -> 2025-10-29 — warp-core snapshot header + tx/rules hardening (PR #9 base) - -- Adopt Snapshot v1 header shape in `warp-core` with `parents: Vec`, and canonical digests: - - `state_root` (reachable‑only graph hashing) - - `plan_digest` (ready‑set ordering; empty = blake3(len=0)) - - `decision_digest` (Aion; zero for now) - - `rewrites_digest` (applied rewrites; empty = blake3(len=0)) -- Make `Engine::snapshot()` emit a header‑shaped view that uses the same canonical empty digests so a no‑op commit equals a pre‑tx snapshot. -- Enforce tx lifecycle: track `live_txs`, invalidate on commit, deny operations on closed/zero txs. -- Register rules defensively: error on duplicate name or duplicate id; assign compact rule ids for execute path. -- Scheduler remains crate‑private with explicit ordering invariant docs (ascending `(scope_hash, rule_id)`). -- Tests tightened: velocity preservation, commit after `NoMatch` is a no‑op, relative tolerances for rotation, negative scalar multiplies. - -> 2025-10-28 — Devcontainer/toolchain alignment - -- Toolchain floor via `rust-toolchain.toml`: 1.71.1 (workspace-wide). -- Devcontainer must not override default; selection is controlled by `rust-toolchain.toml`. -- Post-create installs 1.71.1 (adds rustfmt/clippy and wasm32 target). -- CI pins 1.71.1 for all jobs (single matrix; no separate floor job). - -> 2025-10-28 — Pre-commit auto-format flag update - -- Renamed `AUTO_FMT` → `ECHO_AUTO_FMT` in `.githooks/pre-commit`. -- README, AGENTS, and CONTRIBUTING updated to document hooks installation and the new flag. - -> 2025-10-28 — PR #8 (warp-geom foundation) updates - -- Focus: compile + clippy pass for the new geometry crate baseline. -- Changes in this branch: - - warp-geom crate foundations: `types::{Aabb, Transform}`, `temporal::{Tick, Timespan, SweepProxy}`. - - Removed premature `pub mod broad` (broad-phase lands in a separate PR) to fix E0583. - - Transform::to_mat4 now builds `T*R*S` using `Mat4::new` and `Quat::to_mat4` (no dependency on warp-core helpers). - - Clippy: resolved similar_names in `Aabb::transformed`; relaxed `nursery`/`cargo` denies to keep scope tight. - - Merged latest `main` to inherit CI/toolchain updates. - -> 2025-10-28 — PR #7 (warp-core engine spike) - -- Landed on main; see Decision Log for summary of changes and CI outcomes. - -> 2025-10-30 — warp-core determinism tests and API hardening - -- **Focus**: Address PR feedback for the split-core-math-engine branch. Add tests for snapshot reachability, tx lifecycle, scheduler drain order, and duplicate rule registration. Harden API docs and FFI (TxId repr, const ctors). -- **Definition of done**: `cargo test -p warp-core` passes; clippy clean for warp-core with strict gates; no workspace pushes yet (hold for more feedback). - -> 2025-10-30 — CI toolchain policy: use stable everywhere - -- **Focus**: Simplify CI by standardizing on `@stable` toolchain (fmt, clippy, tests, audit). Remove MSRV job; developers default to stable via `rust-toolchain.toml`. -- **Definition of done**: CI workflows updated; Security Audit uses latest cargo-audit on stable; docs updated. - -> 2025-10-30 — Minor rustdoc/lint cleanups (warp-core) - -- **Focus**: Address clippy::doc_markdown warning by clarifying Snapshot docs (`state_root` backticks). -- **Definition of done**: Lints pass under pedantic; no behavior changes. - -> 2025-10-30 — Spec + lint hygiene (core) - -- **Focus**: Remove duplicate clippy allow in `crates/warp-core/src/lib.rs`; clarify `docs/spec-merkle-commit.md` (edge_count may be 0; explicit empty digests; genesis parents). -- **Definition of done**: Docs updated; clippy clean. - ---- - -## Immediate Backlog - -- [x] ECS storage blueprint (archetype layout, chunk metadata, copy-on-write strategy). -- [x] Scheduler pseudo-code and DAG resolution rules. -- [x] Codex’s Baby command lifecycle with flush phases + backpressure policies. -- [x] Branch tree persistence spec (three-way diffs, roaring bitmaps, epochs, hashing). -- [x] Deterministic math module API surface (vectors, matrices, PRNG, fixed-point toggles). -- [x] Deterministic math validation strategy. -- [x] Branch merge conflict playbook. -- [x] Scaffold Rust workspace (`crates/warp-core`, `crates/warp-ffi`, `crates/warp-wasm`, `crates/warp-cli`). -- [ ] Port ECS archetype storage + branch diff engine to Rust. -- [x] Implement deterministic PRNG + math module in Rust. -- [x] Expose C ABI for host integrations (`warp-ffi`). -- [ ] Embed Rhai for scripting (deterministic sandbox + host modules). -- [ ] Integrate Rhai runtime with deterministic sandboxing and host modules. -- [ ] Adapt TypeScript CLI/inspector to Rust backend (WASM/FFI). -- [ ] Archive TypeScript prototype under `/reference/` as spec baseline. -- [x] Add Rust CI jobs (cargo test, replay verification). -- [ ] Integrate roaring bitmaps into ECS dirty tracking. -- [ ] Implement chunk epoch counters on mutation. -- [ ] Add deterministic hashing module (canonical encode + BLAKE3). -- [ ] Build DirtyChunkIndex pipeline from ECS to branch tree. - -### Code Tasks (Phase 1 prep) -- [x] Install & configure Vitest. -- [x] Install & configure Vitest. -- [x] Set up `crates/warp-core/tests/common/` helpers & fixtures layout. -- [ ] Write failing tests for entity ID allocation + recycling. -- [ ] Prototype `TimelineFingerprint` hashing & equality tests. -- [x] Scaffold deterministic PRNG wrapper with tests. -- [x] Establish `cargo test` pipeline in CI (incoming GitHub Actions). -- [ ] Integrate roaring bitmaps into ECS dirty tracking. -- [ ] Implement chunk epoch counters on mutation. -- [ ] Add deterministic hashing module (canonical encode + BLAKE3). -- [ ] Build DirtyChunkIndex pipeline from ECS to branch tree. -- [ ] Implement merge decision recording + decisions digest. -- [ ] Implement paradox detection (read/write set comparison). -- [ ] Implement entropy tracking formula in branch tree. -- [ ] Prototype epoch-aware refcount API (stub for single-thread). -- [ ] Implement deterministic GC scheduler (sorted node order + intervals). -- [ ] Update Codex's Baby to Phase 0.5 spec (event envelope, bridge, backpressure, inspector packet, security). - -### Tooling & Docs -- [ ] Build `docs/data-structures.md` with Mermaid diagrams (storage, branch tree with roaring bitmaps). -- [ ] Extend `docs/diagrams.md` with scheduler flow & command queue animations. -- [ ] Publish decision-log quick reference (templates, cadence, examples; owner: Documentation squad before Phase 1 kickoff). -- [ ] Design test fixture layout (`test/fixtures/…`) with sample component schemas. -- [ ] Document roaring bitmap integration and merge strategies. -- [ ] Update future inspector roadmap with conflict heatmaps and causality lens. - ---- - -## Decision Log (High-Level) - -| Date | Decision | Context | Follow-up | -| ---- | -------- | ------- | --------- | -| 2025-10-23 | Monorepo seeded with pnpm & TypeScript skeleton | Baseline repo reset from legacy prototypes to Echo | Implement Phase 0 specs | -| 2025-10-24 | Branch tree spec v0.1: roaring bitmaps, chunk epochs, content-addressed IDs | Feedback loop to handle deterministic merges | Implement roaring bitmap integration | -| 2025-10-25 | Language direction pivot: Echo core to Rust | TypeScript validated specs; long-term determinism enforced via Rust + C ABI + Rhai scripting | Update Phase 1 backlog: scaffold Rust workspace, port ECS/diff engine, FFI bindings | -| 2025-10-25 | Math validation fixtures & Rust test harness | Established deterministic scalar/vector/matrix/quaternion/PRNG coverage in warp-core | Extend coverage to browser environments and fixed-point mode | -| 2025-10-26 | Adopt WARP + Confluence as core architecture | WARP v2 (typed DPOi engine) + Confluence replication baseline | Scaffold warp-core/ffi/wasm/cli crates; implement rewrite executor spike; integrate Rust CI; migrate TS prototype to `/reference` | -| 2025-12-28 | Mechanical rename: RMG → WARP | Align the repo’s terminology and public surface to the AIΩN Foundations Series naming | Keep decode aliases during transition; update bridge/mapping docs as divergences land | - -(Keep this table updated; include file references or commit hashes when useful.) - ---- - -## Next Up Queue - -1. M2.1: DPO concurrency theorem coverage (issue #206) -2. Demo 2: Splash Guy docs course modules (issue #226) -3. Demo 3: Tumble Tower Stage 1 physics (issue #232) -4. TT1 follow-ons: dt policy / retention / merge semantics / capabilities (issues #243–#246) -5. ECS storage implementation plan - -Populate with concrete tasks in priority order. When you start one, move it to “Today’s Intent.” - ---- - -## Notes to Future Codex - -- Update this document and `docs/decision-log.md` for daily runtime updates. -- Record test coverage gaps as they appear; they inform future backlog items. -- Ensure roaring bitmap and hashing dependencies are deterministic across environments. -- Inspector pins must be recorded to keep GC deterministic. -- When finishing a milestone, snapshot the diagrams and link them in the memorial for posterity. - -Remember: every entry here shrinks temporal drift between Codices. Leave breadcrumbs; keep Echo’s spine alive. 🌀 -> 2025-11-02 — Hotfix: deterministic rollup check (CI) - -- Made CI rollup check robust against legacy non-deterministic headers by normalizing out lines starting with `Generated:` before comparing. Current generator emits a stable header, but this guards older branches and avoids false negatives. - -> 2025-11-02 — Hotfix follow-up: tighter normalization + annotation - -> 2025-11-02 — PR-11: benches crate skeleton (M1) - -- Add `crates/warp-benches` with Criterion harness and a minimal motion-throughput benchmark that exercises public `warp-core` APIs. -- Scope: benches-only; no runtime changes. Document local run (`cargo bench -p warp-benches`). diff --git a/docs/golden-vectors.md b/docs/golden-vectors.md new file mode 100644 index 00000000..71f6bfe8 --- /dev/null +++ b/docs/golden-vectors.md @@ -0,0 +1,76 @@ + + +# ABI Golden Vectors (v1) + +Status: **Phase 1 Frozen** + +These vectors ensure that both the Rust kernel and the JS host implement the +**Canonical CBOR (subset)** mapping identically. + +## 1. Scalars + +| Value | Hex Encoding | Description | +| :--- | :--- | :--- | +| `null` | `f6` | CBOR Null | +| `true` | `f5` | CBOR True | +| `false` | `f4` | CBOR False | +| `0` | `00` | Smallest Int | +| `-1` | `20` | Smallest Neg Int | +| `23` | `17` | Boundary Int | +| `24` | `18 18` | 1-byte Int | +| `255` | `18 ff` | 1-byte Int Max | +| `256` | `19 01 00` | 2-byte Int | + +## 2. Strings (UTF-8) + +| Value | Hex Encoding | Description | +| :--- | :--- | :--- | +| `""` | `60` | Empty string | +| `"a"` | `61 61` | 1-char string | +| `"echo"` | `64 65 63 68 6f` | 4-char string | + +## 3. Maps (Sorted Keys) + +Maps MUST be sorted by the bytewise representation of their encoded keys. + +### Example: `{ "b": 1, "a": 2 }` + +- Key `"a"` encodes to `61 61` +- Key `"b"` encodes to `61 62` +- Correct Order: `"a"`, then `"b"` +- **Hex**: `a2 61 61 02 61 62 01` + +## 4. Nested Structures + +### AppState Sample + +```json +{ + "theme": "DARK", + "navOpen": true, + "routePath": "/" +} +``` + +**Canonical Sort Order:** +1. `"navOpen"` (`67 6e 61 76 4f 70 65 6e`) +2. `"routePath"` (`69 72 6f 75 74 65 50 61 74 68`) +3. `"theme"` (`65 74 68 65 6d 65`) + +Wait, let's check bytewise: +- `navOpen`: `67 ...` +- `routePath`: `69 ...` +- `theme`: `65 ...` + +Bytewise order: +1. `65 ...` (`"theme"`) +2. `67 ...` (`"navOpen"`) +3. `69 ...` (`"routePath"`) + +**Hex Encoding**: +`a3` (map of 3) +`65 74 68 65 6d 65` ("theme") `64 44 41 52 4b` ("DARK") +`67 6e 61 76 4f 70 65 6e` ("navOpen") `f5` (true) +`69 72 6f 75 74 65 50 61 74 68` ("routePath") `61 2f` ("/") + +**Full**: `a3 65 74 68 65 6d 65 64 44 41 52 4b 67 6e 61 76 4f 70 65 6e f5 69 72 6f 75 74 65 50 61 74 68 61 2f` diff --git a/docs/guide/start-here.md b/docs/guide/start-here.md index fbf5aa71..8a0020ef 100644 --- a/docs/guide/start-here.md +++ b/docs/guide/start-here.md @@ -36,10 +36,8 @@ ECS is a *useful storage and API layer*, but the deeper “ground truth” model 2. Collision tour: [/guide/collision-tour](/guide/collision-tour) 3. Interactive collision DPO tour (static HTML): [/collision-dpo-tour.html](/collision-dpo-tour.html) -### If you want “what should I work on?” +### If you want what should I work on? -- Execution plan (living intent, “Today’s Intent” at the top): [/execution-plan](/execution-plan) -- Decision log (chronological record of decisions): [/decision-log](/decision-log) - Docs map (curated index): [/docs-index](/docs-index) ## How These Docs Are Organized diff --git a/docs/index.md b/docs/index.md index 3dfeb5b3..29e9716a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,5 +24,3 @@ and each tick applies deterministic rewrite rules to that graph. ## When You Need a Map - Docs map (curated): [/docs-index](/docs-index) -- Execution plan (living intent): [/execution-plan](/execution-plan) -- Decision log (chronological): [/decision-log](/decision-log) diff --git a/docs/js-cbor-mapping.md b/docs/js-cbor-mapping.md new file mode 100644 index 00000000..cff47eff --- /dev/null +++ b/docs/js-cbor-mapping.md @@ -0,0 +1,48 @@ + + +# JS → Canonical CBOR Mapping Rules (ABI v1) + +Status: **Frozen for website kernel P1** + +These rules define how host-side JS/TS values must be mapped into the canonical CBOR +encoder before hitting the ledger. The same rules apply to wasm helpers +(`encode_command`, `encode_query_vars`) and to native tooling. + +## Scalars +- `null` → CBOR null +- `boolean` → CBOR bool +- `string` → CBOR text (UTF-8) +- `number` + - If integral and `abs(n) <= Number.MAX_SAFE_INTEGER` → CBOR integer (shortest width) + - Else → CBOR float (smallest width that round-trips; NaN/±∞ allowed, canonicalized) +- **Ban** `undefined` (error) +- **Ban** `BigInt` for P1 (use string/bytes if needed) + +## Bytes +- `Uint8Array` → CBOR byte string (definite length) + +## Arrays +- JS array → CBOR array (definite length), elements encoded recursively. + +## Objects (Maps) +- Keys **must** be strings; non-string keys are rejected. +- Encoded as CBOR map with keys sorted by their CBOR byte encoding (canonical). +- Duplicate keys are rejected. +- Unknown/extra fields should be rejected at schema validation (Zod/Rust). + +## Prohibited CBOR features +- No tags. +- No indefinite-length strings, arrays, or maps. +- Shortest encodings required for ints/floats. + +## Error surface (host-facing) +- INVALID_INPUT for: undefined, BigInt, non-string map keys, duplicate keys, unknown fields, + missing required fields, non-canonical float/int widths, indefinite-length items. + +## Canonical payload identity +- The exact CBOR bytes produced by these rules are the authoritative payload for hashing + and ledger stamping. Re-encoding the same logical value must yield identical bytes. + +## References +- `crates/echo-wasm-abi/src/canonical.rs` — canonical encoder/decoder and rejection tests. +- ADR-0013 / JS-ABI v1 framing (canonical CBOR payload inside packet header). diff --git a/docs/notes/f32scalar-deterministic-trig-implementation-guide.md b/docs/notes/f32scalar-deterministic-trig-implementation-guide.md index 51a57df2..85ccbaef 100644 --- a/docs/notes/f32scalar-deterministic-trig-implementation-guide.md +++ b/docs/notes/f32scalar-deterministic-trig-implementation-guide.md @@ -259,7 +259,7 @@ When the backend lands, update: - `docs/SPEC_DETERMINISTIC_MATH.md` checklist (`sin/cos` deterministic approximation) -And add a short decision-log entry noting: +Document: - the chosen LUT resolution/interpolation - the accepted error budget diff --git a/docs/notes/pr-141-comment-burn-down.md b/docs/notes/pr-141-comment-burn-down.md deleted file mode 100644 index 8b7e025f..00000000 --- a/docs/notes/pr-141-comment-burn-down.md +++ /dev/null @@ -1,184 +0,0 @@ - - -# PR #141 — Comment Burn-Down - -PR: [#141](https://github.com/flyingrobots/echo/pull/141) - -## Purpose & Retention - -This file is a PR-scoped, action-oriented index of review threads → fixing SHAs. - -- Canonical design decisions belong in `docs/decision-log.md`. -- After PR #141 merges, this file may be deleted or moved to `docs/legacy/` if it remains useful as a historical artifact. - -## Snapshot (2025-12-28) - -- Head branch: `echo/wasm-spec-000-scaffold` -- Base branch: `main` -- Head commit (for this snapshot): `4469b9e` -- Latest CodeRabbit review commit: `4469b9e` (review submitted 2025-12-28) - -### Extraction (paginated, per EXTRACT-PR-COMMENTS procedure) - -```bash -gh api --paginate repos/flyingrobots/echo/pulls/141/comments --jq '.[]' | jq -s '.' > /tmp/pr141-review-comments.json -gh api --paginate repos/flyingrobots/echo/issues/141/comments --jq '.[]' | jq -s '.' > /tmp/pr141-issue-comments.json -``` - -- PR review comments (inline): 153 total - - Top-level: 102 - - Replies: 51 -- Issue comments (conversation): 4 (CodeRabbit summaries + maintainer notes + consolidated fix-map comments) - -## Buckets (Top-Level Review Comments) - -Notes: - -- `P0` == CodeRabbit “🔴 Critical” (blockers). -- Many comments are “stale” in GitHub terms (carried forward across commits); each item below was verified against current code/docs before action. -- Some CodeRabbit comments include a built-in “✅ Confirmed …” marker; many do not. This file is the canonical burn-down record for PR #141. - -### P0 — Blockers - -- [x] [r2649872451](https://github.com/flyingrobots/echo/pull/141#discussion_r2649872451) `crates/warp-wasm/Cargo.toml` — Restore the canonical crates.io `"wasm"` category for WASM crates (verified via crates.io categories API). Fixed in `84e63d3`. - -- [x] [r2649834667](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834667) `crates/warp-ffi/Cargo.toml` — Standardize workspace MSRV (`rust-version`) and align with the pinned toolchain. Fixed in `0f8e95d`. -- [x] [r2649834670](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834670) `deny.toml` — CodeRabbit claimed OpenSSL + Ubuntu-font allowlist entries were orphaned; verified dependency chains and strengthened justification. Fixed in `3e5b52d`. - -- [x] [r2649763194](https://github.com/flyingrobots/echo/pull/141#discussion_r2649763194) `.github/workflows/ci.yml` — Supply-chain hardening: replace mutable `@v2` with a pinned `cargo-deny-action` commit SHA. Fixed in `602ba1e`. - -- [x] [r2645857657](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857657) `crates/echo-wasm-bindings/src/lib.rs` — Only log rewrites for successful mutations (no-op history is a semantic violation). Fixed in `7825d81`. -- [x] [r2645857663](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857663) `crates/echo-wasm-bindings/src/lib.rs` — Prevent dangling edges: validate `from`/`to` nodes exist before connecting. Fixed in `7825d81`. -- [x] [r2645857667](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857667) `crates/echo-wasm-bindings/src/lib.rs` — Do not record `DeleteNode` rewrites when the node does not exist. Fixed in `7825d81`. -- [x] [r2645857670](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857670) `crates/echo-wasm-bindings/src/lib.rs` — Remove `.unwrap()` from WASM boundary; avoid panics and deprecated serde helpers. Fixed in `7825d81`. - -- [x] [r2612251496](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251496) `docs/METHODOLOGY.md` — Remove/clarify phantom crate (`crates/echo-kernel`) in the methodology diagram. Fixed in `cfe9270`. -- [x] [r2612251499](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251499) `docs/METHODOLOGY.md` — Mark hosted spec domains and completion-hash certification as planned (not implemented yet). Fixed in `cfe9270`. -- [x] [r2612251505](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251505) `docs/METHODOLOGY.md` — Definition of Done must include the repo’s quality gates (tests, docs, clippy, docs-guard, SPDX, fmt). Fixed in `cfe9270` + `641e482`. - -- [x] [r2645857677](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857677) `docs/decision-log.md` — Remove duplicate decision-log row (keep the authoritative combined entry). Fixed in `641e482`. -- [x] [r2645857683](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857683) `docs/jitos/spec-0000.md` — Fix incorrect `crate::warp_core::*` example imports (use external `warp_core` crate paths). Fixed in `cf286e9`. -- [x] [r2612251514](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251514) `docs/tasks.md` — Remove duplicate contradictory task entries. Fixed in `cfe9270`. - -- [x] [r2645857694](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857694) `specs/spec-000-rewrite/Cargo.toml` — CodeRabbit claimed `edition = "2024"` is invalid; it is valid under the repo toolchain (`rust-toolchain.toml` pins Rust 1.90.0) and the crate declares `rust-version = "1.90.0"` (see `0f8e95d`). No code change required. -- [x] [r2649731206](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731206) `specs/spec-000-rewrite/Cargo.toml` — Bump Leptos from `0.6` → `0.8.15` and run `cargo update` + fix compile breakage. Fixed in `1a0c870`. - -### P1 — Major - -- [x] [r2649872452](https://github.com/flyingrobots/echo/pull/141#discussion_r2649872452) `docs/jitos/spec-0000.md` — Make the `WarpGraph` example struct `Serialize`/`Deserialize` so the WASM example’s `serde_json::to_string(&self.warp)` compiles. Fixed in `922553f`. - -- [x] [r2649834662](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834662) `Cargo.toml` — Add `echo-wasm-abi` to `[workspace.dependencies]` to restore workspace inheritance for internal consumers. Fixed in `150415b`. -- [x] [r2649834663](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834663) `crates/echo-session-service/Cargo.toml` — Migrate internal deps to `workspace = true` (avoid version drift vs root `[workspace.dependencies]`). Fixed in `2ee0a07`. -- [x] [r2649834682](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834682) `scripts/run_cargo_audit.sh` — DRY: derive ignore IDs from `deny.toml` and add a self-test to prevent drift. Fixed in `3570069` (self-test portability tweak in `e5954e4`). - -- [x] [r2649763195](https://github.com/flyingrobots/echo/pull/141#discussion_r2649763195) `.github/workflows/security-audit.yml` — Remove duplicate `cargo-audit` job definition from CI; centralize audit args in `scripts/run_cargo_audit.sh` to prevent ignore-list drift. Fixed in `602ba1e`. - -- [x] [r2649699435](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699435) `crates/echo-session-ws-gateway/src/main.rs` — Add negative tests for frame parsing (partial header/body, too-small, payload-too-large). Fixed in `46bc079`. -- [x] [r2649699436](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699436) `crates/echo-wasm-abi/src/lib.rs` — Remove vestigial `#[serde_as]` usage (no annotations present). Fixed in `46bc079`. -- [x] [r2649699438](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699438) `crates/echo-wasm-bindings/README.md` — Document API surface with explicit type signatures. Fixed in `46bc079`. -- [x] [r2649731190](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731190) `crates/echo-wasm-abi/src/lib.rs` — Add a separate field-name slot so `old_value` can carry the actual prior field value; update docs/tests to match. Fixed in `1f36f77`. -- [x] [r2649731193](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731193) `crates/echo-wasm-bindings/src/lib.rs` — Record `Set` rewrites with `subject = field_name` and `old_value = prior_value` (or `None`); add a regression test. Fixed in `1f36f77`. - -- [x] [r2612251468](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251468) `crates/echo-session-client/src/lib.rs` — Classify protocol errors by code so session-level errors become `Global` notifications. Fixed in `12ecd95`. -- [x] [r2612251472](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251472) `crates/echo-session-ws-gateway/Cargo.toml` — Upgrade `axum`/`axum-server` to compatible, modern versions. Fixed in `89c2bb1`. -- [x] [r2612251488](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251488) `crates/echo-session-ws-gateway/src/main.rs` — Don’t swallow task errors; improve logging for debuggability. Fixed in `89c2bb1`. -- [x] [r2612251492](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251492) `crates/echo-session-ws-gateway/src/main.rs` — DRY: factor duplicate frame-length arithmetic into a helper. Fixed in `89c2bb1`. -- [x] [r2612251482](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251482) `crates/echo-session-ws-gateway/src/main.rs` — Cap the frame accumulator to prevent DoS via malformed streams. Fixed in `89c2bb1`. - -- [x] [r2645857640](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857640) `crates/echo-wasm-abi/Cargo.toml` — Declare MSRV for edition-2024 crates. Fixed in `2431e9f`. -- [x] [r2645857649](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857649) `crates/echo-wasm-abi/src/lib.rs` — Expand rustdoc: intent, invariants, and examples for public types. Fixed in `2431e9f`. -- [x] [r2645857654](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857654) `crates/echo-wasm-bindings/src/lib.rs` — Expand `DemoKernel` rustdoc to document intent and invariants. Fixed in `95f8eda` (and tightened in `7825d81`). - -- [x] [r2645857687](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857687) `docs/jitos/spec-0000.md` — Replace deprecated serde-on-`JsValue` helpers; keep WASM boundary panic-free. Fixed in `7825d81` + `cf286e9`. - -### P2 — Minor - -- [x] [r2649872453](https://github.com/flyingrobots/echo/pull/141#discussion_r2649872453) `docs/jitos/spec-0000.md` — Implement `SemanticOp::Disconnect` in the spec example (avoid silent no-op while claiming reversibility). Fixed in `922553f`. -- [x] [r2649872454](https://github.com/flyingrobots/echo/pull/141#discussion_r2649872454) `scripts/check_rust_versions.sh` — Remove the undocumented `find -mindepth/-maxdepth` layout constraint (scan recursively) and add self-tests. Fixed in `56a37f8`. - -- [x] [r2649834666](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834666) `crates/echo-wasm-bindings/Cargo.toml` (and `crates/echo-wasm-abi/Cargo.toml`) — CodeRabbit claimed `"wasm"` was an invalid crates.io category; verified it exists and restored it for WASM crates. Fixed in `84e63d3` (after an intermediate churn pass in `3ccaf47`). -- [x] [r2649834672](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834672) `deny.toml` — RUSTSEC-2024-0370 no longer appears in the lock; remove the stale ignore to avoid drift. Fixed in `1bf90d3`. -- [x] [r2649834674](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834674) `docs/execution-plan.md` — Verified: Phase Overview table already has 5 columns per row; no change required. -- [x] [r2649834677](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834677) `docs/execution-plan.md` — Verified: no trailing whitespace present; no change required. -- [x] [r2649834678](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834678) `docs/execution-plan.md` — Verified: no trailing whitespace present; no change required. - -- [x] [r2649731188](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731188) `crates/echo-wasm-abi/Cargo.toml` — Resolve SPDX header vs `license` field mismatch by aligning build/config files to `Apache-2.0` headers (per repo license split). Fixed in `042ec2b`. -- [x] [r2649731189](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731189) `crates/echo-wasm-abi/Cargo.toml` — `"wasm"` is a valid crates.io category; keep it on WASM crates for discoverability. Fixed in `84e63d3`. -- [x] [r2649731196](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731196) `deny.toml` — Keep `OpenSSL`/`Ubuntu-font-1.0` allowlist entries but add explicit justification (required by current transitive dependencies). Fixed in `17687f2`. -- [x] [r2649731199](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731199) `docs/jitos/spec-0000.md` — Fix heading-increment violation (MD001). Fixed in `6ee8811`. - -- [x] [r2649763196](https://github.com/flyingrobots/echo/pull/141#discussion_r2649763196) `docs/execution-plan.md` — Fix MD034: wrap bare PR-comment URL in angle brackets. Fixed in `6ee8811`. -- [x] [r2649763197](https://github.com/flyingrobots/echo/pull/141#discussion_r2649763197) `docs/jitos/spec-0000.md` — Fix MD040: add language identifiers to fenced code blocks (`text`/`rust`/`javascript`). Fixed in `6ee8811`. -- [x] [r2649731200](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731200) `docs/jitos/spec-0000.md` — Fix MD040: add language identifiers to fenced code blocks. Fixed in `6ee8811`. -- [x] [r2649763198](https://github.com/flyingrobots/echo/pull/141#discussion_r2649763198) `docs/jitos/spec-0000.md` — Fix MD009: strip trailing whitespace throughout. Fixed in `6ee8811`. - -- [x] [r2649699430](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699430) `crates/echo-session-client/src/lib.rs` — Strengthen test to assert full notification structure (kind/title/body), not just scope. Fixed in `46bc079`. -- [x] [r2649699439](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699439) `crates/echo-wasm-bindings/src/lib.rs` — Make `add_node` a no-op on duplicate ids to avoid clobbering + semantic ambiguity; add regression test. Fixed in `46bc079`. -- [x] [r2649699447](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699447) `docs/notes/pr-141-comment-burn-down.md` — Replace bare URL with a Markdown link (MD034). Fixed in `46bc079`. -- [x] [r2649699453](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699453) `docs/notes/pr-141-comment-burn-down.md` — Capitalize “Markdown” (proper noun). Fixed in `46bc079`. -- [x] [r2649699463](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699463) `specs/spec-000-rewrite/Cargo.toml` — Replace invalid categories (`gui`/`education` → valid crates.io slugs) in `46bc079`; `"wasm"` category later restored in `84e63d3`. -- [x] [r2649699470](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699470) `specs/spec-000-rewrite/spec.md` — Fix MD022 (blank line after headings). Fixed in `46bc079`. - -- [x] [r2612251521](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251521) `README.md` — Remove trailing whitespace / tighten formatting. Fixed in `cf286e9`. -- [x] [r2645857690](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857690) `README.md` — Add alt text to images. Fixed in `cf286e9`. -- [x] [r2612251524](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251524) `README.md` — Resolve Markdown formatting nits in the referenced section. Fixed in `cf286e9`. - -- [x] [r2612251540](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251540) `WASM-TASKS.md` — Fix heading spacing. Fixed in `6238c98`. -- [x] [r2612251473](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251473) `crates/echo-session-ws-gateway/README.md` — Add missing blank lines around headings/fences. Fixed in `6238c98`. -- [x] [r2612251477](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251477) `crates/echo-session-ws-gateway/src/main.rs` — Add a timeout to UDS connect to avoid hanging forever. Fixed in `89c2bb1`. - -- [x] [r2645857679](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857679) `docs/execution-plan.md` — Add verifiable evidence pointers (commit SHAs / branch notes) to completion claims. Fixed in `641e482`. -- [x] [r2645857680](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857680) `docs/jitos/spec-0000.md` — Improve Markdown spacing/readability (MD022). Fixed in `cf286e9`. -- [x] [r2612251509](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251509) `docs/spec-concurrency-and-authoring.md` — Add missing blank lines around fences. Fixed in `6238c98`. -- [x] [r2612251512](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251512) `docs/spec-concurrency-and-authoring.md` — Clarify that `echo::delay()`/`echo::emit()` are Echo host functions (not built-in Rhai). Fixed in `6238c98`. - -### P3 — Trivial - -- [x] [r2649892639](https://github.com/flyingrobots/echo/pull/141#discussion_r2649892639) `scripts/check_rust_versions.sh` — Replace `sed` prefix formatting with bash-native output and support `rust-version.workspace = true` (with regression tests). Fixed in `7e84b16`. -- [x] [r2649892640](https://github.com/flyingrobots/echo/pull/141#discussion_r2649892640) `specs/spec-000-rewrite/Cargo.toml` — Inherit shared `license`/`repository`/`rust-version` from `[workspace.package]` to reduce duplication and drift. Fixed in `e4e5c19`. - -- [x] [r2649872448](https://github.com/flyingrobots/echo/pull/141#discussion_r2649872448) `.github/workflows/ci.yml` — Move `cargo-audit` runner self-tests out of the “Task Lists Guard” job (into the dependency policy job). Fixed in `56a37f8`. -- [x] [r2649872450](https://github.com/flyingrobots/echo/pull/141#discussion_r2649872450) `crates/echo-session-client/Cargo.toml` — Standardize internal deps on workspace inheritance (`echo-session-proto.workspace = true`). Fixed in `dfa938a`. -- [x] [r2649872455](https://github.com/flyingrobots/echo/pull/141#discussion_r2649872455) `scripts/check_rust_versions.sh` — Quote the inner prefix-expansion to avoid glob-pattern surprises (ShellCheck SC2295). Fixed in `56a37f8`. -- [x] [r2649872457](https://github.com/flyingrobots/echo/pull/141#discussion_r2649872457) `scripts/check_rust_versions.sh` — Robustly parse `rust-version` by extracting the first quoted value (safe even with inline comments containing quotes). Fixed in `56a37f8`. - -- [x] [r2649834681](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834681) `Makefile` — Add guard rails to `spec-000-dev` / `spec-000-build` for missing `trunk` and missing `specs/spec-000-rewrite`. Fixed in `8db8ac6`. -- [x] [r2649834665](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834665) `crates/echo-session-ws-gateway/Cargo.toml` — Add explicit `rust-version` to the manifest (aligned via workspace MSRV standardization). Fixed in `0f8e95d`. -- [x] [r2649834680](https://github.com/flyingrobots/echo/pull/141#discussion_r2649834680) `docs/jitos/spec-0000.md` — Style: vary sentence openings in the “Next Steps” list to avoid repetitive “Add …” phrasing. Fixed in `82fce3f`. - -- [x] [r2649763192](https://github.com/flyingrobots/echo/pull/141#discussion_r2649763192) `.github/workflows/ci.yml` — Harden task-lists job (explicit `bash`, executable check, richer diagnostics). Fixed in `602ba1e`. -- [x] [r2649731202](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731202) `scripts/check_task_lists.sh` — Do not silently skip missing files (warn + fail when none exist). Fixed in `602ba1e`. -- [x] [r2649731204](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731204) `scripts/check_task_lists.sh` — Compare task text case-insensitively for conflict detection. Fixed in `602ba1e`. -- [x] [r2649731186](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731186) `.githooks/pre-commit` — Fail if task list guard script exists but is not executable (avoid silent skip). Fixed in `5086881`. -- [x] [r2649731194](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731194) `crates/echo-wasm-bindings/tests/api_tests.rs` — Add explicit self-edge (`connect("A","A")`) behavior test. Fixed in `5086881`. -- [x] [r2649731195](https://github.com/flyingrobots/echo/pull/141#discussion_r2649731195) `crates/warp-viewer/Cargo.toml` — DRY: move local path+version pins into `[workspace.dependencies]` and inherit with `workspace = true`. Fixed in `5086881`. - -- [x] [r2649699428](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699428) `crates/echo-session-client/src/lib.rs` — Extract the `>= 400` scope threshold into a named constant. Fixed in `46bc079`. -- [x] [r2649699432](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699432) `crates/echo-session-ws-gateway/README.md` — Add language to fenced code blocks (MD040). Fixed in `46bc079`. -- [x] [r2649699434](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699434) `crates/echo-session-ws-gateway/src/main.rs` — Add rustdoc for JS-ABI constants (frame structure intent). Fixed in `46bc079`. -- [x] [r2649699437](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699437) `crates/echo-wasm-abi/src/lib.rs` — Broaden serialization tests to cover all `SemanticOp` variants. Fixed in `46bc079`. -- [x] [r2649699441](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699441) `crates/echo-wasm-bindings/tests/api_tests.rs` — “Edge-case coverage significantly improved” (ack; no action required). Fixed in `46bc079`. -- [x] [r2649699442](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699442) `docs/jitos/spec-0000.md` — Remove interactive “Which one do you want me to generate next?” prompt from the spec doc. Fixed in `46bc079`. -- [x] [r2649699444](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699444) `docs/notes/pr-141-comment-burn-down.md` — Explain relationship to `docs/decision-log.md` and define a retention policy. Fixed in `46bc079`. -- [x] [r2649699466](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699466) `specs/spec-000-rewrite/index.html` — Add an explicit note about keeping CSS inline for Phase 0 (extraction planned later). Fixed in `46bc079`. -- [x] [r2649699471](https://github.com/flyingrobots/echo/pull/141#discussion_r2649699471) `WASM-TASKS.md` / `docs/tasks.md` — Add automated enforcement for “task lists must not contradict themselves” (pre-commit + CI). Fixed in `46bc079`. - -- [x] [r2612251483](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251483) `crates/echo-session-ws-gateway/src/main.rs` — Avoid immediate ping tick (let handshake settle). Fixed in `89c2bb1`. -- [x] [r2645857635](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857635) `crates/echo-session-ws-gateway/src/main.rs` — Log rejected Origin values for debugging. Fixed in `89c2bb1`. - -- [x] [r2645857642](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857642) `crates/echo-wasm-abi/Cargo.toml` — Pin dependencies to minor versions for reproducibility. Fixed in `2431e9f`. -- [x] [r2645857643](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857643) `crates/echo-wasm-abi/README.md` — Fix heading spacing. Fixed in `2431e9f`. - -- [x] [r2645857651](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857651) `crates/echo-wasm-bindings/README.md` — Fix Markdown formatting / align exposed API docs. Fixed in `cf286e9`. -- [x] [r2645857656](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857656) `crates/echo-wasm-bindings/src/lib.rs` — Reorder ops to mutate, then log (future-proof history consistency). Fixed in `95f8eda`. -- [x] [r2645857675](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857675) `crates/echo-wasm-bindings/tests/api_tests.rs` — Add tests for error/no-op paths and boundary conditions. Fixed in `7825d81`. - -- [x] [r2612251529](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251529) `specs/spec-000-rewrite/index.html` — Remove orphaned `#app` mount node. Fixed in `f70ba94`. -- [x] [r2645857695](https://github.com/flyingrobots/echo/pull/141#discussion_r2645857695) `specs/spec-000-rewrite/spec.md` — Replace “to add” with an explicit Phase-0 win condition. Fixed in `cf286e9`. -- [x] [r2612251537](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251537) `specs/spec-000-rewrite/src/lib.rs` — Remove redundant `#[allow(missing_docs)]` when the item is documented. Fixed in `f70ba94`. -- [x] [r2612251535](https://github.com/flyingrobots/echo/pull/141#discussion_r2612251535) `specs/spec-000-rewrite/src/lib.rs` — Same redundancy: doc comment + `#[allow(missing_docs)]`. Fixed in `f70ba94`. - -### PX — Agent Artifacts (Codex connector bot) - -- [x] [r2612244537](https://github.com/flyingrobots/echo/pull/141#discussion_r2612244537) Backend disconnect should stop ping loop. Fixed earlier in `970a4b5` (and refined in `89c2bb1`). -- [x] [r2612244530](https://github.com/flyingrobots/echo/pull/141#discussion_r2612244530) Gate Spec-000 wasm entrypoint/deps so host builds stay green. Fixed earlier in `2fec335`. diff --git a/docs/notes/xtask-wizard.md b/docs/notes/xtask-wizard.md index bb8f454c..c8113fe2 100644 --- a/docs/notes/xtask-wizard.md +++ b/docs/notes/xtask-wizard.md @@ -1,23 +1,26 @@ -# xtask “workday wizard” — concept note + +# xtask "workday wizard" — concept note Goal: a human-friendly `cargo xtask` (or `just`/`make` alias) that walks a contributor through starting and ending a work session, with automation hooks for branches, PRs, issues, and planning. ## Core flow ### Start session -- Prompt for intent/issue: pick from open GitHub issues (via gh CLI) or free text → writes to `docs/execution-plan.md` Today’s Intent and opens a draft entry in `docs/decision-log.md`. + +- Prompt for intent/issue: pick from open GitHub issues (via gh CLI) or free text. - Branch helper: suggest branch name (`echo/-`), create and checkout if approved. - Env checks: toolchain match, hooks installed (`make hooks`), `cargo fmt -- --check`/`clippy` optional preflight. ### During session + - Task DAG helper: load tasks from issue body / local `tasks.yaml`; compute simple priority/topo order (dependencies, P1/P0 tags). - Bench/test shortcuts: menu to run common commands (clippy, cargo test -p warp-core, bench targets). -- Docs guard assist: if runtime code touched, remind to update execution-plan + decision-log; offer to append templated entries. +- Docs guard assist: if runtime code touched, remind to update relevant specs/ADRs. ### End session -- Summarize changes: gather `git status`, staged/untracked hints; prompt for decision-log entry (Context/Decision/Rationale/Consequence). +- Summarize changes: gather `git status`, staged/untracked hints. - PR prep: prompt for PR title/body template (with issue closing keywords); optionally run `git commit` and `gh pr create`. - Issue hygiene: assign milestone/board/labels via gh CLI; auto-link PR to issue. @@ -35,7 +38,7 @@ Goal: a human-friendly `cargo xtask` (or `just`/`make` alias) that walks a contr ## Open questions - How much is automated vs. suggested (avoid surprising commits)? - Should Docs Guard be enforced via wizard or still via hooks? -- Where to store per-session summaries (keep in git via decision-log or external log)? +- Where to store per-session summaries (keep in git or external log)? ## Next steps - Prototype a minimal “start session” + “end session” flow with `gh` optional. diff --git a/docs/phase1-plan.md b/docs/phase1-plan.md index 0f0a6105..ea244a7a 100644 --- a/docs/phase1-plan.md +++ b/docs/phase1-plan.md @@ -115,7 +115,5 @@ Optimization roadmap once baseline is working: ## Documentation Checklist - Update `docs/warp-runtime-architecture.md` as rules/loop evolve. -- Append decision log entries per phase. -- Record demo outcomes in `docs/decision-log.md`, prefixing the Decision column with `Demo —` (e.g., `Demo 2 — Timeline hash verified`). Phase 1 completes when Demo 6 (Live Coding) runs atop the Rust WARP runtime with inspector tooling in place, using Rhai as the scripting layer. diff --git a/docs/procedures/EXTRACT-PR-COMMENTS.md b/docs/procedures/EXTRACT-PR-COMMENTS.md index 37f007eb..532d57ac 100644 --- a/docs/procedures/EXTRACT-PR-COMMENTS.md +++ b/docs/procedures/EXTRACT-PR-COMMENTS.md @@ -229,7 +229,7 @@ Example: ```bash # Suppose you have a single comment object (e.g., from /tmp/comments-latest.json): -COMMENT_PATH="docs/decision-log.md" +COMMENT_PATH="docs/METHODOLOGY.md" COMMENT_LINE=42 # Clamp the start line to 1 (sed doesn't like 0/negative ranges). diff --git a/docs/release-criteria.md b/docs/release-criteria.md index 68a0da59..cb63e1b4 100644 --- a/docs/release-criteria.md +++ b/docs/release-criteria.md @@ -15,7 +15,6 @@ Checklist for closing Phase 0.5 and starting Phase 1 implementation. - [ ] Deterministic config loader produces `configHash`. - [ ] Plugin manifest loader validates capabilities and records `pluginsManifestHash`. - [ ] Inspector JSONL writer produces canonical frames. -- [ ] Decision log updated with outcomes (including EPI bundle). - [ ] Documentation index current (spec map). Once all items checked, open Phase 1 milestone and migrate outstanding tasks to implementation backlog. diff --git a/docs/scheduler.md b/docs/scheduler.md index 23d85077..f91c6bad 100644 --- a/docs/scheduler.md +++ b/docs/scheduler.md @@ -23,7 +23,7 @@ This page is a landing map: “which scheduler doc should I read?” | Understand determinism + rewrite scheduling in Rust | `docs/spec-warp-core.md` | `docs/scheduler-warp-core.md` | | Validate `reserve()` correctness / determinism properties | `docs/scheduler-warp-core.md` | `crates/warp-core/src/scheduler.rs` tests + `crates/warp-core/tests/*` | | Benchmark rewrite scheduler throughput | `docs/scheduler-performance-warp-core.md` | `crates/warp-benches/benches/scheduler_drain.rs` | -| Understand the planned ECS/system scheduler | `docs/spec-scheduler.md` | `docs/spec-concurrency-and-authoring.md`, `docs/spec-codex-baby.md` | +| Understand the planned ECS/system scheduler | `docs/spec-scheduler.md` | `docs/spec-concurrency-and-authoring.md` | --- @@ -53,7 +53,6 @@ and coordinate systems (and future ECS/timeline concepts). Related docs: - Authoring/concurrency model: `docs/spec-concurrency-and-authoring.md` -- Codex’s Baby integration concepts: `docs/spec-codex-baby.md` --- diff --git a/docs/spec-canonical-inbox-sequencing.md b/docs/spec-canonical-inbox-sequencing.md new file mode 100644 index 00000000..8b243531 --- /dev/null +++ b/docs/spec-canonical-inbox-sequencing.md @@ -0,0 +1,197 @@ + + +# Spec: Canonical Inbox Sequencing + Deterministic Scheduler Tie-Break + +## 0) Purpose + +Guarantee that for a given tick, the full WARP graph is bit-identical across runs +that ingest the same set of intents in any order. + +This requires: +- intent identity is content-based, not sequence-based +- within-tick ordering is canonical (derived), not arrival-based +- conflict resolution is deterministic and order-independent +- hashing/enumeration is canonical (sorted) +- ledger entries are append-only (no event node deletion) + +This spec aligns with ADR-0003: ingress accepts intent_bytes, runtime assigns +canonical sequence numbers, idempotency is keyed by intent_id = H(intent_bytes), +and enumerations that influence state are canonical. + +## 1) Terms + +- intent_bytes: canonical bytes submitted via ingress. +- intent_id: H(intent_bytes) (content hash). +- seq: canonical sequence number assigned by runtime/kernel. +- tick: a kernel step where rewrites apply and materializations emit. +- footprint: the read/write set (or conflict domain) used by the scheduler to detect conflicts. + +## 2) Invariants + +I1 - Content identity + +Two intents are the "same intent" iff intent_bytes are byte-identical, therefore +intent_id matches. + +I2 - Idempotent ingress + +Ingress must be idempotent by intent_id -> seq_assigned. Retries return the +original seq. + +I3 - Arrival order is non-semantic (within a tick) + +Within a tick, the relative ordering of intents MUST NOT depend on arrival +order, insertion order, hash-map iteration order, or thread interleavings. + +I4 - Canonical enumeration + +Any enumeration that influences state, hashing, serialization, or replay MUST +be in canonical sorted order. + +I5 - Ledger is append-only + +Inbox/ledger **event nodes** are immutable and MUST NOT be deleted. + +Processing an event is modeled as **queue maintenance**: remove a `pending` +marker (edge or flag) so the event is no longer considered pending, without +removing the ledger entry itself. + +## 3) Data model requirements + +### 3.1 Ledger / Inbox entries + +Each pending inbox entry MUST carry: +- intent_id: Hash32 +- intent_bytes: Bytes (canonical) +- optional: seq: u64 (canonical rank / audit field; see §4) +- optional: tick_id (if your model persists tick partitioning) +- optional: priority_class: u8 (stable scheduler priority input; defaults to 128 if absent) + +Rule: seq is NOT part of identity. Identity is intent_id. + +Minimal implementation model (recommended for determinism): +- Ledger entry = immutable event node keyed by `intent_id` (or derived from it). +- Pending membership = `edge:pending` from `sim/inbox` → `event`. +- Applied/consumed = delete the pending edge (queue maintenance), keeping the + event node forever. + +### 3.2 Tick membership (important boundary) + +To get bit-identical results under permutations, the set of intents in each tick +must be the same across runs. + +For tests like DIND "permute and converge," enforce one of: +- ingest all intents before starting ticks (single-tick bucket), or +- include an explicit tick tag/hint inside intent_bytes so membership is + deterministic from content. + +If membership differs, you changed causality, and bit-identical full graphs are +not expected. + +## 4) Canonical ordering (and optional sequence assignment) algorithm + +### 4.1 When ordering/seq is assigned + +Canonical order is derived at the tick boundary for the set of pending intents +for that tick (or for the ledger segment being committed). + +If you persist `seq` for auditing/debugging, it is assigned at the same boundary +and MUST be a deterministic function of the pending set. + +### 4.2 Canonical ranking + +Given a tick's pending set P: +1) Deduplicate by intent_id (idempotency). +2) Sort intents by intent_id ascending (bytewise). +3) (Optional) Assign seq = 1..|P| in that sorted order. + +That is the canonical order. + +Consequence: ingesting intents in any order yields the same seq assignments, the +same node/edge insertion schedule, and the same full hash. + +### 4.3 Idempotency interaction + +If an intent is re-ingested: +- compute intent_id +- if already present (committed or pending), return DUPLICATE + seq_assigned and + DO NOT create a new inbox entry. + +## 5) Scheduler: deterministic conflict resolution + +### 5.1 Conflict detection + +Two intents conflict if their computed footprints overlap per existing rules. + +### 5.2 Deterministic tie-break key + +When choosing a winner among conflicting candidates, the scheduler MUST use a +stable tie-break key independent of evaluation order. + +Define: + +priority_key(intent) = ( + priority_class, // stable, explicit (e.g., system > user > background) + intent_id // stable content hash +) + +Then: +- The winner is the intent with **min(priority_key)** (ascending lexicographic order). +- Losers are **deferred** to the next tick (not rejected). This ensures eventual + delivery while maintaining determinism. Rejection is only permitted for malformed + intents that fail validation. + +If you need multi-phase scheduling, extend the tuple, but every field must be +stable and derived from content/state in a canonical way. + +### 5.3 Evaluation order must not leak + +Even if you compute footprints in parallel, the final chosen schedule must be +equivalent to: +- consider all pending intents +- compute (or cache) footprints +- select winners using priority_key ordering only + +No "first one we happened to see" logic. + +## 6) Graph construction + hashing canonicalization + +To make the whole WARP graph bit-identical, ensure these are canonical: +- node IDs for inbox entries: derive from intent_id alone (not tick_id, not seq). + This ensures the same intent always produces the same node ID regardless of when + it was ingested. +- pending edge IDs: derive from (inbox_root, intent_id) or an equivalent stable function +- edge insertion order: if edges are stored in vectors/lists, insert in canonical + sorted order +- attachment ordering: canonical sort +- any map iteration: never used directly; materialize keys, sort, then emit + +This is the same "sorted enumeration" rule ADR-0003 already states for reads; +apply it everywhere hashing touches. + +## 7) Required tests (prove it suite) + +T1 - Permutation invariance (full hash) +- Take a fixed set S of canonical intent bytes. +- Run N seeds; each seed shuffles ingestion order of S. +- Enforce same tick membership (e.g., ingest all before first tick). +- Assert: + - full graph hash identical across all seeds + - ledger/inbox node IDs identical + - seq assignments identical + +T2 - Conflict invariance +- Construct S where at least two intents conflict (overlapping footprints). +- Shuffle ingestion order. +- Assert the same winner intent_id is chosen (and same losers deferred), across + seeds. + +T3 - Idempotency invariance +- Ingest same intent twice (different arrival times, different threads). +- Assert no duplicate ledger entry; same seq returned. + +## 8) Implementation summary + +Move seq assignment from "ingest arrival" to "tick commit canonicalization" and +make the scheduler's winner selection purely a function of stable keys +(priority_class + intent_id). diff --git a/docs/spec-capabilities-and-security.md b/docs/spec-capabilities-and-security.md index 23c44f81..cbe8e87b 100644 --- a/docs/spec-capabilities-and-security.md +++ b/docs/spec-capabilities-and-security.md @@ -48,7 +48,7 @@ interface SecurityEnvelope { ### Signer Registry - `signerId` resolves to public key; stored in block manifest header. -- Registry modifications recorded in decision log. +- Registry modifications recorded in block manifest (see Serialization Protocol § Block Manifest). --- diff --git a/docs/spec-codex-baby.md b/docs/spec-codex-baby.md deleted file mode 100644 index 6bbcabf1..00000000 --- a/docs/spec-codex-baby.md +++ /dev/null @@ -1,229 +0,0 @@ - - -# Codex’s Baby Specification (Phase 0.5) - -Codex’s Baby (CB) is Echo’s deterministic event bus. It orchestrates simulation events, cross-branch messaging, and inspector telemetry while respecting causality, security, and the determinism invariants defined in Phase 0.5. - ---- - -## Terminology -- **Event** – immutable envelope describing a mutation request or signal. Commands are a subtype of events. -- **Phase Lane** – per-scheduler phase queue storing pending events. -- **Priority Lane** – optional high-priority sub-queue for engine-critical events. - -```ts -type EventKind = string; // e.g., "input/keyboard", "ai/proposal", "net/reliable" -``` - ---- - -## Event Envelope - -```ts -interface EventEnvelope { - readonly id: number; // monotonic per branch per tick - readonly kind: EventKind; - readonly chronos: ChronosTick; // target tick (>= current for same-branch) - readonly kairos: KairosBranchId; // target branch - readonly aionWeight?: number; - readonly payload: TPayload; - - // Determinism & causality - readonly prngSpan?: { seedStart: string; count: number }; - readonly readSet?: ReadKey[]; - readonly writeSet?: WriteKey[]; - readonly causeIds?: readonly string[]; // upstream event/diff hashes - - // Security & provenance - readonly caps?: readonly string[]; // capability tokens required by handlers - readonly envelopeHash?: string; // BLAKE3(canonical bytes) - readonly signature?: string; // optional Ed25519 signature - readonly signerId?: string; - - readonly metadata?: Record; // inspector notes only -} -``` - -ID semantics: -- Reset to 0 each tick per branch. -- Cross-branch mail records `bridgeSeq`, but delivery order remains `(chronos, id, bridgeSeq)`. -- Canonical encoding: sorted keys for maps/arrays, little-endian numeric fields, no timestamps in hash. - ---- - -## Queues & Lanes -Per scheduler phase, CB maintains a deterministic ring buffer with an optional priority lane. - -```ts -interface EventQueue { - readonly phase: SchedulerPhase; - priorityLane: RingBuffer; - normalLane: RingBuffer; - size: number; - capacity: number; - highWater: number; - immediateUses: number; -} -``` - -Dequeue order per phase: `priorityLane` FIFO, then `normalLane` FIFO. Capacities are configurable per lane; high-water marks tracked for inspector telemetry. - ---- - -## Handler Contract - -```ts -interface EventHandler { - readonly kind: EventKind; - readonly phase: SchedulerPhase; - readonly priority?: number; - readonly once?: boolean; - readonly requiresCaps?: readonly string[]; - (evt: EventEnvelope, ctx: EventContext): void; -} -``` - -Registration: -- Deterministic order captured in `handlerTableHash = BLAKE3(sorted(phase, kind, priority, registrationIndex))`. -- Hash recorded once per run for replay audits. - -Handlers may only run if `requiresCaps` ⊆ `evt.caps`; otherwise `ERR_CAPABILITY_DENIED` halts the tick deterministically. - ---- - -## Event Context - -```ts -interface EventContext { - readonly timeline: TimelineFingerprint; // { chronos, kairos, aion } - readonly rng: DeterministicRNG; // obeys evt.prngSpan if present - - enqueue(phase: SchedulerPhase, evt: EventEnvelope): void; - forkBranch(fromNode?: NodeId): BranchId; - sendCross(evt: EventEnvelope): void; // wraps Temporal Bridge -} -``` - -Rules: -- `enqueue` targets same-branch events with `evt.chronos >= currentTick`. -- `sendCross` is the only sanctioned cross-timeline route. -- If `evt.prngSpan` provided, handler must consume exactly `count` draws; mismatch raises `ERR_PRNG_MISMATCH`. - ---- - -## Temporal Bridge - -Features: -- **Exactly-once toggle** via dedup set `seenEnvelopes: Set` on receiver. -- **Retro delivery**: if `evt.chronos < head(target).chronos`, spawn retro branch β′ from LCA, rewrite target, tag `evt.metadata.retro = true`. -- **Reroute on collapse**: if branch collapses before delivery, forward to merge target and record `evt.metadata.reroutedFrom`. -- **Paradox pre-check**: if `evt.writeSet` intersects reads applied since LCA, route to paradox handler/quarantine and increment entropy by `wM + wP`. - -Delivery policy defaults to at-least-once; exactly-once enables dedup. - ---- - -## Immediate Channel -- Whitelist event kinds (`engine/halt`, `engine/diagnostic`, etc.). -- Per-tick budget; exceeding emits `ERR_IMMEDIATE_BUDGET_EXCEEDED` and halts deterministically. - ---- - -## Backpressure Policies - -```ts -type BackpressureMode = "throw" | "dropOldest" | "dropNewest"; -``` - -- Development default: `throw` (abort tick with `ERR_QUEUE_OVERFLOW`). -- Production defaults: `dropNewest` for `pre_update`, `dropOldest` for `update`/`post_update`. -- Each drop records `DropRecord { phase, kind, id, chronos }` added to the run manifest; replay reproduces drop order. - ---- - -## Inspector Packet - -```ts -interface CBInspectorFrame { - tick: ChronosTick; - branch: KairosBranchId; - queues: { - [phase in SchedulerPhase]?: { - size: number; - capacity: number; - highWater: number; - enqueued: number; - dispatched: number; - dropped: number; - immediateUses?: number; - p50Latency: number; // enqueue→dispatch ticks - p95Latency: number; - kindsTopN: Array<{ kind: EventKind; count: number }>; - }; - }; -} -``` - -Emitted after `timeline_flush` so metrics do not perturb simulation. - ---- - -## Determinism Hooks -- Dispatch order per phase: FIFO by `(chronos, id, bridgeSeq)` with deterministic tie-break by registration order and priority. -- PRNG spans enforced; mismatches halt tick. -- `readSet`/`writeSet` recorded for causality graph and paradox detection. -- Security envelopes verified before handler invocation; tampering emits `ERR_ENVELOPE_TAMPERED`. - ---- - -## Capability & Security -- `requiresCaps` enforced at dispatch. -- If `signature` present, verify `Ed25519(signature, envelopeHash)`; failure halts deterministically. -- Capability violations and tampering log deterministic error nodes. - ---- - -## Public API Surface - -```ts -interface CodexBaby { - on(handler: EventHandler): void; - off(handler: EventHandler): void; - - emit(phase: SchedulerPhase, evt: EventEnvelope): void; // same branch - emitCross(evt: EventEnvelope): void; // via bridge - - flush(phase: SchedulerPhase, ctx: EventContext): void; // scheduler hook - stats(): CBInspectorFrame; // inspector packet -} -``` - -All mutations route through CB; external systems observe only inspector packets and deterministic manifests. - ---- - -## Implementation Checklist -1. **Rename & Canonicalize** – adopt `EventEnvelope`, implement canonical encoder + BLAKE3 hash. -2. **Handler Table Hash** – compute once at startup, record in run manifest. -3. **Backpressure & Drop Records** – per-queue policies with deterministic drop manifests. -4. **PRNG Span Enforcement** – wrap handlers to track draws when `prngSpan` present. -5. **Temporal Bridge Enhancements** – dedup, retro branch creation, reroute on collapse, paradox pre-check. -6. **Capability Gate** – enforce `requiresCaps`, emit errors on violation. -7. **Inspector Packet** – produce `CBInspectorFrame` with latency/top-N metrics. -8. **Immediate Channel Budget** – whitelist + counter enforcement. -9. **Error Codes** – define deterministic errors: `ERR_QUEUE_OVERFLOW`, `ERR_CAPABILITY_DENIED`, `ERR_ENVELOPE_TAMPERED`, `ERR_IMMEDIATE_BUDGET_EXCEEDED`, `ERR_PRNG_MISMATCH`. -10. **Docs & Samples** – provide example flow (input event → system → diff) with read/write sets and zero PRNG span. - ---- - -## Test Matrix -- **Determinism:** Same event set (with PRNG spans) across Node/Chromium/WebKit ⇒ identical `worldHash` and handlerTableHash. -- **Backpressure:** Force overflow for each mode; replay reproduces drop manifest. -- **Temporal Bridge:** Cross-branch retro delivery creates β′ correctly and respects paradox quarantine. -- **Security:** Capability mismatch raises deterministic error; tampered signatures rejected. -- **Immediate Channel:** Budget exceed halts deterministically; under budget yields identical final state. -- **Inspector Metrics:** Latencies stable; inspector calls have no side effects. - ---- - -Adhering to this spec aligns Codex’s Baby with the causality layer and determinism guarantees established for the branch tree. diff --git a/docs/spec-ecs-storage.md b/docs/spec-ecs-storage.md index 7f2b5203..841a76a0 100644 --- a/docs/spec-ecs-storage.md +++ b/docs/spec-ecs-storage.md @@ -150,4 +150,4 @@ Chunk { - Interaction with scripting languages (e.g., user-defined components) — need extension points for custom allocation? - Evaluate fallback for environments lacking `SharedArrayBuffer` (maybe optional). -Document updates should flow into implementation tickets and tests (see execution-plan backlog). Once verified, record results in the decision log. +Document updates should flow into implementation tickets and tests (see GitHub Issues). diff --git a/docs/spec-scheduler.md b/docs/spec-scheduler.md index 1d778ccc..7dd59d5b 100644 --- a/docs/spec-scheduler.md +++ b/docs/spec-scheduler.md @@ -274,4 +274,4 @@ Objective: validate scheduler behavior and complexity under realistic dependency - Strategy for cross-branch scheduling: separate scheduler per branch vs shared graph with branch-specific execution queues. - Should initialization phase run lazily when systems added mid-game, or strictly at startup? -Document updates feed into implementation tasks (`execution-plan` backlog). Once implemented, update the decision log with real-world adjustments. +Document updates feed into implementation tasks (GitHub Issues). diff --git a/docs/spec-warp-core.md b/docs/spec-warp-core.md index 332ef7b6..8e70603d 100644 --- a/docs/spec-warp-core.md +++ b/docs/spec-warp-core.md @@ -60,7 +60,7 @@ The stable public surface is intentionally exposed via re-exports from - **Scheduling & MWMR:** `Footprint`, `PortKey`. - **Runtime:** `Engine`, `EngineError`, `ApplyResult`. - **Boundary artifacts:** `Snapshot`, `TickReceipt`, `WarpTickPatchV1`, `WarpOp`, `SlotId`. -- **Utilities:** demo builders (`build_motion_demo_engine`), payload helpers (`encode_motion_atom_payload`, etc.). +- **Utilities:** payload helpers (`encode_motion_atom_payload`, etc.). This doc describes those pieces and how they fit. @@ -359,13 +359,14 @@ Start here (in order): ### 11.1 Minimal (single instance) -For a working “known-good” bootstrap, use the demo helper: +For a working "known-good" bootstrap, use the demo helper from `echo-dry-tests`: ```rust use warp_core::{ - build_motion_demo_engine, encode_motion_atom_payload, make_node_id, make_type_id, - ApplyResult, AttachmentValue, NodeRecord, MOTION_RULE_NAME, + encode_motion_atom_payload, make_node_id, make_type_id, + ApplyResult, AttachmentValue, NodeRecord, }; +use echo_dry_tests::{build_motion_demo_engine, MOTION_RULE_NAME}; let mut engine = build_motion_demo_engine(); diff --git a/docs/spec-world-api.md b/docs/spec-world-api.md index 8972a167..8711a256 100644 --- a/docs/spec-world-api.md +++ b/docs/spec-world-api.md @@ -83,7 +83,7 @@ api.emit("update", { --- ## Change Management -- API changes logged in decision log with version bump. +- API changes require version bump. - Deprecated methods remain no-op until next major release. - Extensions (e.g., debug utilities) provided under `api.debug.*` and marked unstable. diff --git a/docs/tasks.md b/docs/tasks.md index ae3f8164..6bdde4ae 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -11,4 +11,4 @@ - [x] Client wiring: bidirectional tool connection (receive + publish); surface authority/epoch errors as notifications. - [x] Demo path: doc for one session-service + two viewers (publisher + subscriber) showing shared WARP changes (`docs/guide/wvp-demo.md`). - [x] Tests: protocol conformance (authority rejection, gapless enforcement, dirty-loop behavior, toggle respect) and integration test with two clients + server loopback. (Tracking: #169) -- [x] Docs sync: update execution-plan intents and decision-log entries as slices land. +- [x] Docs sync: update GitHub Issues as slices land. diff --git a/docs/two-lane-abi.md b/docs/two-lane-abi.md new file mode 100644 index 00000000..e8b5db98 --- /dev/null +++ b/docs/two-lane-abi.md @@ -0,0 +1,53 @@ + + +# Two-Lane ABI Design (Control Plane vs. Data Plane) + +Status: **Phase 1 Complete** + +The Echo WASM ABI is split into two distinct logical lanes to separate stable, +schema-driven application logic from the low-level mechanical plumbing of the kernel. + +## 1. Control Plane (The "Handshake" Lane) + +The Control Plane is used at boot time and during structural transitions to ensure +the host and the kernel are speaking the same language. + +### Registry Handshake +- **`get_registry_info()`**: Returns canonical CBOR bytes containing `schema_sha256_hex`, + `codec_id`, and `registry_version`. +- **Purpose**: The host verifies these fields against its own generated manifest + (from `wesley-generator-vue`) before calling any other functions. +- **Fail-Fast**: If the hash or version mismatches, the host refuses to mount + to prevent undefined behavior and ledger corruption. + +### Metadata Accessors +- **`get_codec_id()`**, **`get_registry_version()`**, **`get_schema_sha256_hex()`**: + Helper accessors for debugging and runtime inspection. + +## 2. Data Plane (The "Execution" Lane) + +The Data Plane handles the high-frequency flow of state changes and information retrieval. + +### Input Lane (Intents) +- **`dispatch_intent(bytes)`**: Enqueues an opaque, pre-validated command payload + into the kernel's inbox. +- **Envelope**: The host uses `encode_command(op_id, payload)` to wrap app-specific + data in a canonical CBOR structure that the kernel knows how to route. + +### Output Lane (Projection) +- **`step(budget)`**: Advances the causal clock. Returns a `StepResult`. +- **`drain_view_ops()`**: Returns an array of `ViewOp`s emitted during the + preceding steps. These drive the host UI (e.g., toasts, navigation). + +### Query Lane (Read-Only) +- **`execute_query(id, vars)`**: Executes a schema-validated, side-effect-free + lookup against the current graph. +- **Purity Guard**: The ABI layer enforces that only operations marked as `Query` + in the registry can be invoked here. + +## 3. Ledger Reconciliation + +The separation of lanes allows the kernel to reconcile the **Intent Log** (Data Plane) +against the **Schema Version** (Control Plane) recorded in the provenance layer. +Future versions will include the `registry_version` in every tick header to allow +for multi-version playback. diff --git a/docs/warp-demo-roadmap.md b/docs/warp-demo-roadmap.md index 2d272742..803aa11e 100644 --- a/docs/warp-demo-roadmap.md +++ b/docs/warp-demo-roadmap.md @@ -22,7 +22,7 @@ This document captures the interactive demos and performance milestones we want - Criterion-based benches exercise flat, chained, branching, and timeline-flush scenarios (mirrors `docs/scheduler-benchmarks.md`). - Success criteria: median tick time < 0.5 ms for toy workload (100 entities, 10 rules); percentile tails recorded. -- Bench harness outputs JSON summaries (mean, median, std dev) consumed by the inspector and appended to the decision log. +- Bench harness outputs JSON summaries (mean, median, std dev) consumed by the inspector. - Deterministic PRNG seeds recorded so benches are reproducible across CI machines. ## Demo 3: Timeline Fork/Merge Replay @@ -74,7 +74,7 @@ This document captures the interactive demos and performance milestones we want | 1F | Demo dashboards | Inspector frame overlays, JSON ingestion | -**Prerequisites:** BLAKE3 hashing utilities, deterministic PRNG module, snapshot serialiser, inspector graph viewer, documentation workflow (`docs/decision-log.md`) for logging demo outcomes, CI runners with wasm/criterion toolchains. +**Prerequisites:** BLAKE3 hashing utilities, deterministic PRNG module, snapshot serialiser, inspector graph viewer, CI runners with wasm/criterion toolchains. **Timeline:** diff --git a/docs/workflows.md b/docs/workflows.md index 27a81572..2f363866 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -10,9 +10,8 @@ This doc is the “official workflow index” for Echo: how we work, what invari ## Session Workflow -- Start a work session by updating *Today’s Intent* in `docs/execution-plan.md`. -- During work, record decisions and blockers in `docs/decision-log.md` (canonical), and keep `docs/execution-plan.md` in sync. -- Before opening a PR, confirm the docs guard requirements below. +- Record architectural decisions in ADRs (`docs/adr/`) or PR descriptions. +- Before opening a PR, run the validation workflow below. --- @@ -36,10 +35,7 @@ cargo test --workspace cargo clippy --all-targets -- -D warnings -D missing_docs ``` -Docs guard (CI enforces this): -- If a PR touches **non-doc** files, it must also update: - - `docs/execution-plan.md` - - `docs/decision-log.md` +Validation commands: --- diff --git a/packages/wesley-generator-vue/package.json b/packages/wesley-generator-vue/package.json new file mode 100644 index 00000000..2824849c --- /dev/null +++ b/packages/wesley-generator-vue/package.json @@ -0,0 +1,8 @@ +{ + "name": "@wesley/generator-vue", + "version": "0.1.0", + "description": "Generate Vue client artifacts (ops.ts, schemas.ts, client.ts, useEcho.ts) from Echo Ops IR", + "type": "module", + "main": "src/index.mjs", + "license": "Apache-2.0" +} diff --git a/packages/wesley-generator-vue/src/index.mjs b/packages/wesley-generator-vue/src/index.mjs new file mode 100644 index 00000000..abe69334 --- /dev/null +++ b/packages/wesley-generator-vue/src/index.mjs @@ -0,0 +1,326 @@ +import path from "node:path"; + +/** + * Generate Vue artifacts (ops.ts, schemas.ts, client.ts, useEcho.ts) from Echo Ops IR. + * @param {object} ir - Echo Ops IR JSON (contains ops[], types[], schema_sha256, codec_id). + * @param {object} options - { outDir?: string } + * @returns {{ files: { path: string, content: string }[] }} + */ +export async function generateVue(ir, options = {}) { + if (!ir || !Array.isArray(ir.ops)) { + throw new Error("@wesley/generator-vue requires Echo Ops IR with `ops[]`"); + } + const outDir = options.outDir ?? "src/wesley/generated"; + const files = []; + files.push({ path: path.join(outDir, "ops.ts"), content: emitOps(ir) }); + files.push({ path: path.join(outDir, "schemas.ts"), content: emitSchemas(ir) }); + files.push({ path: path.join(outDir, "client.ts"), content: emitClient(ir) }); + files.push({ path: path.join(outDir, "useEcho.ts"), content: emitUseEcho() }); + return { files }; +} + +// ----------------------- emitOps ----------------------- + +function emitOps(ir) { + const schemaSha = ir.schema_sha256 ?? "unknown"; + const codecId = ir.codec_id ?? "unknown"; + const regVer = ir.registry_version ?? 0; + const ops = [...ir.ops].sort((a, b) => { + const ak = String(a.kind).toUpperCase(); + const bk = String(b.kind).toUpperCase(); + if (ak !== bk) return ak.localeCompare(bk); + const an = String(a.name); + const bn = String(b.name); + if (an !== bn) return an.localeCompare(bn); + return (a.op_id ?? 0) - (b.op_id ?? 0); + }); + + const lines = []; + lines.push("// AUTO-GENERATED. DO NOT EDIT."); + lines.push(`// schema_sha256: ${schemaSha}`); + lines.push(`// codec_id: ${codecId}`); + lines.push(`// registry_version: ${regVer}`); + lines.push(""); + lines.push(`export const SCHEMA_SHA256 = ${JSON.stringify(schemaSha)};`); + lines.push(`export const CODEC_ID = ${JSON.stringify(codecId)};`); + lines.push(`export const REGISTRY_VERSION = ${Number(regVer)};`); + lines.push(""); + + for (const op of ops) { + const kind = String(op.kind).toUpperCase(); + const constName = + kind === "QUERY" + ? `QUERY_${toScreamingSnake(op.name)}_ID` + : `MUT_${toScreamingSnake(op.name)}_ID`; + lines.push(`export const ${constName} = ${op.op_id} as const;`); + } + + lines.push(""); + lines.push(`export type OpKind = "QUERY" | "MUTATION";`); + lines.push(`export type OpDef = { kind: OpKind; name: string; opId: number };`); + lines.push(""); + lines.push("export const OPS: readonly OpDef[] = ["); + for (const op of ops) { + const kind = String(op.kind).toUpperCase(); + lines.push( + ` { kind: ${JSON.stringify(kind)}, name: ${JSON.stringify(op.name)}, opId: ${op.op_id} },` + ); + } + lines.push("] as const;"); + lines.push(""); + lines.push("export function findOpId(kind: OpKind, name: string): number | undefined {"); + lines.push(" const hit = OPS.find((o) => o.kind === kind && o.name === name);"); + lines.push(" return hit?.opId;"); + lines.push("}"); + lines.push(""); + return lines.join("\n"); +} + +// ----------------------- emitSchemas ----------------------- + +function emitSchemas(ir) { + const schemaSha = ir.schema_sha256 ?? "unknown"; + const codecId = ir.codec_id ?? "unknown"; + const regVer = ir.registry_version ?? 0; + const types = ir.types ?? []; + const ops = ir.ops ?? []; + + const lines = []; + lines.push("// AUTO-GENERATED. DO NOT EDIT."); + lines.push(`// schema_sha256: ${schemaSha}`); + lines.push(`// codec_id: ${codecId}`); + lines.push(`// registry_version: ${regVer}`); + lines.push(""); + lines.push('import { z } from "zod";'); + lines.push(""); + + const typeMap = new Map(types.map((t) => [t.name, t])); + + // ENUMs + for (const t of types) { + if (t.kind !== "ENUM") continue; + const values = (t.values ?? []).map((v) => JSON.stringify(v)).join(", "); + lines.push(`export const ${schemaName(t.name)} = z.enum([${values}]);`); + } + if (types.some((t) => t.kind === "ENUM")) lines.push(""); + + // OBJECTs + for (const t of types) { + if (t.kind !== "OBJECT") continue; + const fields = (t.fields ?? []).map((f) => { + const schemaExpr = wrapField(f, typeMap); + return ` ${JSON.stringify(f.name)}: ${schemaExpr},`; + }); + lines.push( + `export const ${schemaName(t.name)} = z.object({\n${fields.join("\n")}\n}).strict();` + ); + } + if (types.some((t) => t.kind === "OBJECT")) lines.push(""); + + // Op var/result schemas + lines.push("// Operation variable/result schemas"); + for (const op of ops) { + const varsSchemaName = `${pascal(op.name)}VarsSchema`; + const resultSchemaName = `${pascal(op.name)}ResultSchema`; + const args = op.args ?? []; + const argLines = args.map((a) => { + const schemaExpr = wrapArg(a, typeMap); + return ` ${JSON.stringify(a.name)}: ${schemaExpr},`; + }); + lines.push(`export const ${varsSchemaName} = z.object({\n${argLines.join("\n")}\n}).strict();`); + if (op.result_type) { + lines.push(`export const ${resultSchemaName} = ${refType(op.result_type, typeMap)};`); + } else { + lines.push(`export const ${resultSchemaName} = z.undefined();`); + } + lines.push(""); + } + + return lines.join("\n"); +} + +function refType(name, typeMap) { + if (isScalar(name)) return scalarSchema(name); + if (!typeMap.has(name)) throw new Error(`Unknown type: ${name}`); + return schemaName(name); +} +function wrapField(f, typeMap) { + return wrapType(f.type, !!f.list, !!f.required, typeMap); +} +function wrapArg(a, typeMap) { + return wrapType(a.type, !!a.list, !!a.required, typeMap); +} +function wrapType(typeName, list, required, typeMap) { + let expr = refType(typeName, typeMap); + if (list) expr = `z.array(${expr})`; + if (!required) expr = `${expr}.optional()`; + return expr; +} +function schemaName(name) { + return `${name}Schema`; +} +function isScalar(t) { + return t === "String" || t === "Boolean" || t === "Int" || t === "Float" || t === "ID"; +} +function scalarSchema(t) { + switch (t) { + case "String": + case "ID": + return "z.string()"; + case "Boolean": + return "z.boolean()"; + case "Int": + return "z.number().int()"; + case "Float": + return "z.number()"; + default: + throw new Error(`Unknown scalar: ${t}`); + } +} + +// ----------------------- emitClient ----------------------- + +function emitClient(ir) { + const schemaSha = ir.schema_sha256 ?? "unknown"; + const codecId = ir.codec_id ?? "unknown"; + const ops = ir.ops ?? []; + const queries = ops.filter((o) => String(o.kind).toUpperCase() === "QUERY"); + const muts = ops.filter((o) => String(o.kind).toUpperCase() === "MUTATION"); + + const lines = []; + lines.push("// AUTO-GENERATED. DO NOT EDIT."); + lines.push(`// schema_sha256: ${schemaSha}`); + lines.push(`// codec_id: ${codecId}`); + lines.push(""); + lines.push('import {'); + lines.push(' CODEC_ID, SCHEMA_SHA256, REGISTRY_VERSION,'); + for (const op of ops) { + const kind = String(op.kind).toUpperCase(); + const constName = + kind === "QUERY" + ? `QUERY_${toScreamingSnake(op.name)}_ID` + : `MUT_${toScreamingSnake(op.name)}_ID`; + lines.push(` ${constName},`); + } + lines.push('} from "./ops";'); + lines.push('import {'); + for (const op of ops) { + const name = pascal(op.name); + lines.push(` ${name}VarsSchema,`); + lines.push(` ${name}ResultSchema,`); + } + lines.push('} from "./schemas";'); + lines.push(""); + lines.push("export type Bytes = Uint8Array;"); + lines.push(""); + lines.push("export interface EchoWasm {"); + lines.push(" dispatch_intent(intentBytes: Bytes): void;"); + lines.push(" step(stepBudget: number): Bytes; // StepResult"); + lines.push(" drain_view_ops(): any; // ViewOp[]"); + lines.push(" get_ledger(): any; // (Snapshot, Receipt, Patch)[]"); + lines.push(" jump_to_tick(tickIndex: number): void;"); + lines.push(" render_state(): void;"); + lines.push(" get_head(): Bytes; // HeadInfo"); + lines.push(""); + lines.push(" execute_query(queryId: number, varsBytes: Bytes): Bytes;"); + lines.push(" encode_command(opId: number, payload: unknown): Bytes;"); + lines.push(" encode_query_vars(queryId: number, vars: unknown): Bytes;"); + lines.push(""); + lines.push(" get_registry_info?(): Bytes;"); + lines.push("}"); + lines.push(""); + lines.push("export type RegistryInfo = { schema_sha256: string; codec_id: string; registry_version: number };"); + lines.push(""); + lines.push("export class WesleyClient {"); + lines.push(" constructor(private wasm: EchoWasm) {}"); + lines.push(""); + lines.push(" verifyRegistry(decodeRegistryInfo?: (bytes: Bytes) => RegistryInfo) {"); + lines.push(" if (!this.wasm.get_registry_info || !decodeRegistryInfo) return;"); + lines.push(" const info = decodeRegistryInfo(this.wasm.get_registry_info());"); + lines.push(" if (info.schema_sha256 !== SCHEMA_SHA256) throw new Error('Schema hash mismatch');"); + lines.push(" if (info.codec_id !== CODEC_ID) throw new Error('Codec mismatch');"); + lines.push(" if (info.registry_version !== REGISTRY_VERSION) throw new Error('Registry version mismatch');"); + lines.push(" }"); + lines.push(""); + lines.push(" getLedger() {"); + lines.push(" return this.wasm.get_ledger();"); + lines.push(" }"); + lines.push(""); + lines.push(" jumpToTick(index: number) {"); + lines.push(" this.wasm.jump_to_tick(index);"); + lines.push(" }"); + lines.push(""); + lines.push(" renderState() {"); + lines.push(" this.wasm.render_state();"); + lines.push(" }"); + lines.push(""); + for (const q of queries) { const fn = `query${pascal(q.name)}`; + const constName = `QUERY_${toScreamingSnake(q.name)}_ID`; + lines.push(` ${fn}(vars: unknown = {}) {`); + lines.push(` const parsed = ${pascal(q.name)}VarsSchema.parse(vars);`); + lines.push(` const varsBytes = this.wasm.encode_query_vars(${constName}, parsed);`); + lines.push(` const resultBytes = this.wasm.execute_query(${constName}, varsBytes);`); + lines.push(` return { bytes: resultBytes, schema: ${pascal(q.name)}ResultSchema };`); + lines.push(" }"); + lines.push(""); + } + for (const m of muts) { + const fn = `dispatch${pascal(m.name)}`; + const constName = `MUT_${toScreamingSnake(m.name)}_ID`; + const args = (m.args ?? []).map((a) => safeIdent(a.name)).join(", "); + const payloadObj = + (m.args ?? []).length === 0 + ? "{}" + : `{ ${(m.args ?? []).map((a) => `${safeIdent(a.name)}: ${safeIdent(a.name)}`).join(", ")} }`; + lines.push(` ${fn}(${args}) {`); + lines.push(` const payload = ${pascal(m.name)}VarsSchema.parse(${payloadObj});`); + lines.push(` const bytes = this.wasm.encode_command(${constName}, payload);`); + lines.push(" this.wasm.dispatch_intent(bytes);"); + lines.push(" }"); + lines.push(""); + } + lines.push("}"); + lines.push(""); + return lines.join("\n"); +} + +// ----------------------- emitUseEcho ----------------------- + +function emitUseEcho() { + return `// AUTO-GENERATED scaffold. You will likely extend this.\n` + + `import { WesleyClient } from "./client";\n\n` + + `export function useEcho(wasm) {\n` + + ` const client = new WesleyClient(wasm);\n` + + ` const pump = (budget = 1000) => {\n` + + ` const res = wasm.step(budget);\n` + + ` // TODO: decode StepResult and apply ViewOps via wasm.drain_view_ops()\n` + + ` return res;\n` + + ` };\n` + + ` return { client, pump };\n` + + `}\n`; +} + +// ----------------------- helpers ----------------------- + +function pascal(name) { + return String(name) + .replace(/[^A-Za-z0-9]+/g, " ") + .trim() + .split(/\s+/) + .map((w) => w.slice(0, 1).toUpperCase() + w.slice(1)) + .join(""); +} + +function toScreamingSnake(name) { + return String(name) + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + .replace(/[^A-Za-z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") + .toUpperCase(); +} + +function safeIdent(name) { + const s = String(name).replace(/[^A-Za-z0-9_]/g, "_"); + if (!/^[A-Za-z_]/.test(s)) return `_${s}`; + return s; +} diff --git a/scripts/ban-globals.sh b/scripts/ban-globals.sh new file mode 100755 index 00000000..78216d3e --- /dev/null +++ b/scripts/ban-globals.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +set -euo pipefail + +# Ban global state patterns in Echo/WARP core crates. +# Goal: no hidden wiring, no init-order dependency, no mutable process-wide state. +# +# Allowed: +# - const +# - immutable static data (e.g. magic bytes, lookup tables) +# +# Forbidden: +# - OnceLock/LazyLock/lazy_static/once_cell/thread_local/static mut +# - "install_*" global init patterns (heuristic) +# +# Usage: +# ./scripts/ban-globals.sh +# +# Optional env: +# BAN_GLOBALS_PATHS="crates/warp-core crates/warp-wasm crates/echo-wasm-abi" +# BAN_GLOBALS_ALLOWLIST=".ban-globals-allowlist" + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +PATHS_DEFAULT="crates/warp-core crates/warp-wasm crates/echo-wasm-abi" +PATHS="${BAN_GLOBALS_PATHS:-$PATHS_DEFAULT}" + +ALLOWLIST="${BAN_GLOBALS_ALLOWLIST:-.ban-globals-allowlist}" + +# ripgrep is fast and consistent +if ! command -v rg >/dev/null 2>&1; then + echo "ERROR: ripgrep (rg) is required." >&2 + exit 2 +fi + +# Patterns are conservative on purpose. +# If you truly need an exception, add an allowlist line with a justification. +PATTERNS=( + '\bOnceLock\b' + '\bLazyLock\b' + '\blazy_static!\b' + '\bonce_cell\b' + '\bthread_local!\b' + '\bstatic mut\b' + '\binstall_[a-zA-Z0-9_]*\b' +) + +echo "ban-globals: scanning paths:" +for p in $PATHS; do echo " - $p"; done +echo + +# Build rg args +RG_ARGS=(--hidden --no-ignore --glob '!.git/*' --glob '!target/*' --glob '!**/node_modules/*') + +# Apply allowlist as inverted matches (each line is a regex or fixed substring) +# Allowlist format: +# \t +# or: +# +ALLOW_RG_EXCLUDES=() +if [[ -f "$ALLOWLIST" ]]; then + # Read first column (pattern) per line, ignore comments + while IFS= read -r line; do + [[ -z "$line" ]] && continue + [[ "$line" =~ ^# ]] && continue + pat="${line%%$'\t'*}" + pat="${pat%% *}" + [[ -z "$pat" ]] && continue + # Exclude lines matching allowlisted pattern + ALLOW_RG_EXCLUDES+=(--glob "!$pat") + done < "$ALLOWLIST" +fi + +violations=0 + +for pat in "${PATTERNS[@]}"; do + echo "Checking: $pat" + # We can't "glob exclude by line"; allowlist is file-level. Keep it simple: + # If you need surgical exceptions, prefer moving code or refactoring. + if rg "${RG_ARGS[@]}" "${ALLOW_RG_EXCLUDES[@]}" -n -S "$pat" $PATHS; then + echo + violations=$((violations+1)) + else + echo " OK" + fi + echo + done + +if [[ $violations -ne 0 ]]; then + echo "ban-globals: FAILED ($violations pattern group(s) matched)." + echo "Fix the code or (rarely) justify an exception in $ALLOWLIST." + exit 1 +fi + +echo "ban-globals: PASSED." diff --git a/scripts/ban-nondeterminism.sh b/scripts/ban-nondeterminism.sh new file mode 100755 index 00000000..18dba5fc --- /dev/null +++ b/scripts/ban-nondeterminism.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +set -euo pipefail + +# Determinism Drill Sergeant: ban nondeterministic APIs and patterns +# +# Usage: +# ./scripts/ban-nondeterminism.sh +# +# Optional env: +# DETERMINISM_PATHS="crates/warp-core crates/warp-wasm crates/echo-wasm-abi" +# DETERMINISM_ALLOWLIST=".ban-nondeterminism-allowlist" + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +if ! command -v rg >/dev/null 2>&1; then + echo "ERROR: ripgrep (rg) is required." >&2 + exit 2 +fi + +PATHS_DEFAULT="crates/warp-core crates/warp-wasm crates/echo-wasm-abi" +PATHS="${DETERMINISM_PATHS:-$PATHS_DEFAULT}" + +ALLOWLIST="${DETERMINISM_ALLOWLIST:-.ban-nondeterminism-allowlist}" + +RG_ARGS=(--hidden --no-ignore --glob '!.git/*' --glob '!target/*' --glob '!**/node_modules/*') + +# You can allow file-level exceptions via allowlist (keep it tiny). +ALLOW_GLOBS=() +if [[ -f "$ALLOWLIST" ]]; then + while IFS= read -r line; do + [[ -z "$line" ]] && continue + [[ "$line" =~ ^# ]] && continue + pat="${line%%$'\t'*}" + pat="${pat%% *}" + [[ -z "$pat" ]] && continue + ALLOW_GLOBS+=(--glob "!$pat") + done < "$ALLOWLIST" +fi + +# Patterns: conservative and intentionally annoying. +# If you hit a false positive, refactor; don't immediately allowlist. +PATTERNS=( + # Time / entropy (core determinism killers) + '\bstd::time::SystemTime\b' + '\bSystemTime::now\b' + '\bstd::time::Instant\b' + '\bInstant::now\b' + '\bstd::thread::sleep\b' + '\b(tokio|async_std)::time::sleep\b' + + # Randomness + '\brand::\b' + '\bgetrandom::\b' + '\bfastrand::\b' + + # Unordered containers that will betray you if they cross a boundary + '\bstd::collections::HashMap\b' + '\bstd::collections::HashSet\b' + '\bhashbrown::HashMap\b' + '\bhashbrown::HashSet\b' + + # JSON & “helpful” serialization in core paths + '\bserde_json::\b' + '\bserde_wasm_bindgen::\b' + + # Float nondeterminism hotspots (you can tune these) + '\b(f32|f64)::NAN\b' + '\b(f32|f64)::INFINITY\b' + '\b(f32|f64)::NEG_INFINITY\b' + '\.sin\(' + '\.cos\(' + '\.tan\(' + '\.sqrt\(' + '\.pow[f]?\(' + + # Host/environment variability + '\bstd::env::\b' + '\bstd::fs::\b' + '\bstd::process::\b' + + # Concurrency primitives (optional—uncomment if you want core to be single-thread-only) + # '\bstd::sync::Mutex\b' + # '\bstd::sync::RwLock\b' + # '\bstd::sync::atomic::\b' +) + +echo "ban-nondeterminism: scanning paths:" +for p in $PATHS; do echo " - $p"; done +echo + +violations=0 +for pat in "${PATTERNS[@]}"; do + echo "Checking: $pat" + if rg "${RG_ARGS[@]}" "${ALLOW_GLOBS[@]}" -n -S "$pat" $PATHS; then + echo + violations=$((violations+1)) + else + echo " OK" + fi + echo + done + +if [[ $violations -ne 0 ]]; then + echo "ban-nondeterminism: FAILED ($violations pattern group(s) matched)." + echo "Fix the code or (rarely) justify an exception in $ALLOWLIST." + exit 1 +fi + +echo "ban-nondeterminism: PASSED." diff --git a/scripts/ban-unordered-abi.sh b/scripts/ban-unordered-abi.sh new file mode 100755 index 00000000..c42a5fc4 --- /dev/null +++ b/scripts/ban-unordered-abi.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +if ! command -v rg >/dev/null 2>&1; then + echo "ERROR: ripgrep (rg) is required." >&2 + exit 2 +fi + +# Adjust these to your repo conventions +ABI_HINTS=( + "abi" + "codec" + "message" + "frame" + "packet" + "envelope" + "dto" + "wire" +) + +RG_ARGS=(--hidden --no-ignore --glob '!.git/*' --glob '!target/*' --glob '!**/node_modules/*') + +# Find Rust files likely involved in ABI/wire formats. +# Build pattern and trim trailing '|' to avoid matching everything +pattern="$(printf '%s|' "${ABI_HINTS[@]}")" +pattern="${pattern%|}" +mapfile -t files < <(rg "${RG_ARGS[@]}" -l -g'*.rs' "$pattern" crates/ || true) + +if [[ ${#files[@]} -eq 0 ]]; then + echo "ban-unordered-abi: no ABI-ish files found (by heuristic). OK." + exit 0 +fi + +echo "ban-unordered-abi: scanning ABI-ish Rust files..." +violations=0 + +# HashMap/HashSet are not allowed in ABI-ish types. Use Vec<(K,V)> sorted, BTreeMap, IndexMap with explicit canonicalization, etc. +if rg "${RG_ARGS[@]}" -n -S '\b(HashMap|HashSet)\b' "${files[@]}"; then + violations=$((violations+1)) +fi + +if [[ $violations -ne 0 ]]; then + echo "ban-unordered-abi: FAILED. Unordered containers found in ABI-ish code." + echo "Fix by using Vec pairs + sorting, or BTreeMap + explicit canonical encode ordering." + exit 1 +fi + +echo "ban-unordered-abi: PASSED." diff --git a/scripts/bootstrap_dense_rewrite.mjs b/scripts/bootstrap_dense_rewrite.mjs new file mode 100644 index 00000000..416b2381 --- /dev/null +++ b/scripts/bootstrap_dense_rewrite.mjs @@ -0,0 +1,113 @@ +import fs from "node:fs"; +// Simplified seeded random number generator (Xorshift32) for deterministic scenario generation +function xorshift32(state) { + let x = state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + return x >>> 0; // unsigned 32-bit integer +} + +class RNG { + constructor(seed) { + this.state = seed || 1; + } + next() { + this.state = xorshift32(this.state); + return this.state; + } + range(min, max) { + return min + (this.next() % (max - min)); + } + choice(arr) { + return arr[this.range(0, arr.length)]; + } +} + +const OUT_PATH = "testdata/dind/010_dense_rewrite_seed0001.eintlog"; + +// ELOG Header +const MAGIC = Buffer.from("ELOG"); +const VERSION = Buffer.alloc(2); VERSION.writeUInt16LE(1); +const FLAGS = Buffer.alloc(2); FLAGS.writeUInt16LE(0); +const RESERVED = Buffer.alloc(8); + +// Helper to write +function writeLog(schemaHashHex, frames) { + const fd = fs.openSync(OUT_PATH, "w"); + fs.writeSync(fd, MAGIC); + fs.writeSync(fd, VERSION); + fs.writeSync(fd, FLAGS); + fs.writeSync(fd, Buffer.from(schemaHashHex, "hex")); + fs.writeSync(fd, RESERVED); + + for (const frame of frames) { + const len = Buffer.alloc(4); + len.writeUInt32LE(frame.byteLength); + fs.writeSync(fd, len); + fs.writeSync(fd, frame); + } + fs.closeSync(fd); + console.log(`Wrote ${OUT_PATH}`); +} + +// Op Constants +// EINT(4) + OpID(4) + Len(4) + Payload +function packFrame(opId, payload) { + const header = Buffer.alloc(12); + header.write("EINT", 0); + header.writeUInt32LE(opId, 4); + header.writeUInt32LE(payload.length, 8); + return Buffer.concat([header, payload]); +} + +// OpIDs from generated codecs (hardcoded here to keep script self-contained and stable) +const OP_SET_THEME = 1822649880; +const OP_ROUTE_PUSH = 2216217860; +const OP_TOGGLE_NAV = 3272403183; +const OP_TOAST = 4255241313; +const OP_DROP_BALL = 778504871; + +const rng = new RNG(12345); // Fixed seed +const frames = []; + +// Generate 500 ops (mix of trivial state changes to churn the graph) +for (let i = 0; i < 500; i++) { + const opType = rng.choice(["THEME", "NAV", "ROUTE", "TOAST", "DROP"]); + + if (opType === "THEME") { + const mode = rng.range(0, 3); // 0=LIGHT, 1=DARK, 2=SYSTEM + const payload = Buffer.alloc(2); + payload.writeUInt16LE(mode, 0); + frames.push(packFrame(OP_SET_THEME, payload)); + } else if (opType === "NAV") { + frames.push(packFrame(OP_TOGGLE_NAV, Buffer.alloc(0))); + } else if (opType === "ROUTE") { + const path = `/page/${rng.range(0, 100)}`; + const strBytes = Buffer.from(path, 'utf8'); + const payload = Buffer.alloc(4 + strBytes.length); + payload.writeUInt32LE(strBytes.length, 0); + strBytes.copy(payload, 4); + frames.push(packFrame(OP_ROUTE_PUSH, payload)); + } else if (opType === "TOAST") { + const msg = `Message ${rng.range(0, 1000)}`; + const strBytes = Buffer.from(msg, 'utf8'); + const payload = Buffer.alloc(4 + strBytes.length); + payload.writeUInt32LE(strBytes.length, 0); + strBytes.copy(payload, 4); + frames.push(packFrame(OP_TOAST, payload)); + } else if (opType === "DROP") { + frames.push(packFrame(OP_DROP_BALL, Buffer.alloc(0))); + } +} + +// Get Schema Hash +const codecs = fs.readFileSync("crates/echo-dind-tests/src/generated/codecs.rs", "utf8"); +const match = codecs.match(/pub const SCHEMA_HASH: &str = "([0-9a-fA-F]+)";/); +if (!match) throw new Error("Could not find SCHEMA_HASH in codecs.rs"); +const schemaHash = match[1]; +if (!/^[0-9a-f]{64}$/i.test(schemaHash)) { + throw new Error(`Invalid SCHEMA_HASH: expected 64 hex chars, got ${schemaHash.length}`); +} + +writeLog(schemaHash, frames); diff --git a/scripts/bootstrap_dind_log.mjs b/scripts/bootstrap_dind_log.mjs new file mode 100644 index 00000000..25689ef2 --- /dev/null +++ b/scripts/bootstrap_dind_log.mjs @@ -0,0 +1,74 @@ +import fs from "node:fs"; +// Fix import path: relative to scripts/ is ../src/wasm/echo_protocol.ts +// But node might not like .ts extension directly without loader. +// I'll assume I can run this with `bun` or `ts-node`, or simply read the file and eval it since it has no imports itself. +// Actually, `echo_protocol.ts` is pure JS syntax (enums are problematic in raw JS, but the generated file uses `export enum` which is TS). +// Wait, the generated `echo_protocol.ts` uses `export enum`. Node won't run that. +// I should rely on the *generator* to output a JS version or I should parse it. +// OR, I can just use raw bytes for this bootstrap script since I know the spec. +// "EINT" + OpID + Len + Vars. +// SetTheme is OpID 1822649880. Args: u16 mode. +// Mode DARK = 1. +// Payload: 01 00. +// Envelope: "EINT" + 18 32 A3 6C + 02 00 00 00 + 01 00. +// I will just hardcode the bytes for the bootstrap to avoid TS dependency hell in this script. + +const OUT_PATH = "testdata/dind/000_smoke_theme.eintlog"; + +// ELOG Header +const MAGIC = Buffer.from("ELOG"); +const VERSION = Buffer.alloc(2); VERSION.writeUInt16LE(1); +const FLAGS = Buffer.alloc(2); FLAGS.writeUInt16LE(0); +const RESERVED = Buffer.alloc(8); + +// Helper to write +function writeLog(schemaHashHex, frames) { + const fd = fs.openSync(OUT_PATH, "w"); + fs.writeSync(fd, MAGIC); + fs.writeSync(fd, VERSION); + fs.writeSync(fd, FLAGS); + fs.writeSync(fd, Buffer.from(schemaHashHex, "hex")); + fs.writeSync(fd, RESERVED); + + for (const frame of frames) { + const len = Buffer.alloc(4); + len.writeUInt32LE(frame.byteLength); + fs.writeSync(fd, len); + fs.writeSync(fd, frame); + } + fs.closeSync(fd); + console.log(`Wrote ${OUT_PATH}`); +} + +const frames = []; + +// 1. Set Theme DARK +// OpID: 1822649880 (0x6ca33218) +// Args: 01 00 (u16) +const op1 = Buffer.concat([ + Buffer.from("EINT"), + Buffer.from([0x18, 0x32, 0xa3, 0x6c]), // OpID LE + Buffer.from([0x02, 0x00, 0x00, 0x00]), // Len LE + Buffer.from([0x01, 0x00]) // Payload +]); +frames.push(op1); + +// 2. Set Theme LIGHT +// Args: 00 00 +const op2 = Buffer.concat([ + Buffer.from("EINT"), + Buffer.from([0x18, 0x32, 0xa3, 0x6c]), + Buffer.from([0x02, 0x00, 0x00, 0x00]), + Buffer.from([0x00, 0x00]) +]); +frames.push(op2); + +const codecs = fs.readFileSync("crates/echo-dind-tests/src/generated/codecs.rs", "utf8"); +const match = codecs.match(/SCHEMA_HASH:\s*&str\s*=\s*"([0-9a-fA-F]+)"/); +if (!match) throw new Error("Could not find SCHEMA_HASH"); +const schemaHash = match[1]; +if (!/^[0-9a-f]{64}$/i.test(schemaHash)) { + throw new Error(`Invalid SCHEMA_HASH: expected 64 hex chars, got ${schemaHash.length}`); +} + +writeLog(schemaHash, frames); diff --git a/scripts/bootstrap_error_determinism.mjs b/scripts/bootstrap_error_determinism.mjs new file mode 100644 index 00000000..64f8b05a --- /dev/null +++ b/scripts/bootstrap_error_determinism.mjs @@ -0,0 +1,97 @@ +import fs from "node:fs"; + +const OUT_PATH = "testdata/dind/030_error_determinism.eintlog"; + +// ELOG Header +const MAGIC = Buffer.from("ELOG"); +const VERSION = Buffer.alloc(2); VERSION.writeUInt16LE(1); +const FLAGS = Buffer.alloc(2); FLAGS.writeUInt16LE(0); +const RESERVED = Buffer.alloc(8); + +// Helper to write +function writeLog(schemaHashHex, frames) { + const fd = fs.openSync(OUT_PATH, "w"); + fs.writeSync(fd, MAGIC); + fs.writeSync(fd, VERSION); + fs.writeSync(fd, FLAGS); + fs.writeSync(fd, Buffer.from(schemaHashHex, "hex")); + fs.writeSync(fd, RESERVED); + + for (const frame of frames) { + const len = Buffer.alloc(4); + len.writeUInt32LE(frame.byteLength); + fs.writeSync(fd, len); + fs.writeSync(fd, frame); + } + fs.closeSync(fd); + console.log(`Wrote ${OUT_PATH}`); +} + +const frames = []; + +// 1. Valid Op (SetTheme DARK) - Baseline +// OpID: 1822649880 (0x6ca33218) +// Args: 01 00 (u16) +frames.push(Buffer.concat([ + Buffer.from("EINT"), + Buffer.from([0x18, 0x32, 0xa3, 0x6c]), + Buffer.from([0x02, 0x00, 0x00, 0x00]), + Buffer.from([0x01, 0x00]) +])); + +// 2. Invalid Op ID (0xFFFFFFFF) +// Should be ignored/no-op +frames.push(Buffer.concat([ + Buffer.from("EINT"), + Buffer.from([0xff, 0xff, 0xff, 0xff]), + Buffer.from([0x00, 0x00, 0x00, 0x00]), + Buffer.alloc(0) +])); + +// 3. Valid Op, Wrong Length (Declared 2, provided 1) +// SetTheme +frames.push(Buffer.concat([ + Buffer.from("EINT"), + Buffer.from([0x18, 0x32, 0xa3, 0x6c]), + Buffer.from([0x02, 0x00, 0x00, 0x00]), + Buffer.from([0x01]) // Missing one byte +])); + +// 4. Malformed Envelope (Missing Magic) +frames.push(Buffer.from("JUNK000000000000")); + +// 5. Valid Op, Valid Envelope, Logic Error (e.g. valid payload but rule doesn't match? +// Actually, in our kernel, if op decodes, it creates an intent node. +// If no rule matches the intent, it just sits there or gets cleaned up? +// Current `dispatch_intent` adds `sim/inbox/event:SEQ`. +// If no rule matches, the event stays or is skipped? +// `step()` iterates events. If no rule applies, it finishes. +// So state WILL change (sequence number increments, event node created). +// Determinism means it changes the SAME way every time. + +// Let's add a "valid but unknown" op that passes envelope check but has no rule. +// OpID: 0x12345678 (Random valid u32) +frames.push(Buffer.concat([ + Buffer.from("EINT"), + Buffer.from([0x78, 0x56, 0x34, 0x12]), + Buffer.from([0x00, 0x00, 0x00, 0x00]), + Buffer.alloc(0) +])); + +// 6. Valid Op (SetTheme LIGHT) - Recovery check +frames.push(Buffer.concat([ + Buffer.from("EINT"), + Buffer.from([0x18, 0x32, 0xa3, 0x6c]), + Buffer.from([0x02, 0x00, 0x00, 0x00]), + Buffer.from([0x00, 0x00]) +])); + +const codecs = fs.readFileSync("crates/echo-dind-tests/src/generated/codecs.rs", "utf8"); +const match = codecs.match(/pub const SCHEMA_HASH: &str = "([0-9a-fA-F]+)";/); +if (!match) throw new Error("Could not find SCHEMA_HASH in codecs.rs"); +const schemaHash = match[1]; +if (!/^[0-9a-f]{64}$/i.test(schemaHash)) { + throw new Error(`Invalid SCHEMA_HASH: expected 64 hex chars, got ${schemaHash.length}`); +} + +writeLog(schemaHash, frames); diff --git a/scripts/bootstrap_math_determinism.mjs b/scripts/bootstrap_math_determinism.mjs new file mode 100644 index 00000000..c077c1f2 --- /dev/null +++ b/scripts/bootstrap_math_determinism.mjs @@ -0,0 +1,70 @@ +import fs from "node:fs"; + +const OUT_PATH = "testdata/dind/060_math_determinism.eintlog"; + +// ELOG Header +const MAGIC = Buffer.from("ELOG"); +const VERSION = Buffer.alloc(2); VERSION.writeUInt16LE(1); +const FLAGS = Buffer.alloc(2); FLAGS.writeUInt16LE(0); +const RESERVED = Buffer.alloc(8); + +function writeLog(schemaHashHex, frames) { + const fd = fs.openSync(OUT_PATH, "w"); + fs.writeSync(fd, MAGIC); + fs.writeSync(fd, VERSION); + fs.writeSync(fd, FLAGS); + fs.writeSync(fd, Buffer.from(schemaHashHex, "hex")); + fs.writeSync(fd, RESERVED); + + for (const frame of frames) { + const len = Buffer.alloc(4); + len.writeUInt32LE(frame.byteLength); + fs.writeSync(fd, len); + fs.writeSync(fd, frame); + } + fs.closeSync(fd); + console.log(`Wrote ${OUT_PATH}`); +} + +// OpIDs from crates/echo-dind-tests/src/generated/codecs.rs +const OP_DROP_BALL = 778504871; +const OP_TOGGLE_NAV = 3272403183; + +function makeFrame(opId, args = Buffer.alloc(0)) { + const opBuf = Buffer.alloc(4); + opBuf.writeUInt32LE(opId); + + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32LE(args.length); + + return Buffer.concat([ + Buffer.from("EINT"), + opBuf, + lenBuf, + args + ]); +} + +const frames = []; + +// 1. Drop Ball (Initiates Motion/Physics) +frames.push(makeFrame(OP_DROP_BALL)); + +// 2. Padding steps to allow physics to simulate +// Each step in the harness runs the physics rule once if applicable. +// We'll add 50 steps of "Toggle Nav" (or just dummy ops if we had a NoOp) +// Toggle Nav is safe, it just flips a bit. +for (let i = 0; i < 50; i++) { + frames.push(makeFrame(OP_TOGGLE_NAV)); +} + +// Get Schema Hash +const codecs = fs.readFileSync("crates/echo-dind-tests/src/generated/codecs.rs", "utf8"); +const match = codecs.match(/SCHEMA_HASH:\s*&str\s*=\s*"([0-9a-fA-F]+)"/); +if (!match) throw new Error("Could not find SCHEMA_HASH"); +const schemaHash = match[1]; +if (!/^[0-9a-f]{64}$/i.test(schemaHash)) { + throw new Error(`Invalid SCHEMA_HASH: expected 64 hex chars, got ${schemaHash.length}`); +} + +writeLog(schemaHash, frames); diff --git a/scripts/bootstrap_randomized_convergent.mjs b/scripts/bootstrap_randomized_convergent.mjs new file mode 100644 index 00000000..0e3b1699 --- /dev/null +++ b/scripts/bootstrap_randomized_convergent.mjs @@ -0,0 +1,119 @@ +import fs from "node:fs"; + +// Simplified seeded random number generator (Xorshift32) +function xorshift32(state) { + let x = state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + return x >>> 0; // unsigned 32-bit integer +} + +class RNG { + constructor(seed) { + this.state = seed || 1; + } + next() { + this.state = xorshift32(this.state); + return this.state; + } + range(min, max) { + return min + (this.next() % (max - min)); + } + shuffle(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = this.range(0, i + 1); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } +} + +// Op Constants +function packFrame(opId, payload) { + const header = Buffer.alloc(12); + header.write("EINT", 0); + header.writeUInt32LE(opId, 4); + header.writeUInt32LE(payload.length, 8); + return Buffer.concat([header, payload]); +} + +// putKv is 3000000001 (0xB2D05E01) +const OP_PUT_KV = 3000000001; + +function generateLog(seed, outPath, schemaHashHex) { + const rng = new RNG(seed); + + // Generate 200 disjoint KV inserts. + // Key space: "key_{i}" for i in 0..200. + // Since keys are unique, node IDs will be unique (hash of key). + // Operations are commutative (set union of disjoint nodes). + + const ops = []; + for (let i = 0; i < 200; i++) { + const key = `key_${i}`; + const val = `val_${i}_seed_${seed}`; // Value can vary per seed? No, must be same multiset. + // Wait, "Same multiset of operations". + // If I change value per seed, it's NOT the same multiset. + // I must generate the SAME set of (key, value) pairs, just shuffled. + const fixedVal = `val_${i}`; + + const keyBytes = Buffer.from(key, 'utf8'); + const valBytes = Buffer.from(fixedVal, 'utf8'); + + // Encode Args: key (String), value (String) + // String encoding: Len(u32 LE) + Bytes + let size = 4 + keyBytes.length + 4 + valBytes.length; + const payload = Buffer.alloc(size); + let offset = 0; + + payload.writeUInt32LE(keyBytes.length, offset); offset += 4; + keyBytes.copy(payload, offset); offset += keyBytes.length; + + payload.writeUInt32LE(valBytes.length, offset); offset += 4; + valBytes.copy(payload, offset); offset += valBytes.length; + + ops.push(packFrame(OP_PUT_KV, payload)); + } + + // Shuffle the operations + rng.shuffle(ops); + + // ELOG Header + const MAGIC = Buffer.from("ELOG"); + const VERSION = Buffer.alloc(2); VERSION.writeUInt16LE(1); + const FLAGS = Buffer.alloc(2); FLAGS.writeUInt16LE(0); + const RESERVED = Buffer.alloc(8); + + const fd = fs.openSync(outPath, "w"); + fs.writeSync(fd, MAGIC); + fs.writeSync(fd, VERSION); + fs.writeSync(fd, FLAGS); + fs.writeSync(fd, Buffer.from(schemaHashHex, "hex")); + fs.writeSync(fd, RESERVED); + + for (const frame of ops) { + const len = Buffer.alloc(4); + len.writeUInt32LE(frame.byteLength); + fs.writeSync(fd, len); + fs.writeSync(fd, frame); + } + fs.closeSync(fd); + console.log(`Wrote ${outPath}`); +} + +// Main +const codecs = fs.readFileSync("crates/echo-dind-tests/src/generated/codecs.rs", "utf8"); +const match = codecs.match(/pub const SCHEMA_HASH: &str = "([0-9a-fA-F]+)";/); +if (!match) throw new Error("Could not find SCHEMA_HASH in codecs.rs"); +const schemaHash = match[1]; +if (!/^[0-9a-f]{64}$/i.test(schemaHash)) { + throw new Error(`Invalid SCHEMA_HASH: expected 64 hex chars, got ${schemaHash.length}`); +} + +// Generate 3 seeds +for (let i = 1; i <= 3; i++) { + const seed = 5000 + i; + const out = `testdata/dind/051_randomized_convergent_seed000${i}.eintlog`; + generateLog(seed, out, schemaHash); +} diff --git a/scripts/bootstrap_randomized_order.mjs b/scripts/bootstrap_randomized_order.mjs new file mode 100644 index 00000000..0735e3a5 --- /dev/null +++ b/scripts/bootstrap_randomized_order.mjs @@ -0,0 +1,111 @@ +import fs from "node:fs"; + +// Simplified seeded random number generator (Xorshift32) for deterministic scenario generation +function xorshift32(state) { + let x = state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + return x >>> 0; // unsigned 32-bit integer +} + +class RNG { + constructor(seed) { + this.state = seed || 1; + } + next() { + this.state = xorshift32(this.state); + return this.state; + } + range(min, max) { + return min + (this.next() % (max - min)); + } + // Fisher-Yates shuffle + shuffle(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = this.range(0, i + 1); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } +} + +// Op Constants +function packFrame(opId, payload) { + const header = Buffer.alloc(12); + header.write("EINT", 0); + header.writeUInt32LE(opId, 4); + header.writeUInt32LE(payload.length, 8); + return Buffer.concat([header, payload]); +} + +const OP_ROUTE_PUSH = 2216217860; +const OP_TOAST = 4255241313; + +function generateLog(seed, outPath, schemaHashHex) { + const rng = new RNG(seed); + const frames = []; + + // Set of operations to perform (order-independent set) + // 100 RoutePush ops with unique paths + // 100 Toast ops with unique messages + + const ops = []; + for (let i = 0; i < 100; i++) { + const path = `/node/${i}`; + const strBytes = Buffer.from(path, 'utf8'); + const payload = Buffer.alloc(4 + strBytes.length); + payload.writeUInt32LE(strBytes.length, 0); + strBytes.copy(payload, 4); + ops.push(packFrame(OP_ROUTE_PUSH, payload)); + } + for (let i = 0; i < 100; i++) { + const msg = `Toast ${i}`; + const strBytes = Buffer.from(msg, 'utf8'); + const payload = Buffer.alloc(4 + strBytes.length); + payload.writeUInt32LE(strBytes.length, 0); + strBytes.copy(payload, 4); + ops.push(packFrame(OP_TOAST, payload)); + } + + // Shuffle the operations + rng.shuffle(ops); + + // ELOG Header + const MAGIC = Buffer.from("ELOG"); + const VERSION = Buffer.alloc(2); VERSION.writeUInt16LE(1); + const FLAGS = Buffer.alloc(2); FLAGS.writeUInt16LE(0); + const RESERVED = Buffer.alloc(8); + + const fd = fs.openSync(outPath, "w"); + fs.writeSync(fd, MAGIC); + fs.writeSync(fd, VERSION); + fs.writeSync(fd, FLAGS); + fs.writeSync(fd, Buffer.from(schemaHashHex, "hex")); + fs.writeSync(fd, RESERVED); + + for (const frame of ops) { + const len = Buffer.alloc(4); + len.writeUInt32LE(frame.byteLength); + fs.writeSync(fd, len); + fs.writeSync(fd, frame); + } + fs.closeSync(fd); + console.log(`Wrote ${outPath}`); +} + +// Main +const codecs = fs.readFileSync("crates/echo-dind-tests/src/generated/codecs.rs", "utf8"); +const match = codecs.match(/pub const SCHEMA_HASH: &str = "([0-9a-fA-F]+)";/); +if (!match) throw new Error("Could not find SCHEMA_HASH in codecs.rs"); +const schemaHash = match[1]; +if (!/^[0-9a-f]{64}$/i.test(schemaHash)) { + throw new Error(`Invalid SCHEMA_HASH: expected 64 hex chars, got ${schemaHash.length}`); +} + +// Generate 3 seeds +for (let i = 1; i <= 3; i++) { + const seed = 1000 + i; + const out = `testdata/dind/050_randomized_order_small_seed000${i}.eintlog`; + generateLog(seed, out, schemaHash); +} diff --git a/scripts/check-append-only.js b/scripts/check-append-only.js index 7484bfb7..e736856e 100644 --- a/scripts/check-append-only.js +++ b/scripts/check-append-only.js @@ -5,9 +5,7 @@ import { execFileSync } from "node:child_process"; const files = [ "AGENTS.md", - "docs/decision-log.md", "TASKS-DAG.md", - "docs/execution-plan.md", ]; const args = process.argv.slice(2); diff --git a/scripts/dind-run-suite.mjs b/scripts/dind-run-suite.mjs new file mode 100644 index 00000000..962fea63 --- /dev/null +++ b/scripts/dind-run-suite.mjs @@ -0,0 +1,128 @@ +import fs from "node:fs"; +import { execSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const ROOT_DIR = process.env.DIND_ROOT || path.resolve(SCRIPT_DIR, ".."); +const MANIFEST_PATH = path.resolve(ROOT_DIR, "testdata/dind/MANIFEST.json"); + +function loadManifest() { + return JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8")); +} + +function parseArgs() { + const args = process.argv.slice(2); + const config = { + tags: [], + excludeTags: [], + mode: "run", // run | torture + runs: 20, // for torture + emitRepro: false + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--tags") { + config.tags = args[++i].split(","); + } else if (arg === "--exclude-tags") { + config.excludeTags = args[++i].split(","); + } else if (arg === "--mode") { + config.mode = args[++i]; + } else if (arg === "--runs") { + config.runs = parseInt(args[++i], 10); + } else if (arg === "--emit-repro") { + config.emitRepro = true; + } + } + return config; +} + +function matches(scenario, config) { + if (config.tags.length > 0) { + if (!config.tags.some(t => scenario.tags.includes(t))) return false; + } + if (config.excludeTags.length > 0) { + if (config.excludeTags.some(t => scenario.tags.includes(t))) return false; + } + return true; +} + +function main() { + const manifest = loadManifest(); + const config = parseArgs(); + + console.log(`DIND SUITE: Mode=${config.mode}, Tags=${config.tags.join(",") || "ALL"}`); + + let failedCount = 0; + const results = []; + + for (const scenario of manifest) { + if (!matches(scenario, config)) continue; + + console.log(`\n>>> Running: ${scenario.desc} (${scenario.path})`); + + const scenarioPath = path.resolve(ROOT_DIR, "testdata/dind", scenario.path); + let cmd = `cargo run -p echo-dind-harness --quiet -- ${config.mode}`; + cmd += ` ${scenarioPath}`; + + if (config.mode === "run") { + // Look for golden + const golden = path.resolve( + ROOT_DIR, + "testdata/dind", + scenario.path.replace(".eintlog", ".hashes.json") + ); + if (fs.existsSync(golden)) { + cmd += ` --golden ${golden}`; + } + } else if (config.mode === "torture") { + cmd += ` --runs ${config.runs}`; + } + + if (config.emitRepro) { + const reproDir = path.resolve( + ROOT_DIR, + "test-results/dind", + scenario.path.replace(".eintlog", "") + ); + // cleanup old repro + if (fs.existsSync(reproDir)) { + fs.rmSync(reproDir, { recursive: true, force: true }); + } + cmd += ` --emit-repro ${reproDir}`; + } + + const start = performance.now(); + let passed = false; + try { + execSync(cmd, { stdio: "inherit" }); + passed = true; + } catch (e) { + console.error(`!!! FAILED: ${scenario.desc}`); + failedCount++; + } + const duration = performance.now() - start; + + results.push({ + scenario: scenario.path, + desc: scenario.desc, + duration_ms: Math.round(duration), + passed, + tags: scenario.tags, + mode: config.mode + }); + } + + fs.writeFileSync("dind-report.json", JSON.stringify(results, null, 2)); + console.log(`\nWrote dind-report.json`); + + if (failedCount > 0) { + console.error(`\nDIND SUITE FAILED: ${failedCount} scenarios failed.`); + process.exit(1); + } else { + console.log(`\nDIND SUITE PASSED.`); + } +} + +main(); diff --git a/scripts/scaffold-community.sh b/scripts/scaffold-community.sh index dc7a3220..1f6eeb47 100755 --- a/scripts/scaffold-community.sh +++ b/scripts/scaffold-community.sh @@ -13,7 +13,7 @@ DEFAULT_PROJECT_NAME=${PROJECT_NAME:-$(basename "$ROOT_DIR")} DEFAULT_DESCRIPTION=$(sed -n '1s/^# //p;1q' README.md 2>/dev/null || echo "") DEFAULT_PACKAGE_MANAGER=${PACKAGE_MANAGER:-cargo} DEFAULT_ARCH_DOC=${ARCHITECTURE_DOC:-docs/architecture-outline.md} -DEFAULT_EXEC_PLAN=${EXECUTION_PLAN_DOC:-docs/execution-plan.md} +DEFAULT_EXEC_PLAN=${EXECUTION_PLAN_DOC:-docs/tasks.md} DEFAULT_AGENT_GUIDE=${AGENT_GUIDE:-AGENTS.md} DEFAULT_TAG=${PROJECT_TAG:-Echo} DEFAULT_DEVLOG=${DEVLOG_THREAD:-echo-devlog} diff --git a/testdata/dind/010_dense_rewrite_seed0001.eintlog b/testdata/dind/010_dense_rewrite_seed0001.eintlog new file mode 100644 index 00000000..16c648bd Binary files /dev/null and b/testdata/dind/010_dense_rewrite_seed0001.eintlog differ diff --git a/testdata/dind/010_dense_rewrite_seed0001.hashes.json b/testdata/dind/010_dense_rewrite_seed0001.hashes.json new file mode 100644 index 00000000..0859e420 --- /dev/null +++ b/testdata/dind/010_dense_rewrite_seed0001.hashes.json @@ -0,0 +1,509 @@ +{ + "elog_version": 1, + "schema_hash_hex": "0427e9fd236e92dfc8c0765f7bd429fc233bfbfab3cb67c9d03b22a0f31e7f8a", + "hash_domain": "DIND_STATE_HASH_V1", + "hash_alg": "BLAKE3", + "hashes_hex": [ + "e0c2acac7f7eca867f8575a1861460425976de2fb399146b694de06507a7f890", + "7763c828f814e21154ce118e5d96a5c23c62c6bd0d823a7fc850b775eb3d4475", + "ece23bb38c51a1ad9661d1542e900bb15c69cb6dcdd872873db60a0fb524dbdb", + "56157ac58ec764098b17918704a263755d91428c69f67dd1d3bb5bc22ae30e08", + "44d9d1f4bafb34163caf1353fdd345050b1122d9466ef1eda785d5671a6d01c3", + "7f153e62be6ffe555a5574b14ab55164eb5597a1a0d40e386f3719713e3f2df3", + "52f169ea512d139996911e020293026c16374726f28943fa3b208485ddfb8272", + "d591f18a3fc9bb284ff84d5e159ba6c5286637f7a74af9144551117d1355cff4", + "54f677de28d2428019b997f1844e6ddfab8337a45f76a4a71ff8ec44b8d9e551", + "e727bc5862fa08ba5ddab18cb8c6132093cfc5de6357ad162f43e470901f42aa", + "7ed0a223db6689de6f2e9ddb24238e172bc7f3cc9ae10c7adafc9901c578f76f", + "e9dcc3b5763812489ceee9c28e65da15013c7d4ba97383538a597c74e0adc518", + "2cd8a2b6530f6700301428af0ce1c11f6c40df944614305bb69dabcc558b8e30", + "31f1fe4d004dc5d6c9a25af43ffefdaa4c2e511c3abe5b6396b34a66f8bce9d5", + "39af98955a2992df188d0c32bf19fcb296a2cf39da5afe44ec5d95173a3e111b", + "f7f65283d13c1a5b7c40bc51c4ca38c03cd784a78e3ee2ed8e24165acd6eaee3", + "8f938f0af12ef3fd2f2abdf9cdb1ccb519aaed7a6710ce62e8fcd6e3253b1bca", + "afd8f8bad2bee817396e1547faa55eb37cd8eba6d0f7909e4f3ac77b1f72fb3a", + "4200ac94aaea8e59749b8d21a77ad20c9e47813c769a68e33cdbad37630405d2", + "cdd316eaa21b350de9221a14ea46b7f76109a600bb008b109244fdb063cfc2e1", + "2092b7eef7425d3799ee59b524d59979aeffa145d0968c26497cd11ab83d00e1", + "f83d166dd97c4f3810f7d2b14061a3d2696cf2bacd728699d4c1890ce8265aa4", + "256aa534c35628ee92ca587bf2e620499f33c2851fd7b24665273ceeccb12e88", + "7cd0fdbaf387f7456606620ba7f05f7fd019a077f08baddf771e4c559ba2eb6d", + "35d7d2a88bf6aadc7e14ff8aa165166726ae676fba24c30ec9c0ad987459beaf", + "f50e9ef1a96f2d7078286a8949d90d22d9f4d43f9127e2b6649aa0994c4e270a", + "6e86b105c8f42731519206a4b95a6305bae2f5de9844b9f4412f8bc0b0683e75", + "c75ffa825124ad7683fc722d521bcb3ad18602109be01a4d9bd2f9dca0ddefb9", + "b49d2deb91d5965339ce269c5531ff102d2ccc2626a518d8c2a24f31950d6732", + "a66b63f74c2f0876e0a3bce554dfdbf591dcc96c9ad6bc8b6db7cd9a4cb3890b", + "e753072e6a8c8ac6736792f877809a09192e10e892bd8bfad573946e4fa3b57f", + "e753072e6a8c8ac6736792f877809a09192e10e892bd8bfad573946e4fa3b57f", + "04ea7ba127cd2475ca9645e4de87e3e7f92eab7f3784fc7d228836c925609e9b", + "04ea7ba127cd2475ca9645e4de87e3e7f92eab7f3784fc7d228836c925609e9b", + "ce57fa980af3556a3a8faf157608aff8e0cbfcd4588cdd1766b1a26172b879ba", + "ce57fa980af3556a3a8faf157608aff8e0cbfcd4588cdd1766b1a26172b879ba", + "abde2ce2005818929b7ba373c65888da16f4a25fcee81a35e31a4db098e36ffa", + "606aafd56dc558e49f804ccecca8c18a1c4c90daa5503659528790bdc003b1b8", + "606aafd56dc558e49f804ccecca8c18a1c4c90daa5503659528790bdc003b1b8", + "606aafd56dc558e49f804ccecca8c18a1c4c90daa5503659528790bdc003b1b8", + "606aafd56dc558e49f804ccecca8c18a1c4c90daa5503659528790bdc003b1b8", + "606aafd56dc558e49f804ccecca8c18a1c4c90daa5503659528790bdc003b1b8", + "606aafd56dc558e49f804ccecca8c18a1c4c90daa5503659528790bdc003b1b8", + "606aafd56dc558e49f804ccecca8c18a1c4c90daa5503659528790bdc003b1b8", + "606aafd56dc558e49f804ccecca8c18a1c4c90daa5503659528790bdc003b1b8", + "606aafd56dc558e49f804ccecca8c18a1c4c90daa5503659528790bdc003b1b8", + "c3a90e14dc9376890c097698318fe4ce08fa3dc699ee4001cb8f91204a927b5a", + "5ddbad30cc7c48054aa6e26b832b5cd24eef7135c2571812020457e4c00dc3bd", + "5ddbad30cc7c48054aa6e26b832b5cd24eef7135c2571812020457e4c00dc3bd", + "5ddbad30cc7c48054aa6e26b832b5cd24eef7135c2571812020457e4c00dc3bd", + "d4b26c0b37ffc94a4c3ff3489859c6d369d26bba6c5e914b5affd056c75055d2", + "d4b26c0b37ffc94a4c3ff3489859c6d369d26bba6c5e914b5affd056c75055d2", + "e6557466a351e4b589f063706905f0ee42e8932710eca86343a882ed7f2ba91f", + "e6557466a351e4b589f063706905f0ee42e8932710eca86343a882ed7f2ba91f", + "e6557466a351e4b589f063706905f0ee42e8932710eca86343a882ed7f2ba91f", + "e6557466a351e4b589f063706905f0ee42e8932710eca86343a882ed7f2ba91f", + "58692569bdc7caf313eaad91ab47390a102bf7e39f7b7c456c7f4f897dc5d886", + "58692569bdc7caf313eaad91ab47390a102bf7e39f7b7c456c7f4f897dc5d886", + "58692569bdc7caf313eaad91ab47390a102bf7e39f7b7c456c7f4f897dc5d886", + "58692569bdc7caf313eaad91ab47390a102bf7e39f7b7c456c7f4f897dc5d886", + "58692569bdc7caf313eaad91ab47390a102bf7e39f7b7c456c7f4f897dc5d886", + "4aa1a4e1ca9b46142051e2a92830eba11a3bfd0bcc5ef4a80f7ae3b588a088ac", + "4aa1a4e1ca9b46142051e2a92830eba11a3bfd0bcc5ef4a80f7ae3b588a088ac", + "4aa1a4e1ca9b46142051e2a92830eba11a3bfd0bcc5ef4a80f7ae3b588a088ac", + "4aa1a4e1ca9b46142051e2a92830eba11a3bfd0bcc5ef4a80f7ae3b588a088ac", + "4aa1a4e1ca9b46142051e2a92830eba11a3bfd0bcc5ef4a80f7ae3b588a088ac", + "4aa1a4e1ca9b46142051e2a92830eba11a3bfd0bcc5ef4a80f7ae3b588a088ac", + "ffaf6a3a2f78d7cef599d361ea1177cd9012d571c5254d67320eca85de869753", + "ffaf6a3a2f78d7cef599d361ea1177cd9012d571c5254d67320eca85de869753", + "682c4386342351800263f1286b40925afc970cb0bce1f644e4c40b4a27293877", + "682c4386342351800263f1286b40925afc970cb0bce1f644e4c40b4a27293877", + "682c4386342351800263f1286b40925afc970cb0bce1f644e4c40b4a27293877", + "682c4386342351800263f1286b40925afc970cb0bce1f644e4c40b4a27293877", + "682c4386342351800263f1286b40925afc970cb0bce1f644e4c40b4a27293877", + "682c4386342351800263f1286b40925afc970cb0bce1f644e4c40b4a27293877", + "682c4386342351800263f1286b40925afc970cb0bce1f644e4c40b4a27293877", + "1d103e8ffa29cf00df3690b3c67ae838bcf701b2500be5c01fa3ee697471b154", + "1d103e8ffa29cf00df3690b3c67ae838bcf701b2500be5c01fa3ee697471b154", + "1d103e8ffa29cf00df3690b3c67ae838bcf701b2500be5c01fa3ee697471b154", + "1d103e8ffa29cf00df3690b3c67ae838bcf701b2500be5c01fa3ee697471b154", + "1d103e8ffa29cf00df3690b3c67ae838bcf701b2500be5c01fa3ee697471b154", + "b76567f6af936943c721f85f4a50b74fbb78a9ee4ddd9dec6fed761f60cb654d", + "112739997a16d199c7f8b4e22cf51ccad5a0995bac413bd8d0cb79d38b04428c", + "b32ffdb96f1dfba7f5ea1e3216c4e4de4498ac671d6de361d530c2fd4eaad460", + "1c35652bbb3975ae9c0451b98f5017fc92fb50b3e922ef4a651fc42b049f9c54", + "1c35652bbb3975ae9c0451b98f5017fc92fb50b3e922ef4a651fc42b049f9c54", + "1c35652bbb3975ae9c0451b98f5017fc92fb50b3e922ef4a651fc42b049f9c54", + "1c35652bbb3975ae9c0451b98f5017fc92fb50b3e922ef4a651fc42b049f9c54", + "1c35652bbb3975ae9c0451b98f5017fc92fb50b3e922ef4a651fc42b049f9c54", + "1c35652bbb3975ae9c0451b98f5017fc92fb50b3e922ef4a651fc42b049f9c54", + "1c35652bbb3975ae9c0451b98f5017fc92fb50b3e922ef4a651fc42b049f9c54", + "e650baf32ad773908b02d551d6ce68191e8afd6af4945adafaae98db4559f7b3", + "e650baf32ad773908b02d551d6ce68191e8afd6af4945adafaae98db4559f7b3", + "e650baf32ad773908b02d551d6ce68191e8afd6af4945adafaae98db4559f7b3", + "e650baf32ad773908b02d551d6ce68191e8afd6af4945adafaae98db4559f7b3", + "0fe409b84b6f08e6afe53be8fd00db10cfcd5497b2bf2cb4120ad1abbccba5f4", + "3d7eefd7245c76246d0a3aa995a4f5720be147dbc62219eb11e7daa1e2c9d9b8", + "ca4cc9eedda81d32e96e4aa7d328fa94c237e7e38536366cc410e314977317a2", + "3b8b6c8e6407680f83bdb85d05d3555d3f50d53c0eb09851c7e85d6a3052395d", + "3b8b6c8e6407680f83bdb85d05d3555d3f50d53c0eb09851c7e85d6a3052395d", + "3b8b6c8e6407680f83bdb85d05d3555d3f50d53c0eb09851c7e85d6a3052395d", + "a761560a9c7076521a41efe15e82a4045c229b9ce68de2ee3ab507c4d92a1d09", + "a761560a9c7076521a41efe15e82a4045c229b9ce68de2ee3ab507c4d92a1d09", + "a761560a9c7076521a41efe15e82a4045c229b9ce68de2ee3ab507c4d92a1d09", + "e9df74b2c49e8eb782c9bf2c5f09756db5e7393448a004593c92187e56b568ca", + "e9df74b2c49e8eb782c9bf2c5f09756db5e7393448a004593c92187e56b568ca", + "99a3424a72239a902aba985478e3b453667d031391941af0ae76e04cd70b6205", + "99a3424a72239a902aba985478e3b453667d031391941af0ae76e04cd70b6205", + "99a3424a72239a902aba985478e3b453667d031391941af0ae76e04cd70b6205", + "99a3424a72239a902aba985478e3b453667d031391941af0ae76e04cd70b6205", + "99a3424a72239a902aba985478e3b453667d031391941af0ae76e04cd70b6205", + "ecd83d567578f6a4c30d795168b0bd61186e8d1cf24152de1568ccfc313d6e08", + "720a59c0f9bb05180665d29a8759b5f5dbf6271cc1d11e34a02afa6aeae98ae6", + "720a59c0f9bb05180665d29a8759b5f5dbf6271cc1d11e34a02afa6aeae98ae6", + "720a59c0f9bb05180665d29a8759b5f5dbf6271cc1d11e34a02afa6aeae98ae6", + "720a59c0f9bb05180665d29a8759b5f5dbf6271cc1d11e34a02afa6aeae98ae6", + "720a59c0f9bb05180665d29a8759b5f5dbf6271cc1d11e34a02afa6aeae98ae6", + "2abcad4876363c48f99892e7997d701a9fc4220d6556334209d2cf9fc2fa7028", + "2abcad4876363c48f99892e7997d701a9fc4220d6556334209d2cf9fc2fa7028", + "97579b84b4664fff43ea07ebbab58bb186ed94fcf87072f4d27be56e2c442548", + "97579b84b4664fff43ea07ebbab58bb186ed94fcf87072f4d27be56e2c442548", + "97579b84b4664fff43ea07ebbab58bb186ed94fcf87072f4d27be56e2c442548", + "97579b84b4664fff43ea07ebbab58bb186ed94fcf87072f4d27be56e2c442548", + "97579b84b4664fff43ea07ebbab58bb186ed94fcf87072f4d27be56e2c442548", + "4d10860466dfb1fb719ae32830b94bddcc3e87165343b2ff385684f0efc61647", + "4d10860466dfb1fb719ae32830b94bddcc3e87165343b2ff385684f0efc61647", + "4d10860466dfb1fb719ae32830b94bddcc3e87165343b2ff385684f0efc61647", + "8615f678f84238d3189c0cbefe3379b946e3ca1f1bf6ba3803fd88fe85e2d45f", + "0fcdd6005a9eef1f93106c85d8a2e332c0a932c14ded355f5bfae9e23b074e87", + "0fcdd6005a9eef1f93106c85d8a2e332c0a932c14ded355f5bfae9e23b074e87", + "6d7585171668f4675dd48ec331a6dd56a14abc0cb43c8951c91015e2a04ad8bb", + "6d7585171668f4675dd48ec331a6dd56a14abc0cb43c8951c91015e2a04ad8bb", + "6d7585171668f4675dd48ec331a6dd56a14abc0cb43c8951c91015e2a04ad8bb", + "6d7585171668f4675dd48ec331a6dd56a14abc0cb43c8951c91015e2a04ad8bb", + "dc66a179ca61168b2e56a5ed4cb470ee83d14d20eb30721aa7d65f4cf7ccd4c4", + "8a13d6a1cf89f5cb67e7e9ec52409caba7f5d257f5efb7f682cbb1cd2a609791", + "8a13d6a1cf89f5cb67e7e9ec52409caba7f5d257f5efb7f682cbb1cd2a609791", + "8a13d6a1cf89f5cb67e7e9ec52409caba7f5d257f5efb7f682cbb1cd2a609791", + "8a13d6a1cf89f5cb67e7e9ec52409caba7f5d257f5efb7f682cbb1cd2a609791", + "8a13d6a1cf89f5cb67e7e9ec52409caba7f5d257f5efb7f682cbb1cd2a609791", + "8a13d6a1cf89f5cb67e7e9ec52409caba7f5d257f5efb7f682cbb1cd2a609791", + "8a13d6a1cf89f5cb67e7e9ec52409caba7f5d257f5efb7f682cbb1cd2a609791", + "e5977691ecfb9c87877f7576a2cb2fd060dc8d932f7dc479e17ccc71df183777", + "e5977691ecfb9c87877f7576a2cb2fd060dc8d932f7dc479e17ccc71df183777", + "e5977691ecfb9c87877f7576a2cb2fd060dc8d932f7dc479e17ccc71df183777", + "e5977691ecfb9c87877f7576a2cb2fd060dc8d932f7dc479e17ccc71df183777", + "e5977691ecfb9c87877f7576a2cb2fd060dc8d932f7dc479e17ccc71df183777", + "e5977691ecfb9c87877f7576a2cb2fd060dc8d932f7dc479e17ccc71df183777", + "e5977691ecfb9c87877f7576a2cb2fd060dc8d932f7dc479e17ccc71df183777", + "d6ef8d611c05dab6170bc49d2479abb1d41a0b6f888cdbe8cf7ecfc7298817b1", + "85846abffb0bf62dd632c455761ea96d408ecb41adb78d60d563b253cbf7842b", + "85846abffb0bf62dd632c455761ea96d408ecb41adb78d60d563b253cbf7842b", + "85846abffb0bf62dd632c455761ea96d408ecb41adb78d60d563b253cbf7842b", + "85846abffb0bf62dd632c455761ea96d408ecb41adb78d60d563b253cbf7842b", + "85846abffb0bf62dd632c455761ea96d408ecb41adb78d60d563b253cbf7842b", + "85846abffb0bf62dd632c455761ea96d408ecb41adb78d60d563b253cbf7842b", + "6234b4532585350a44732fd8582138a248e4a3cd479a1c39f222a703f3c5ad2b", + "6234b4532585350a44732fd8582138a248e4a3cd479a1c39f222a703f3c5ad2b", + "6234b4532585350a44732fd8582138a248e4a3cd479a1c39f222a703f3c5ad2b", + "6234b4532585350a44732fd8582138a248e4a3cd479a1c39f222a703f3c5ad2b", + "6234b4532585350a44732fd8582138a248e4a3cd479a1c39f222a703f3c5ad2b", + "6234b4532585350a44732fd8582138a248e4a3cd479a1c39f222a703f3c5ad2b", + "6234b4532585350a44732fd8582138a248e4a3cd479a1c39f222a703f3c5ad2b", + "6234b4532585350a44732fd8582138a248e4a3cd479a1c39f222a703f3c5ad2b", + "6234b4532585350a44732fd8582138a248e4a3cd479a1c39f222a703f3c5ad2b", + "0a830652d5590f9137bf93b41b6d01608d9fbea36df79c50c7c51f7dd8a5bb15", + "0a830652d5590f9137bf93b41b6d01608d9fbea36df79c50c7c51f7dd8a5bb15", + "83b2cf8c2726d21b11fe94cc3c208351dbf59a8f09709b0b711b417fd58251ed", + "83b2cf8c2726d21b11fe94cc3c208351dbf59a8f09709b0b711b417fd58251ed", + "5d9ddeb8391bbfd0e50a0d4caa4211ad12f3fbcf3c31fead22554e66214664c8", + "5d9ddeb8391bbfd0e50a0d4caa4211ad12f3fbcf3c31fead22554e66214664c8", + "bae04ee5ca8a6d335b63acd9d8b94f0741d66c38b6dd118bedbf7a0e3b92202d", + "bae04ee5ca8a6d335b63acd9d8b94f0741d66c38b6dd118bedbf7a0e3b92202d", + "f8bf094f316607052f269aa011f9e293e2f5f60e3061ae9029f7618a87ed301c", + "f8bf094f316607052f269aa011f9e293e2f5f60e3061ae9029f7618a87ed301c", + "f8bf094f316607052f269aa011f9e293e2f5f60e3061ae9029f7618a87ed301c", + "f8bf094f316607052f269aa011f9e293e2f5f60e3061ae9029f7618a87ed301c", + "4102caa550123a76bc71d8369a5fc27838cb802ef1b6b13e3a9ee2e88fc6538f", + "4102caa550123a76bc71d8369a5fc27838cb802ef1b6b13e3a9ee2e88fc6538f", + "4102caa550123a76bc71d8369a5fc27838cb802ef1b6b13e3a9ee2e88fc6538f", + "7376246bc1c7d70f12255ca2a2e86dfd89b4348a503fe625817c66f051a44fe2", + "7376246bc1c7d70f12255ca2a2e86dfd89b4348a503fe625817c66f051a44fe2", + "7376246bc1c7d70f12255ca2a2e86dfd89b4348a503fe625817c66f051a44fe2", + "7376246bc1c7d70f12255ca2a2e86dfd89b4348a503fe625817c66f051a44fe2", + "7376246bc1c7d70f12255ca2a2e86dfd89b4348a503fe625817c66f051a44fe2", + "7376246bc1c7d70f12255ca2a2e86dfd89b4348a503fe625817c66f051a44fe2", + "7376246bc1c7d70f12255ca2a2e86dfd89b4348a503fe625817c66f051a44fe2", + "7376246bc1c7d70f12255ca2a2e86dfd89b4348a503fe625817c66f051a44fe2", + "7376246bc1c7d70f12255ca2a2e86dfd89b4348a503fe625817c66f051a44fe2", + "7376246bc1c7d70f12255ca2a2e86dfd89b4348a503fe625817c66f051a44fe2", + "f2248f13a835b2c3e8e9c96192cd435d601da1a5dfdaf8ec1a8a95ded07c18d5", + "f2248f13a835b2c3e8e9c96192cd435d601da1a5dfdaf8ec1a8a95ded07c18d5", + "18a4e8ccb2cb5c5fecff072fee806727ec9e2d7f38497f10cb1d5db9aa87af14", + "18a4e8ccb2cb5c5fecff072fee806727ec9e2d7f38497f10cb1d5db9aa87af14", + "18a4e8ccb2cb5c5fecff072fee806727ec9e2d7f38497f10cb1d5db9aa87af14", + "18a4e8ccb2cb5c5fecff072fee806727ec9e2d7f38497f10cb1d5db9aa87af14", + "18a4e8ccb2cb5c5fecff072fee806727ec9e2d7f38497f10cb1d5db9aa87af14", + "97ec517a3d40b164aa08d370fe2fe5f50aed8c17e5c4e3b6e249e006195590e6", + "97ec517a3d40b164aa08d370fe2fe5f50aed8c17e5c4e3b6e249e006195590e6", + "97ec517a3d40b164aa08d370fe2fe5f50aed8c17e5c4e3b6e249e006195590e6", + "97ec517a3d40b164aa08d370fe2fe5f50aed8c17e5c4e3b6e249e006195590e6", + "97ec517a3d40b164aa08d370fe2fe5f50aed8c17e5c4e3b6e249e006195590e6", + "97ec517a3d40b164aa08d370fe2fe5f50aed8c17e5c4e3b6e249e006195590e6", + "415508cbd429bcbd9590fb6b1dd3016638d1f7784dcceb161b19c43e1b7f5ca5", + "b906e9095e9a1de7c8f9cdca7f3281fbe6b7cab4f512e3528e6b0ce8cd113c77", + "b906e9095e9a1de7c8f9cdca7f3281fbe6b7cab4f512e3528e6b0ce8cd113c77", + "b906e9095e9a1de7c8f9cdca7f3281fbe6b7cab4f512e3528e6b0ce8cd113c77", + "b906e9095e9a1de7c8f9cdca7f3281fbe6b7cab4f512e3528e6b0ce8cd113c77", + "b906e9095e9a1de7c8f9cdca7f3281fbe6b7cab4f512e3528e6b0ce8cd113c77", + "2c4204b961bdceda006530b29d821d58580a44a1b9f86df67bc8ad86ef9f0350", + "2c4204b961bdceda006530b29d821d58580a44a1b9f86df67bc8ad86ef9f0350", + "2c4204b961bdceda006530b29d821d58580a44a1b9f86df67bc8ad86ef9f0350", + "2c4204b961bdceda006530b29d821d58580a44a1b9f86df67bc8ad86ef9f0350", + "fd5975395f579b2f943054214a038c3deee7108af43708d27613588fdfdac81b", + "fd5975395f579b2f943054214a038c3deee7108af43708d27613588fdfdac81b", + "1960e84e66ff8e8c1f1924c522eacbcca2fbf7ecdf4947477aa228f711b28769", + "1960e84e66ff8e8c1f1924c522eacbcca2fbf7ecdf4947477aa228f711b28769", + "1960e84e66ff8e8c1f1924c522eacbcca2fbf7ecdf4947477aa228f711b28769", + "1960e84e66ff8e8c1f1924c522eacbcca2fbf7ecdf4947477aa228f711b28769", + "66a0de4d591792e8d4db8fe0275881bd5510c5ea6b1ab70c4a6ebe0cceb105ce", + "66a0de4d591792e8d4db8fe0275881bd5510c5ea6b1ab70c4a6ebe0cceb105ce", + "66a0de4d591792e8d4db8fe0275881bd5510c5ea6b1ab70c4a6ebe0cceb105ce", + "66a0de4d591792e8d4db8fe0275881bd5510c5ea6b1ab70c4a6ebe0cceb105ce", + "66a0de4d591792e8d4db8fe0275881bd5510c5ea6b1ab70c4a6ebe0cceb105ce", + "f1197431de03bea7474e7feee2fb6e54062a9f5048785827e7b1d037390b8228", + "f1197431de03bea7474e7feee2fb6e54062a9f5048785827e7b1d037390b8228", + "0b0b87652d42e950e98c4f4830f8b7ab877cef9c0ef8116d236007c498cef437", + "0b0b87652d42e950e98c4f4830f8b7ab877cef9c0ef8116d236007c498cef437", + "cd6bfa5f9a89779312b03ab968abc249f00d6a573ca62a61f191dea397c326f0", + "69e28e16ecfa9619720a73ad68625c02c3eca04f18ea249dfe3dfb9f4b33404f", + "2687bc1cb871c47f291d8bd97e5e7fb617fc481396ecea41a0c9b488d32d2a0b", + "2687bc1cb871c47f291d8bd97e5e7fb617fc481396ecea41a0c9b488d32d2a0b", + "2687bc1cb871c47f291d8bd97e5e7fb617fc481396ecea41a0c9b488d32d2a0b", + "2687bc1cb871c47f291d8bd97e5e7fb617fc481396ecea41a0c9b488d32d2a0b", + "2687bc1cb871c47f291d8bd97e5e7fb617fc481396ecea41a0c9b488d32d2a0b", + "2687bc1cb871c47f291d8bd97e5e7fb617fc481396ecea41a0c9b488d32d2a0b", + "2caa62a52146b2c638e8f35e68f0965b3dc38cd9ae38c6bda5fdf02441f06973", + "2caa62a52146b2c638e8f35e68f0965b3dc38cd9ae38c6bda5fdf02441f06973", + "2caa62a52146b2c638e8f35e68f0965b3dc38cd9ae38c6bda5fdf02441f06973", + "2caa62a52146b2c638e8f35e68f0965b3dc38cd9ae38c6bda5fdf02441f06973", + "2808a3ca79b42efe1df29411e9650849294a7f36e7ac5bf9d475ddba2454c16b", + "2808a3ca79b42efe1df29411e9650849294a7f36e7ac5bf9d475ddba2454c16b", + "505dce9b11ea421a0bc21e6163532de1c0620cde1cd33167b911342ef229b6cf", + "7e88efd663c274dbc040d6fdd624e33357bbf0c06cbb04a117daaf271dff36c4", + "7e88efd663c274dbc040d6fdd624e33357bbf0c06cbb04a117daaf271dff36c4", + "c7a7c366784fb5e2da1ee6ba25ab9d1dabd32963c8012ed192549c10788a69ed", + "c7a7c366784fb5e2da1ee6ba25ab9d1dabd32963c8012ed192549c10788a69ed", + "c7a7c366784fb5e2da1ee6ba25ab9d1dabd32963c8012ed192549c10788a69ed", + "c7a7c366784fb5e2da1ee6ba25ab9d1dabd32963c8012ed192549c10788a69ed", + "c7a7c366784fb5e2da1ee6ba25ab9d1dabd32963c8012ed192549c10788a69ed", + "c7a7c366784fb5e2da1ee6ba25ab9d1dabd32963c8012ed192549c10788a69ed", + "c7a7c366784fb5e2da1ee6ba25ab9d1dabd32963c8012ed192549c10788a69ed", + "a14a7acd7862e9bf3a7394ca87341abf699147e5b30d519e8fc5d9ddea0f4d38", + "a14a7acd7862e9bf3a7394ca87341abf699147e5b30d519e8fc5d9ddea0f4d38", + "a14a7acd7862e9bf3a7394ca87341abf699147e5b30d519e8fc5d9ddea0f4d38", + "f7528b53a8c36824999b8cfc8d624c4e3cd5ed90df416e73e5be2ed4d645dceb", + "f7528b53a8c36824999b8cfc8d624c4e3cd5ed90df416e73e5be2ed4d645dceb", + "f7528b53a8c36824999b8cfc8d624c4e3cd5ed90df416e73e5be2ed4d645dceb", + "f7528b53a8c36824999b8cfc8d624c4e3cd5ed90df416e73e5be2ed4d645dceb", + "5dc8d63a955d6955e43c832dfc63f86c7d952421376b9ec154ff9c8d226e3c62", + "5dc8d63a955d6955e43c832dfc63f86c7d952421376b9ec154ff9c8d226e3c62", + "211c24c1d28ecdc80a578fad78837997eb007168e26b25d32b48585b3006bf3c", + "211c24c1d28ecdc80a578fad78837997eb007168e26b25d32b48585b3006bf3c", + "211c24c1d28ecdc80a578fad78837997eb007168e26b25d32b48585b3006bf3c", + "ca3272fb7b3519988e21e0f941ac42f0deb9ff495f342b1c99ed16cb6cdbfb1d", + "181577ab9f80cd9565f163a2d5885df9dddb13f9dba5392be213f0a3604862dc", + "6e6fa9c551112d16248bfa7f18c924214412c54baf115f59a9326a99a5f610e1", + "9ae156f97fe941a85164ee4c2be501263733428a0ff2c704f9f04bf3b397e080", + "9ae156f97fe941a85164ee4c2be501263733428a0ff2c704f9f04bf3b397e080", + "b99c3c391447f8354775df26d377f6e2483a628c47acd3eea319609d1e462840", + "b99c3c391447f8354775df26d377f6e2483a628c47acd3eea319609d1e462840", + "b99c3c391447f8354775df26d377f6e2483a628c47acd3eea319609d1e462840", + "b99c3c391447f8354775df26d377f6e2483a628c47acd3eea319609d1e462840", + "b99c3c391447f8354775df26d377f6e2483a628c47acd3eea319609d1e462840", + "b99c3c391447f8354775df26d377f6e2483a628c47acd3eea319609d1e462840", + "2a45d96733dacba321cb6a0d559a4498330ba03372336c3cda2f3edb441b731c", + "2a45d96733dacba321cb6a0d559a4498330ba03372336c3cda2f3edb441b731c", + "2a45d96733dacba321cb6a0d559a4498330ba03372336c3cda2f3edb441b731c", + "2a45d96733dacba321cb6a0d559a4498330ba03372336c3cda2f3edb441b731c", + "d3f95e4e979ea90772a6b95755460bed821d581dcea37fca6190a7979c36c955", + "230403a6aba44d35c1148d346526f72c6b2a8228c114eda4bdc36c47e5305841", + "230403a6aba44d35c1148d346526f72c6b2a8228c114eda4bdc36c47e5305841", + "0e0f53d7309d28d813ff347869a3789fbe1d1a62a66068813cf80e8d4f343f95", + "4cb49b0828c900ad356a92e396c3f4eed79cc6ded249c99ff13279a25c4570a7", + "4cb49b0828c900ad356a92e396c3f4eed79cc6ded249c99ff13279a25c4570a7", + "4cb49b0828c900ad356a92e396c3f4eed79cc6ded249c99ff13279a25c4570a7", + "4cb49b0828c900ad356a92e396c3f4eed79cc6ded249c99ff13279a25c4570a7", + "4cb49b0828c900ad356a92e396c3f4eed79cc6ded249c99ff13279a25c4570a7", + "4cb49b0828c900ad356a92e396c3f4eed79cc6ded249c99ff13279a25c4570a7", + "4cb49b0828c900ad356a92e396c3f4eed79cc6ded249c99ff13279a25c4570a7", + "272fc8a62ab261964b0b76f83b629e3f3d78f5c7cc883b6ac6d0d8b57e92ee7f", + "c252f1bab672e68fd7c2da58d6dc03ab841dcea5ea573422177ad2d43436cc4a", + "c252f1bab672e68fd7c2da58d6dc03ab841dcea5ea573422177ad2d43436cc4a", + "c252f1bab672e68fd7c2da58d6dc03ab841dcea5ea573422177ad2d43436cc4a", + "c252f1bab672e68fd7c2da58d6dc03ab841dcea5ea573422177ad2d43436cc4a", + "c252f1bab672e68fd7c2da58d6dc03ab841dcea5ea573422177ad2d43436cc4a", + "c252f1bab672e68fd7c2da58d6dc03ab841dcea5ea573422177ad2d43436cc4a", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "16380699fb950551b35c3644116b8d10a18c3856bd2d81b40bcf09687aea68f2", + "742ad64d6f1c15ee5948d36e8190007801b75a4e2b58f3f59ba8eaca65e2b872", + "e86b68199e2e6a47dff3ed62b719be9d231e936182328f75fcb7ccdf86f06be0", + "00f96f7918b88dc135597319f3e95f7c90a3b6f39126d7d351d6d4f4c01a1b64", + "e52e30a7a51273ec43f42beddde212a67fda01e137ff38e6fd2b648b5b9c8e47", + "e52e30a7a51273ec43f42beddde212a67fda01e137ff38e6fd2b648b5b9c8e47", + "c396a501b87ba7fa3dba6570f744b391c80dcd90bbd99da8dbdcf5a9274d3ede", + "c396a501b87ba7fa3dba6570f744b391c80dcd90bbd99da8dbdcf5a9274d3ede", + "c396a501b87ba7fa3dba6570f744b391c80dcd90bbd99da8dbdcf5a9274d3ede", + "c396a501b87ba7fa3dba6570f744b391c80dcd90bbd99da8dbdcf5a9274d3ede", + "c396a501b87ba7fa3dba6570f744b391c80dcd90bbd99da8dbdcf5a9274d3ede", + "deb92208c261713964f6f4e1b1754eb17e9e78ebf6e7a1da367ca55952b636e8", + "deb92208c261713964f6f4e1b1754eb17e9e78ebf6e7a1da367ca55952b636e8", + "deb92208c261713964f6f4e1b1754eb17e9e78ebf6e7a1da367ca55952b636e8", + "deb92208c261713964f6f4e1b1754eb17e9e78ebf6e7a1da367ca55952b636e8", + "deb92208c261713964f6f4e1b1754eb17e9e78ebf6e7a1da367ca55952b636e8", + "deb92208c261713964f6f4e1b1754eb17e9e78ebf6e7a1da367ca55952b636e8", + "deb92208c261713964f6f4e1b1754eb17e9e78ebf6e7a1da367ca55952b636e8", + "61685d13cb839658bab2e4af4492e8d1571ec5eb2156c59e349b007c02c49951", + "61685d13cb839658bab2e4af4492e8d1571ec5eb2156c59e349b007c02c49951", + "0caa87970a3536666d2def1d89d27188cb3dfe5211464a28d8a2f5c8609ee9ea", + "44a0df5fdea0eaaaadcd91a698243e7268aada905a50767b3e5b8aa7abcd01ea", + "44a0df5fdea0eaaaadcd91a698243e7268aada905a50767b3e5b8aa7abcd01ea", + "44a0df5fdea0eaaaadcd91a698243e7268aada905a50767b3e5b8aa7abcd01ea", + "44a0df5fdea0eaaaadcd91a698243e7268aada905a50767b3e5b8aa7abcd01ea", + "44a0df5fdea0eaaaadcd91a698243e7268aada905a50767b3e5b8aa7abcd01ea", + "44a0df5fdea0eaaaadcd91a698243e7268aada905a50767b3e5b8aa7abcd01ea", + "adfa4ed7e5609cf68f1bfe9a47842d4e5c55b7e38ee6d5dc3217045643e23b68", + "adfa4ed7e5609cf68f1bfe9a47842d4e5c55b7e38ee6d5dc3217045643e23b68", + "adfa4ed7e5609cf68f1bfe9a47842d4e5c55b7e38ee6d5dc3217045643e23b68", + "adfa4ed7e5609cf68f1bfe9a47842d4e5c55b7e38ee6d5dc3217045643e23b68", + "adfa4ed7e5609cf68f1bfe9a47842d4e5c55b7e38ee6d5dc3217045643e23b68", + "fdf6a1cf8887e81579c4c3ff8a5c6b42087fcd528ddb587f03e873e5f5b55f2d", + "fdf6a1cf8887e81579c4c3ff8a5c6b42087fcd528ddb587f03e873e5f5b55f2d", + "cf8c118555a53710c135ce24fa63fc33780cd8e10b33884f9c83ed09b2a0e945", + "fc0f27ca03201f1b0d975bff6b34c4b44aa265ce1ed141eaff666d01426bd993", + "fc0f27ca03201f1b0d975bff6b34c4b44aa265ce1ed141eaff666d01426bd993", + "fc0f27ca03201f1b0d975bff6b34c4b44aa265ce1ed141eaff666d01426bd993", + "fc0f27ca03201f1b0d975bff6b34c4b44aa265ce1ed141eaff666d01426bd993", + "fc0f27ca03201f1b0d975bff6b34c4b44aa265ce1ed141eaff666d01426bd993", + "fc0f27ca03201f1b0d975bff6b34c4b44aa265ce1ed141eaff666d01426bd993", + "fc0f27ca03201f1b0d975bff6b34c4b44aa265ce1ed141eaff666d01426bd993", + "fc0f27ca03201f1b0d975bff6b34c4b44aa265ce1ed141eaff666d01426bd993", + "7585339d4a23b4ce7a82da193ef5448e8c756c75e1452c1ce82eae93474c3181", + "7585339d4a23b4ce7a82da193ef5448e8c756c75e1452c1ce82eae93474c3181", + "7585339d4a23b4ce7a82da193ef5448e8c756c75e1452c1ce82eae93474c3181", + "7585339d4a23b4ce7a82da193ef5448e8c756c75e1452c1ce82eae93474c3181", + "7585339d4a23b4ce7a82da193ef5448e8c756c75e1452c1ce82eae93474c3181", + "7585339d4a23b4ce7a82da193ef5448e8c756c75e1452c1ce82eae93474c3181", + "361e04517bf63bf244aa7a3a4978931ee8d3e23b7222412dee30bf0564d87f40", + "361e04517bf63bf244aa7a3a4978931ee8d3e23b7222412dee30bf0564d87f40", + "6fa5dc6389a85bb69586c499820bbe84d174507bc9a21cbdc37785195e690ac0", + "6fa5dc6389a85bb69586c499820bbe84d174507bc9a21cbdc37785195e690ac0", + "6fa5dc6389a85bb69586c499820bbe84d174507bc9a21cbdc37785195e690ac0", + "6fa5dc6389a85bb69586c499820bbe84d174507bc9a21cbdc37785195e690ac0", + "6fa5dc6389a85bb69586c499820bbe84d174507bc9a21cbdc37785195e690ac0", + "a2a57935daf209acd37f3b42ee0e33b2b81e237e8f25920dcb1c1f822a5791be", + "435a6c2d10452e63594e6a3eefd34df27545a6b3dabfe0842928e3048fb3735e", + "435a6c2d10452e63594e6a3eefd34df27545a6b3dabfe0842928e3048fb3735e", + "435a6c2d10452e63594e6a3eefd34df27545a6b3dabfe0842928e3048fb3735e", + "435a6c2d10452e63594e6a3eefd34df27545a6b3dabfe0842928e3048fb3735e", + "8fbce097395dcdc38a5d1bc5cce31f76b7855cb820ca455d7a2e01bc9f0aa8be", + "bc78c0caef4e6bf9e9ebd5f7637b9efc1e6faa059d64bce549fc85e873b986b6", + "bc78c0caef4e6bf9e9ebd5f7637b9efc1e6faa059d64bce549fc85e873b986b6", + "c365f0d495e54938d95a586356e0e0669cde09c063ad5ba7a8f602cfe1054200", + "c365f0d495e54938d95a586356e0e0669cde09c063ad5ba7a8f602cfe1054200", + "ca2732c95a82bd8cbca0acb86af492b64e8335ed0c687434d2721e7fd158cb6d", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "b241ff754b2ceceaf300bb63641737c685ba750775cfbdb51ec7ed002c0ba32a", + "7f97b04207565039dd2804a48b8a75b92859583380f3fe7c203ba7f64f9cf769", + "7f97b04207565039dd2804a48b8a75b92859583380f3fe7c203ba7f64f9cf769", + "7f97b04207565039dd2804a48b8a75b92859583380f3fe7c203ba7f64f9cf769", + "7f97b04207565039dd2804a48b8a75b92859583380f3fe7c203ba7f64f9cf769", + "3c7c7b94f5ac169f50671684645d55237bdc164a97b36487a9c48f9f7f4a5dc4", + "3c7c7b94f5ac169f50671684645d55237bdc164a97b36487a9c48f9f7f4a5dc4", + "bbedd22ab80d2b9c0267ff6728104bfb4b091b9e3223e960de78ca64b8fdbbe2", + "bbedd22ab80d2b9c0267ff6728104bfb4b091b9e3223e960de78ca64b8fdbbe2", + "bbedd22ab80d2b9c0267ff6728104bfb4b091b9e3223e960de78ca64b8fdbbe2", + "b889ddbbf8cafe5c889e95b16a9d306184162432a04447d5069e0abac865b7a9", + "b889ddbbf8cafe5c889e95b16a9d306184162432a04447d5069e0abac865b7a9", + "b889ddbbf8cafe5c889e95b16a9d306184162432a04447d5069e0abac865b7a9", + "b889ddbbf8cafe5c889e95b16a9d306184162432a04447d5069e0abac865b7a9", + "5aa2a3a6303843703f127dd75f4aa627ea0313b68eefb9b434e2227bffe0609c", + "5aa2a3a6303843703f127dd75f4aa627ea0313b68eefb9b434e2227bffe0609c", + "5aa2a3a6303843703f127dd75f4aa627ea0313b68eefb9b434e2227bffe0609c", + "5aa2a3a6303843703f127dd75f4aa627ea0313b68eefb9b434e2227bffe0609c", + "5aa2a3a6303843703f127dd75f4aa627ea0313b68eefb9b434e2227bffe0609c", + "5aa2a3a6303843703f127dd75f4aa627ea0313b68eefb9b434e2227bffe0609c", + "5aa2a3a6303843703f127dd75f4aa627ea0313b68eefb9b434e2227bffe0609c", + "5aa2a3a6303843703f127dd75f4aa627ea0313b68eefb9b434e2227bffe0609c", + "c19c0f6dfee83475ff9748594ade73b89132657d8e6d21c4130f86db25f360db", + "de574c597b8109313c5abfabac17e2b3922627d0e8fc1e3dc2324c3620c0ecb4", + "de574c597b8109313c5abfabac17e2b3922627d0e8fc1e3dc2324c3620c0ecb4", + "de574c597b8109313c5abfabac17e2b3922627d0e8fc1e3dc2324c3620c0ecb4", + "de574c597b8109313c5abfabac17e2b3922627d0e8fc1e3dc2324c3620c0ecb4", + "6f3418a1a304668c0f7463fdc8b07ee739ada1af81e257efbd0afb59f987abbd", + "6f3418a1a304668c0f7463fdc8b07ee739ada1af81e257efbd0afb59f987abbd", + "1025e15a12d1d85f7f76b066aa6b65f60eb4131b547d3c162748dc7623d9f9e1", + "1025e15a12d1d85f7f76b066aa6b65f60eb4131b547d3c162748dc7623d9f9e1", + "1025e15a12d1d85f7f76b066aa6b65f60eb4131b547d3c162748dc7623d9f9e1", + "1025e15a12d1d85f7f76b066aa6b65f60eb4131b547d3c162748dc7623d9f9e1", + "1025e15a12d1d85f7f76b066aa6b65f60eb4131b547d3c162748dc7623d9f9e1", + "284c7bb66180ec94ad782713bcb416f0d7890c344e544e550195bfbc882459b7", + "c072683ae65b191f0eb0c1856872b3491d7ddbf7a57cde761c13fecbdacffc17", + "c072683ae65b191f0eb0c1856872b3491d7ddbf7a57cde761c13fecbdacffc17", + "c072683ae65b191f0eb0c1856872b3491d7ddbf7a57cde761c13fecbdacffc17", + "f7475420008c32a0fe3e1b8c9e91eefd903eb1a1fe951c56cb0b6a007ea432c2", + "f7475420008c32a0fe3e1b8c9e91eefd903eb1a1fe951c56cb0b6a007ea432c2", + "f7475420008c32a0fe3e1b8c9e91eefd903eb1a1fe951c56cb0b6a007ea432c2", + "f7475420008c32a0fe3e1b8c9e91eefd903eb1a1fe951c56cb0b6a007ea432c2", + "f7475420008c32a0fe3e1b8c9e91eefd903eb1a1fe951c56cb0b6a007ea432c2", + "f7475420008c32a0fe3e1b8c9e91eefd903eb1a1fe951c56cb0b6a007ea432c2", + "8553db1b10531875449b8fa2f1eee1b538a69ce0468bad64f3208922d6809efc", + "8553db1b10531875449b8fa2f1eee1b538a69ce0468bad64f3208922d6809efc", + "8553db1b10531875449b8fa2f1eee1b538a69ce0468bad64f3208922d6809efc", + "8553db1b10531875449b8fa2f1eee1b538a69ce0468bad64f3208922d6809efc", + "8553db1b10531875449b8fa2f1eee1b538a69ce0468bad64f3208922d6809efc", + "6db2ea801a30e80aded16095da9b5706bb905d70e7ccec9f59840fc6edc34397", + "6db2ea801a30e80aded16095da9b5706bb905d70e7ccec9f59840fc6edc34397", + "6db2ea801a30e80aded16095da9b5706bb905d70e7ccec9f59840fc6edc34397", + "9dd5240c22d7966d117e932e2f91ea7b985098d097e73cb78072095d437a8843", + "9dd5240c22d7966d117e932e2f91ea7b985098d097e73cb78072095d437a8843", + "9dd5240c22d7966d117e932e2f91ea7b985098d097e73cb78072095d437a8843", + "9e80cb75cdb81ba7da68f054a9c817b2bab0acbeaf39bfb538192e98f5615393", + "ba8f3c345664ea4510a13d3266f59415cfe7b6f1b0db7033e46ff983f60b969c", + "b1ded19aa74e0c74f5866cd253abfbb2967cfdc986f3d0e81658dbec26879355", + "b1ded19aa74e0c74f5866cd253abfbb2967cfdc986f3d0e81658dbec26879355", + "b1ded19aa74e0c74f5866cd253abfbb2967cfdc986f3d0e81658dbec26879355", + "b1ded19aa74e0c74f5866cd253abfbb2967cfdc986f3d0e81658dbec26879355", + "47b58dcd4f8741789de271e85069e4c204f060319c97845fe19fd438481e8574", + "47b58dcd4f8741789de271e85069e4c204f060319c97845fe19fd438481e8574", + "47b58dcd4f8741789de271e85069e4c204f060319c97845fe19fd438481e8574", + "47b58dcd4f8741789de271e85069e4c204f060319c97845fe19fd438481e8574", + "47b58dcd4f8741789de271e85069e4c204f060319c97845fe19fd438481e8574", + "47b58dcd4f8741789de271e85069e4c204f060319c97845fe19fd438481e8574", + "47b58dcd4f8741789de271e85069e4c204f060319c97845fe19fd438481e8574", + "47b58dcd4f8741789de271e85069e4c204f060319c97845fe19fd438481e8574", + "47b58dcd4f8741789de271e85069e4c204f060319c97845fe19fd438481e8574", + "7080a8bd0bc4691a5956e9e3ea43c0277871a780938c1d50696febbfb755e91d", + "7080a8bd0bc4691a5956e9e3ea43c0277871a780938c1d50696febbfb755e91d", + "7080a8bd0bc4691a5956e9e3ea43c0277871a780938c1d50696febbfb755e91d", + "7080a8bd0bc4691a5956e9e3ea43c0277871a780938c1d50696febbfb755e91d", + "7080a8bd0bc4691a5956e9e3ea43c0277871a780938c1d50696febbfb755e91d", + "7080a8bd0bc4691a5956e9e3ea43c0277871a780938c1d50696febbfb755e91d", + "7080a8bd0bc4691a5956e9e3ea43c0277871a780938c1d50696febbfb755e91d", + "7080a8bd0bc4691a5956e9e3ea43c0277871a780938c1d50696febbfb755e91d", + "e0968846b62b68268dbb0e93e946fea9426967346fe773f7b1ae83b13962ed9e", + "e0968846b62b68268dbb0e93e946fea9426967346fe773f7b1ae83b13962ed9e", + "10c890aba8bc254642115f956596d24bbb247fd48b144ae1fee65e7070999d8b", + "10c890aba8bc254642115f956596d24bbb247fd48b144ae1fee65e7070999d8b", + "10c890aba8bc254642115f956596d24bbb247fd48b144ae1fee65e7070999d8b", + "10c890aba8bc254642115f956596d24bbb247fd48b144ae1fee65e7070999d8b", + "10c890aba8bc254642115f956596d24bbb247fd48b144ae1fee65e7070999d8b", + "10c890aba8bc254642115f956596d24bbb247fd48b144ae1fee65e7070999d8b", + "10c890aba8bc254642115f956596d24bbb247fd48b144ae1fee65e7070999d8b", + "10c890aba8bc254642115f956596d24bbb247fd48b144ae1fee65e7070999d8b", + "10c890aba8bc254642115f956596d24bbb247fd48b144ae1fee65e7070999d8b", + "056458f99bd6cb9f627b7f9843add41b25a560c00f714d4c6fde3648aaf3bdc1", + "056458f99bd6cb9f627b7f9843add41b25a560c00f714d4c6fde3648aaf3bdc1", + "056458f99bd6cb9f627b7f9843add41b25a560c00f714d4c6fde3648aaf3bdc1", + "aefbeedce8f1f54263d456804cd9d744968e75b4652122016ce5181e01034735", + "aefbeedce8f1f54263d456804cd9d744968e75b4652122016ce5181e01034735", + "aefbeedce8f1f54263d456804cd9d744968e75b4652122016ce5181e01034735", + "aefbeedce8f1f54263d456804cd9d744968e75b4652122016ce5181e01034735", + "aefbeedce8f1f54263d456804cd9d744968e75b4652122016ce5181e01034735", + "aefbeedce8f1f54263d456804cd9d744968e75b4652122016ce5181e01034735", + "aefbeedce8f1f54263d456804cd9d744968e75b4652122016ce5181e01034735", + "aefbeedce8f1f54263d456804cd9d744968e75b4652122016ce5181e01034735", + "aefbeedce8f1f54263d456804cd9d744968e75b4652122016ce5181e01034735", + "4d84fb41234320f03b6e067d6048f97a43e309e80b8e7c8d5a49d485d1778240", + "72e604a55fa6572359c6e99b256ecb0dd99a3e0084f5a0cb075cd1f32db51b66", + "72e604a55fa6572359c6e99b256ecb0dd99a3e0084f5a0cb075cd1f32db51b66", + "72e604a55fa6572359c6e99b256ecb0dd99a3e0084f5a0cb075cd1f32db51b66", + "72e604a55fa6572359c6e99b256ecb0dd99a3e0084f5a0cb075cd1f32db51b66", + "72e604a55fa6572359c6e99b256ecb0dd99a3e0084f5a0cb075cd1f32db51b66", + "72e604a55fa6572359c6e99b256ecb0dd99a3e0084f5a0cb075cd1f32db51b66", + "72e604a55fa6572359c6e99b256ecb0dd99a3e0084f5a0cb075cd1f32db51b66" + ] +} \ No newline at end of file diff --git a/testdata/dind/030_error_determinism.eintlog b/testdata/dind/030_error_determinism.eintlog new file mode 100644 index 00000000..aacc4b43 Binary files /dev/null and b/testdata/dind/030_error_determinism.eintlog differ diff --git a/testdata/dind/030_error_determinism.hashes.json b/testdata/dind/030_error_determinism.hashes.json new file mode 100644 index 00000000..180e619b --- /dev/null +++ b/testdata/dind/030_error_determinism.hashes.json @@ -0,0 +1,15 @@ +{ + "elog_version": 1, + "schema_hash_hex": "0427e9fd236e92dfc8c0765f7bd429fc233bfbfab3cb67c9d03b22a0f31e7f8a", + "hash_domain": "DIND_STATE_HASH_V1", + "hash_alg": "BLAKE3", + "hashes_hex": [ + "e0c2acac7f7eca867f8575a1861460425976de2fb399146b694de06507a7f890", + "cd9dcb09fc5279eee4b6e4803eb2b9bc51caef33bbda612a0e890949d3c8832e", + "bcbe2957a9c35a36a7023eccf18d701a1119fa4df3f8782146080d9b4b9c4548", + "509ae9f2bc29b5cb3a26d3f99b0b9a6a248a189685645313f86b942bfac62a19", + "6cde55e849a2bc374b080524ca75d5eb24ce399c57e0ac910dad4e9272cfae54", + "89d3ded5bdd67246d7b9ecdd4de1759503e16c8f9fd2fdf391306a54d89ab672", + "7959f8942ad3ecf7e987e031b8aca37062d3eddd3544ffbe93d2905d2d940318" + ] +} \ No newline at end of file diff --git a/testdata/dind/050_randomized_order_small_seed0001.eintlog b/testdata/dind/050_randomized_order_small_seed0001.eintlog new file mode 100644 index 00000000..8feaf109 Binary files /dev/null and b/testdata/dind/050_randomized_order_small_seed0001.eintlog differ diff --git a/testdata/dind/050_randomized_order_small_seed0001.hashes.json b/testdata/dind/050_randomized_order_small_seed0001.hashes.json new file mode 100644 index 00000000..823cec0e --- /dev/null +++ b/testdata/dind/050_randomized_order_small_seed0001.hashes.json @@ -0,0 +1,209 @@ +{ + "elog_version": 1, + "schema_hash_hex": "0427e9fd236e92dfc8c0765f7bd429fc233bfbfab3cb67c9d03b22a0f31e7f8a", + "hash_domain": "DIND_STATE_HASH_V1", + "hash_alg": "BLAKE3", + "hashes_hex": [ + "e0c2acac7f7eca867f8575a1861460425976de2fb399146b694de06507a7f890", + "b11d14bf2f2f4549961e294b0180ede0120a273fbd56859152c97aa685915899", + "0ffb6879a98bb711a3cb5d8680651f0433f7d2fee22f3251c596d0f6ed4902cf", + "d74615efa3e0373c6b4d0eb5822e05573f49940ff63322e9507127b0725d337a", + "89119cd0409ae3d3126e0feead2a1832b3eb4521647b98475d23291b8eb99e36", + "fd1b3dac7aede525f224cbf6a579e22401eaf83934a931c2af688700b08dc218", + "ba6081d469bd57aa2898df9d9ec3a596233ceaf146e2bb0f9e8b510c7655a6de", + "3c119213749accb5ef33d4daad035a9930d8b5639961f3975be69df4cd0d0d88", + "2aebab7243f634193302d94fed55226dc668c5de55824f3a760d7cb79f64363c", + "dd8339e9526dda6d8f55e91a5605c871fa73301f122646c0e89888f210a7e1ca", + "5a2157245f81196cc84023e12e4ec52cbb1239061e66d8605c22bb96bd35ea27", + "89ff0d7e67d1ef4e1b15cf82fc629cd21d07e865161f9f0505b9658baaadc2d6", + "a4326f6160c429c3b5d0953fb15123a17b3f7b6b9230b937774c6d8191dead44", + "70383a3cdc20e958d306e6fcc42ac3dce0ca5f2eb96fd07baa0a3458e428f85f", + "eed0c84de6fecdcbad5c6141c3cae76f3399fa92563c0f01edf03036bc5862c4", + "8881f53d4f80e1e4e8de704219a462353a78face7dc89633f66f241dc3b29d22", + "f717e588f5a6204fcb136848a017a73aab71556f52ec9f84b050e0b426822fc2", + "4dcc8623833aa14ec1bbdd9ee9db0e2b1fcfe480eb790bdd19e51ec3f12cdd73", + "fc1d350b1726942ba899878eb2bc8a190302faa957080e917947d78c968bc94b", + "b1125aa963c7293399cc930b8c909be54b2259be740c985e50ef04afa744603c", + "79f1033de2af77236af48cf570bd9c04706611592afbb26a37e677493314fcc9", + "ad4c40951621e685718b6605d68c0a58d7f443c19a4d3d3ab4b92c80ad2ac57f", + "2b6cde2bb7094967ed537630475ef2d99d2507673992d926d264a28f4b295ac2", + "6a5b986963396ea668afb6752f6c62979be8d643b8a85307b7dd605c07ce5527", + "4212ed4dab302320c086af49f87ad77b8f227583106401d863677583ccac95f4", + "e76eff9b798052a9e0ca28a1d2d5de8ed3f046bf4f8f1499019efa279d0ad9f1", + "d70a717858ac668e2988117cbcfdd352166688299e32e4ec92c2844c0085cf17", + "1b9447457c336b027930aff9574fc772a5b2172de64382d48320ee94e733d01d", + "af1651b25e941b181ebb286dc28f95c311a6699185416575371b0989ed3f2a7f", + "709334251e51dbd326d67ed66ac9855b95e388e971ace8c71d9f390926ffd236", + "fa6d02c698dc7f3bda25c994270dcd9d2e9cdb40cc674da75e54ff8759be5033", + "022192de36f5d1a0d63b57868121894fc93a583dc9fe8bd3ca2917109cdb95db", + "ea68f98b601d0c3e671166f42ae24a70e19380f0fd35180b370df974a2f3101b", + "f353466acac89a2835d5dfda81104e72c9737ac5be6cdfd246357c1f8586b727", + "1f2d07863b8a29064f5b6e6ab6e9ba13878a88c621b729afc2db5928791fa8ba", + "dd5f9abc86be126b958c1a9fb46557ceddf7b0aa89416ed7439d30aa8ba5064f", + "aef856fdd7665c759c9bcfd50219d0610f46847cff064bab91cf04c388996c28", + "dd94cfd5af8dfbf33f80676e6e9608f5c6ec5322be23905953bffe4f17d20c90", + "0a0138bb770d7cdd9cb60001978997c973f9e4ae481ab5305388dfe8b066956c", + "7705e0bbf9dcc428ead323e054c43c6674841678082ebb8beae268552f3a2fe4", + "120c95791aca2cc7fd3bf827023ac022d1df8d174a99c3f50a700807b0cc7d57", + "073fe2b2e894eb5d94e1a8c3404f202768ecbc97f7ad70437dd3698202284930", + "c61e2208fbaea514718ff5e82a8c9a91f5ed97af4caa69dbb292d8377fa5dbab", + "a90e7e6725556ba32b8834f432589a0d7e4d0a37cd592300444b54b144860e53", + "fd83493b37b5ffc7ac596a099ef84f138f7c4f65e0bcc8d569b45b387b174f4e", + "624b11fd6844a3bd77e93d78027a788fb08132756cb4d3703083daebc4dd9504", + "fc217c41d47472d81256188b1a495aa19daaa473ed97084bdcc1a3a728237d9a", + "b0d95126b91e3553652366c0be8feffabe4e6d5cf5691d6e91c0388d5b24df89", + "1ccd1506bfc034310e6ac694b98eb39cdb624c37c67835463c96cf2ee1d223cc", + "23514f6442e61e4d1a04d6e67d7141dd4b73972462b4013673881e43b82bf753", + "a7d34d39cc297c528b36cce364c8d56180584f5f5ae1c9a9390c635aa1cd4455", + "5ee610fdbebc8b7f5f658a114aa5a93a9d02ef8b52451e9c9c3affd19782d6d7", + "a216c580771ada1585df18f97fd03dacb5d9e4da055bbf935520d55bfdc1c5cd", + "8e35c8db77c33eb6df9e266ffb926a8d6f8db48f97b7503301466484462f94e7", + "71d4af3d3c6b110729f28a121777a00a926e446b6e7724a86dadc3fa86935c99", + "9d904608c82edf082ad922c17ff85f1ded62800cadecbcb4c5ab4ecd09e709eb", + "b91fe21f4894e8bbd017181eab8a19c60c5bb5739343216d82de9ff36b52d4a2", + "f8013a937ab6a7f64665f2afcef3e3f29b19c92891c2a8778801fe96e65aa1a3", + "0144d88d949edf7950bc49946154989f4ba55e1164a3f0016520b2f8fc5e97e7", + "4e9518dcd1444db2d6d73607c4fef3754d14863ce06c44318dbe69dbbb06aec4", + "d1fa321d3c6102f6d30553c7ab13efd19b8e479e4e04f25cdb1487651ee6f217", + "85f8efa6f1d8173c8b97d91405b96efdfe626e36fb00fdd3992ae9d1e5ad1bcc", + "81f7ea589c7671987050bd383064926fc6ff761e9cc71d0640e9d9d2fdbab0c1", + "74abf3c9e5986310b2b32e30476b5f8e9d2eac0ca8ce3b7883bef9af5a2e2a65", + "8da2dfe1968174d2fa89d5514fc73b64c903034824467324f21a7d24389c6ec6", + "c96940f5e45e71eb234d8509f274a0dd0e9d0d11215a280612e4a6b58a1043d2", + "5752e648c7202099bf75479dae265c4d8814a41651055e8e9feb0ebf7f69f3f0", + "9788a43d65aea0fef04adaf0b3652ff3670437002eb7eac81be8b06b64bb6b11", + "5810a783c2880c3e0a752e03df6ba20deae1ab04bccb8d9ca041b8f7530858f2", + "193f04eb885a2734efae6d753dcca46a3f566d9e12400ae80d4e20cc5c0d8fa8", + "c98fe0ee4d4823c2d979567fdc8d916b251e2b78df7104b912ae8f1260300009", + "9b0d454c565ab7faa81a8bf64bde15ac9e54228be5eaa26b4eab7f4fb4de920d", + "e4a7fe8181de953353a5288afa9443c0f84c475efd1f7adf12df7146f7fb84f1", + "4974976dbd99dd1f9536c00ff8297ebe5729d83f870fe41d42ac215aeb7c8f57", + "312f52922222b754de69afb567e6da876b59256e465bac845b2b5f452fd17af5", + "d78c95b85c0d02700587405604b239775606f9c4c8c7e25ddf2a83b36d0d3a85", + "dc415fcb62dec91680dbe7979fdb663b572d9b6321586ba00ac9489cddcc2bb4", + "268505f6f922bed99012056051d892566d97e4500e82c4e2f900e99f8986dcce", + "2cb04c806aa8093f54cb4496bed4d511bc25a38113765af21af127f44390f5ea", + "b92bdb293d892ba9eda7c9fb8badc88bc39e69a79aaae6ce53fd582cf3a8d24b", + "caf6fa58f8672e8401642585be7df4c539d45ce6f4e14a1bf71f62036bc58e42", + "d6beae8f274e7e57f3b98ce04feb1833696db9e8a69a35dcea91e8ef5cbc771d", + "c9d9317a4d35d9860e64a971987a803957eadf6f239147a91888590a9038b1bc", + "095614f9d489fd6b28049eb360a450e57355ff40d9e6cfdefb2c47680f3d409c", + "bf2d2713e557afae108d6d92e1e9610d9a690998628b387456f84c45e4fafbce", + "29019a65247f2198a01fc9d41fda2cdc46add8869e172c2826364882b216f8ab", + "bec3628eedeea8a8ef6ebba5cd88639196b497877b0280fc46928f59ca82202d", + "a71cffcb3bda73047f04861de6d09beaf227d312157f47298f0f5e26fa8d80ef", + "8ba391aa91c66b7001bed072ba8af9d84b43a74b7a9fea0d82d3de4851036ec6", + "8bf20ca4b378fba7d22f96b96f500c51c10aac0119cb68a36f5bd0c3683ded75", + "a3e70b3a58f97a19622a179d402a305de13feae313c49f9167a3fc0fb94e9c6e", + "830800cd3cd3615f8c5aec1479169ce344c1db82bd6732f12de6df32811d567f", + "9daf9e85e9d49ea4362e14daf009a8b893d774b928be8c6a1efd771387450180", + "a3a5f02d1857c0ca1d3c82708e757b4454bab3a0ce8bb348605aca654cbf3f03", + "15e8d686f6f0a43d643dc6719efd52f2cdc8071ad0a3946bf936a0994fb725df", + "aeb24bab5ddfdfa3f133092612cf456034257bdd7824cb3e9b84540eb631024f", + "751afeb338eff260467f6f198476251d4b3d559755ad761b26b002c18ea0b9b2", + "a204bd2e196cd15be2aec095deac31b4783ea416506ea670ab3387e68c3dd855", + "b214f20923e79d476dc57dca306d5b0558bdf094d1908a643f8ba7c326a21a47", + "3bd82b97ce0188f0789ae455f8a943e3f63d23ab6d0c83158a36f8cfed7eeb00", + "1b5e9cccf85e1f5010e8cee51107977688ded75a2c5a397d9f41597b97bceb13", + "e60984003a761c37608db979e068f8645e1b8d0b55316a064710794cb4b16bee", + "f4ed1688093506f3029f5229b7ceb9f8769221947450b1209144c5aca1763cdb", + "e014b084b34509bc3c8b72b5142e69ea0a364d4b252520b8478a092b74e8a2f4", + "2893cab3bf3c556482289ecbf0c2ca9237bd9f8f325ffc726438286893d3f051", + "9cb0ef9510246913531ff478ba4540f457a52b775271c2ada25883be7a72e132", + "3e49ac0f3a23b95915349c1a6653abb57278e9d8bccea11671d5bab1b234a355", + "4df90913429103135d7e5ff2e2a507b1583a5351da90d298b63aaad01178a519", + "657978730668529681621ca71ac29b0ab8571fa68be542120c1e49ef4d42342e", + "87881d215f95a82b22a4b61f6b1bd1820dc4385bfe306a8fbe36ee9116e27f9d", + "aa108877ecf5880343943f8f61140a7074e7c6b76a448e2fd255141f38ac1f87", + "922031282cf91f74e940b02c687a7f8c2bbb153baaed2eab878cc1c3921d255a", + "462b027adc0b6284b2ac5733f9e8d3fb6b25525d8cdb60ce81302bf309b11f96", + "2338828b841c7d12bda6980f1ab4f0568dd1d687d02d778653f5165f48d153c6", + "1fb23bf20ccfb25376282f4f8309f8ff411fbcc86b4fa9e0386028684bb9d538", + "290d4b66f61ba8b8d5e15111a05e40e5fa3917b99be86a6b3ce6fe2add0a3afa", + "05c59712f7b6b58cd8c97c5c3746d47ec30d7ac7a2d1e12000b52b863661221f", + "66851d9a78b9a59a691b4485fbee60a27b9d2d0e43f314a9a79c7ac1b3e812a2", + "5fcbc2cc89edaab735d98beb528e487102a553857f216256c7754e6bb1b71edd", + "a532ac04ed3bd7b7e375ddcabc35985959229a85f6f461e43e45b1a237acea13", + "0a1bd0e6aa47a42af570c2eb5cbf2b117ad89fe1bd31381139d1ff526310ac6c", + "ac45f5486a6f0d09f404aece174c8319ea5f9998f40fc271ae00306133e10686", + "50764c078ee3c303013deca56e209cbcf05d2a7041819ac172bd92c0ee7ac057", + "8302c88e669b603b68d4e5dcf2e068f0baaf5e009fa07c8176c22b2e1db119fb", + "8f6df7374760ed7ac91b3fb22f22243ef37021e62133e9f1f982a066b9eaaa82", + "1a02d4df9b9ab4f3e0a508be8bcedc147257ba06efa855038ed19c7b4ad28505", + "3fb9563135aaa93e6d5a790a41dd07b862a34f7d60d40917288d706865eccad6", + "72dab0bc3e087d90858f75012201bc7140ab2836da9a8b9e944c8af07c57f3f2", + "5317ffcdd2b37c1453dce640b3b89f1f30e9464203d9b7541598eb2a88c762ef", + "ad0119687b9947cf78ed26c8a4bf873b58a4a66a8e6d8a0469273dba4cc4a942", + "c29400f61302e7fcede5fc06e2aeddac3a5bb17fa8965c8eef47fa3e54be3509", + "e628a00bc14bed339c68da1065711c32f975bc67be7ae6781c6fe50612485b98", + "5ab1438e674bfa9f4c5ad82b4118976b78c72c6c09b4423519f2563d3db23053", + "70b0a2ea7791ac8e1bea20570a0fab4f22a02f8e2c3429deae8182b8af65137f", + "ad9626f027eb711bf5646aaf2d813078b6488cf51002be3700557fd5d22d63e1", + "838a5f71b0ce8d3037dde3b72795ce7dec470525209b319431956bba237ac768", + "99b13fc6d8d4147fec005a0bd93cb72b8dceb33e65a645b65d14bcc8dfaa942d", + "d13385f81cf344984d073913a2045aec42a46a380a5fa664ae02b34d9d176594", + "1ecb7c5ae0f117fc7bfda24ecd7d3b87d6974bdd848e980a1886517286d36851", + "d9a7c6a9b1e4781c1034760940297b1df74b2f8df23a2ffbd3ea0325a3047827", + "55e49f66e1fae3a4c39cbe206785d28f9b165324a4351817f9618ed39d55caec", + "f88a71b3f2acc117ad025271bfc4a58305875f2f2ed0a5b958e80e01ed260f25", + "f83587b4f4aa0b1e4683ef10b36b9ecc43c2345a094f56530118ba49e7b8537d", + "288678686ef5bab520f3271a24066fcd06a37ae299b084750ae10d7b316c03be", + "00e8562dab48a730fabd71c9e5f725ac6ef7eb68c2977efdb658fa3ab3c960f7", + "c107a5c75523e34998eb4ba5a6460d644207728fdbaad33e09dbf9844e646a3e", + "9790ee47aef5ffac300d55a9c5d41777d2fdfb6635f0a89a22854715d140d713", + "e81c0c430d5cbb6d6da6bd2aa8095564d10e93d9efa2e109cffba440777efb8b", + "f9ea7e79356da9e82f9c56065985317229153738fd233175d6f1ac4b384cfee4", + "1b3a03878578f9efba965bc0d1454958e833940060b930aa80fe7ae7d9b70548", + "5bb850fb538cb7c344bcff57661183e0fb794bcc18fd41526e2e53165e1e7ef1", + "72f966c5a6a59650cf2c6f9777b4718c924dec43cf2c7e95f8a56bcdc519952c", + "28664177be00dfb016728a5183f3b5d25a5ce75e27057f57b2c30117619c437b", + "c36949ae29b10991b34b12acb347ebd806f6905342f6b724ddd38edd424081ea", + "25c79bb6fd9302e8d5ddaa9dca68fdbaa57307dc4473d1c36a3bd6d57c339e7d", + "d74cb0417d0961be1e11c162512845a25b9770e42a60ef822466fb00dda0c9ae", + "6d8dd363cb64a8cd9e983ae789e21047cdeac1ef0544246f3923d766c4df734a", + "69edca14a337881bcab82950363f197a230851c5e0035afae40ad39b7e616214", + "ed6153a27cba893083171013e03099cdcc4d81f1a7455839691b9b242b907d7d", + "73f89f0ec95e47e7dc2e00482094b9868a49b5e1e794fa4c7ddf91ebb6524973", + "e2dad1d6ed19bf9c38d3e3ca3289670c12aa483cddf9d45e791d15605910467e", + "51ed783d355d1aaf29df0fa98a7871aa2e24b58e7146b0c58873a572dac8c8e7", + "474dd02309d5500ecd1048a59dc079648da5cb7fab04790bf41fbc1dfa90d4b4", + "f5907faad2ae3bcc04a13e459ea68aa7f55b92b85456820d0c5e204e27093ed2", + "eebfc66ee05f06108d843b1181ace3d187dd0720eda8c91eb33f4699f0e37f76", + "e09c48a2b7e0a9cd18ec1fa1674e288413193c2c561e393ed4d6d73c51137331", + "46f796068fdb21ddea8588bc0794d42a14f52c47275f6102287118d2bfc2be08", + "09259220d2f3c4fa877c9ec3333e4b033cf461c33301e135c7219b064995fb4e", + "ff73c0f479f3ca2e6d596805cf4c72a0888e48215cf3084f0856745e560aca3f", + "307ebcfff65524c0a2689b71b448dca2e82d14b7e5921608365c1f28d46d8aa0", + "5af7c0d9a22328b111d472b3bd6d9cc0cae624e703d76d51fe3f526bf510aa94", + "0b0b5ebb29c566e03fa6ad524fb53fdc56ea72cde39b22bd38f491f6999976f7", + "cc3d363ddadf58b4ed1b6d1cff9b51d2a1110f7cd5a5f6963ec96b57e297ff2f", + "2cfc1804c4567a1378f0d1b7aadec978b71987505eb04646e029a8d15d84a37c", + "478733c017c0895a8a6d44aa3150bfab30d815d4ad1082eec5baf7e19b7593d0", + "549e0e235a4c7137baf2336e0a4175b3e966ba85b29893887e1df44d39e991d7", + "a9cc4dd21c2a3416666a87c06bb18d1ad9db47bdfad3c4a00a49f981e445c9b1", + "4c38349311446f6eb1103cbe64e79203e578be684738b72b67911c474b4b8b98", + "b6df5622a491908dd64386e5ce49d66ad60d1e5b818c428a9992ed9d06d4577f", + "cc0844aaee174943d079ed021f969cef2e59217e51f9b0a9b0fc380155fd2fc9", + "fcdf8e1f224240f549ff3916e6e6f4668c627ff47539ee174e2f5f5e0af8449f", + "9911f9f2b59f7028ea0aeaf15f25887bf9f893ca86f703c6653d71894da61df3", + "10b362ff66e26e102986a4700601e1b5f8ca0e1d54a6685fc341c609e1dc9e38", + "c18265949d77012099d0261021b3b21a2c0271f381c6f27f7d3617038a6b6705", + "5e7346a6e9d7e1b69d60b21c8f9f59e74bb1112a60db8c902028056358f9eb04", + "0de6b1870dcfa463912334f38805fbc1fec9f6ab5d8e95cf7c9052c7b19b7d54", + "91e9e7c5fd03e088fc20c4039f53bd99fb53c4a327ad755f961fad9b08375419", + "d8b0605cb561d275bd3ed5fa4e2c6d65d4740b66fa982c3b4561da4dde899643", + "d16eeaceb40ab53a94c2e028fce7f106519ad7ce05a5d636d3d7164ec9870043", + "85bf3a76c49bab28ca1f12f4c05f961dad919cb95dbcace53c1242b32461119b", + "ba879730884ead905747ae6eaa5219f9368d142953d47a5aa5dea6f02d97b7e7", + "0373bf9cd6d8cbec872d1d861e9829db87100c3c16d755187b914a774e2a0c86", + "0a691ae98dc8b38274aad40d5fedefb8e02d27fb8606d2ff0346461a0ccdc457", + "4387e7d7de5e777a701aefe7178727c12877d717be55b9893c38cf0c9637baf0", + "1b9ecb2e3c389529e54028e15d3888dfb9957c3c586ca7ad0fcc64b87112c9a0", + "8ee67ee03f806ddaf9fba77f3db58e1a9c5ed2e147f7958b331c7e2f23a713f7", + "4548bbed55d8662ce0a921fbd60dd844f1ebe850c2f5bf5c39fe6b83911350cb", + "4afc9f2b23bbfdc23feb1659da89117c8883d172c7efbde0ce1cee193988840a", + "e98b0c20e30ae6f532ab4bc043a0665576f3c0cb86136524f78115911acab764", + "1da65d34e71718d15375b976c953e7e2d93c093df018a3536eccd2af8d31cad1", + "0d545523954d772e0ddb00efe0cb39fe14053aea0689ee1df6123292dbded85c" + ] +} \ No newline at end of file diff --git a/testdata/dind/050_randomized_order_small_seed0002.eintlog b/testdata/dind/050_randomized_order_small_seed0002.eintlog new file mode 100644 index 00000000..8dc522f6 Binary files /dev/null and b/testdata/dind/050_randomized_order_small_seed0002.eintlog differ diff --git a/testdata/dind/050_randomized_order_small_seed0002.hashes.json b/testdata/dind/050_randomized_order_small_seed0002.hashes.json new file mode 100644 index 00000000..823cec0e --- /dev/null +++ b/testdata/dind/050_randomized_order_small_seed0002.hashes.json @@ -0,0 +1,209 @@ +{ + "elog_version": 1, + "schema_hash_hex": "0427e9fd236e92dfc8c0765f7bd429fc233bfbfab3cb67c9d03b22a0f31e7f8a", + "hash_domain": "DIND_STATE_HASH_V1", + "hash_alg": "BLAKE3", + "hashes_hex": [ + "e0c2acac7f7eca867f8575a1861460425976de2fb399146b694de06507a7f890", + "b11d14bf2f2f4549961e294b0180ede0120a273fbd56859152c97aa685915899", + "0ffb6879a98bb711a3cb5d8680651f0433f7d2fee22f3251c596d0f6ed4902cf", + "d74615efa3e0373c6b4d0eb5822e05573f49940ff63322e9507127b0725d337a", + "89119cd0409ae3d3126e0feead2a1832b3eb4521647b98475d23291b8eb99e36", + "fd1b3dac7aede525f224cbf6a579e22401eaf83934a931c2af688700b08dc218", + "ba6081d469bd57aa2898df9d9ec3a596233ceaf146e2bb0f9e8b510c7655a6de", + "3c119213749accb5ef33d4daad035a9930d8b5639961f3975be69df4cd0d0d88", + "2aebab7243f634193302d94fed55226dc668c5de55824f3a760d7cb79f64363c", + "dd8339e9526dda6d8f55e91a5605c871fa73301f122646c0e89888f210a7e1ca", + "5a2157245f81196cc84023e12e4ec52cbb1239061e66d8605c22bb96bd35ea27", + "89ff0d7e67d1ef4e1b15cf82fc629cd21d07e865161f9f0505b9658baaadc2d6", + "a4326f6160c429c3b5d0953fb15123a17b3f7b6b9230b937774c6d8191dead44", + "70383a3cdc20e958d306e6fcc42ac3dce0ca5f2eb96fd07baa0a3458e428f85f", + "eed0c84de6fecdcbad5c6141c3cae76f3399fa92563c0f01edf03036bc5862c4", + "8881f53d4f80e1e4e8de704219a462353a78face7dc89633f66f241dc3b29d22", + "f717e588f5a6204fcb136848a017a73aab71556f52ec9f84b050e0b426822fc2", + "4dcc8623833aa14ec1bbdd9ee9db0e2b1fcfe480eb790bdd19e51ec3f12cdd73", + "fc1d350b1726942ba899878eb2bc8a190302faa957080e917947d78c968bc94b", + "b1125aa963c7293399cc930b8c909be54b2259be740c985e50ef04afa744603c", + "79f1033de2af77236af48cf570bd9c04706611592afbb26a37e677493314fcc9", + "ad4c40951621e685718b6605d68c0a58d7f443c19a4d3d3ab4b92c80ad2ac57f", + "2b6cde2bb7094967ed537630475ef2d99d2507673992d926d264a28f4b295ac2", + "6a5b986963396ea668afb6752f6c62979be8d643b8a85307b7dd605c07ce5527", + "4212ed4dab302320c086af49f87ad77b8f227583106401d863677583ccac95f4", + "e76eff9b798052a9e0ca28a1d2d5de8ed3f046bf4f8f1499019efa279d0ad9f1", + "d70a717858ac668e2988117cbcfdd352166688299e32e4ec92c2844c0085cf17", + "1b9447457c336b027930aff9574fc772a5b2172de64382d48320ee94e733d01d", + "af1651b25e941b181ebb286dc28f95c311a6699185416575371b0989ed3f2a7f", + "709334251e51dbd326d67ed66ac9855b95e388e971ace8c71d9f390926ffd236", + "fa6d02c698dc7f3bda25c994270dcd9d2e9cdb40cc674da75e54ff8759be5033", + "022192de36f5d1a0d63b57868121894fc93a583dc9fe8bd3ca2917109cdb95db", + "ea68f98b601d0c3e671166f42ae24a70e19380f0fd35180b370df974a2f3101b", + "f353466acac89a2835d5dfda81104e72c9737ac5be6cdfd246357c1f8586b727", + "1f2d07863b8a29064f5b6e6ab6e9ba13878a88c621b729afc2db5928791fa8ba", + "dd5f9abc86be126b958c1a9fb46557ceddf7b0aa89416ed7439d30aa8ba5064f", + "aef856fdd7665c759c9bcfd50219d0610f46847cff064bab91cf04c388996c28", + "dd94cfd5af8dfbf33f80676e6e9608f5c6ec5322be23905953bffe4f17d20c90", + "0a0138bb770d7cdd9cb60001978997c973f9e4ae481ab5305388dfe8b066956c", + "7705e0bbf9dcc428ead323e054c43c6674841678082ebb8beae268552f3a2fe4", + "120c95791aca2cc7fd3bf827023ac022d1df8d174a99c3f50a700807b0cc7d57", + "073fe2b2e894eb5d94e1a8c3404f202768ecbc97f7ad70437dd3698202284930", + "c61e2208fbaea514718ff5e82a8c9a91f5ed97af4caa69dbb292d8377fa5dbab", + "a90e7e6725556ba32b8834f432589a0d7e4d0a37cd592300444b54b144860e53", + "fd83493b37b5ffc7ac596a099ef84f138f7c4f65e0bcc8d569b45b387b174f4e", + "624b11fd6844a3bd77e93d78027a788fb08132756cb4d3703083daebc4dd9504", + "fc217c41d47472d81256188b1a495aa19daaa473ed97084bdcc1a3a728237d9a", + "b0d95126b91e3553652366c0be8feffabe4e6d5cf5691d6e91c0388d5b24df89", + "1ccd1506bfc034310e6ac694b98eb39cdb624c37c67835463c96cf2ee1d223cc", + "23514f6442e61e4d1a04d6e67d7141dd4b73972462b4013673881e43b82bf753", + "a7d34d39cc297c528b36cce364c8d56180584f5f5ae1c9a9390c635aa1cd4455", + "5ee610fdbebc8b7f5f658a114aa5a93a9d02ef8b52451e9c9c3affd19782d6d7", + "a216c580771ada1585df18f97fd03dacb5d9e4da055bbf935520d55bfdc1c5cd", + "8e35c8db77c33eb6df9e266ffb926a8d6f8db48f97b7503301466484462f94e7", + "71d4af3d3c6b110729f28a121777a00a926e446b6e7724a86dadc3fa86935c99", + "9d904608c82edf082ad922c17ff85f1ded62800cadecbcb4c5ab4ecd09e709eb", + "b91fe21f4894e8bbd017181eab8a19c60c5bb5739343216d82de9ff36b52d4a2", + "f8013a937ab6a7f64665f2afcef3e3f29b19c92891c2a8778801fe96e65aa1a3", + "0144d88d949edf7950bc49946154989f4ba55e1164a3f0016520b2f8fc5e97e7", + "4e9518dcd1444db2d6d73607c4fef3754d14863ce06c44318dbe69dbbb06aec4", + "d1fa321d3c6102f6d30553c7ab13efd19b8e479e4e04f25cdb1487651ee6f217", + "85f8efa6f1d8173c8b97d91405b96efdfe626e36fb00fdd3992ae9d1e5ad1bcc", + "81f7ea589c7671987050bd383064926fc6ff761e9cc71d0640e9d9d2fdbab0c1", + "74abf3c9e5986310b2b32e30476b5f8e9d2eac0ca8ce3b7883bef9af5a2e2a65", + "8da2dfe1968174d2fa89d5514fc73b64c903034824467324f21a7d24389c6ec6", + "c96940f5e45e71eb234d8509f274a0dd0e9d0d11215a280612e4a6b58a1043d2", + "5752e648c7202099bf75479dae265c4d8814a41651055e8e9feb0ebf7f69f3f0", + "9788a43d65aea0fef04adaf0b3652ff3670437002eb7eac81be8b06b64bb6b11", + "5810a783c2880c3e0a752e03df6ba20deae1ab04bccb8d9ca041b8f7530858f2", + "193f04eb885a2734efae6d753dcca46a3f566d9e12400ae80d4e20cc5c0d8fa8", + "c98fe0ee4d4823c2d979567fdc8d916b251e2b78df7104b912ae8f1260300009", + "9b0d454c565ab7faa81a8bf64bde15ac9e54228be5eaa26b4eab7f4fb4de920d", + "e4a7fe8181de953353a5288afa9443c0f84c475efd1f7adf12df7146f7fb84f1", + "4974976dbd99dd1f9536c00ff8297ebe5729d83f870fe41d42ac215aeb7c8f57", + "312f52922222b754de69afb567e6da876b59256e465bac845b2b5f452fd17af5", + "d78c95b85c0d02700587405604b239775606f9c4c8c7e25ddf2a83b36d0d3a85", + "dc415fcb62dec91680dbe7979fdb663b572d9b6321586ba00ac9489cddcc2bb4", + "268505f6f922bed99012056051d892566d97e4500e82c4e2f900e99f8986dcce", + "2cb04c806aa8093f54cb4496bed4d511bc25a38113765af21af127f44390f5ea", + "b92bdb293d892ba9eda7c9fb8badc88bc39e69a79aaae6ce53fd582cf3a8d24b", + "caf6fa58f8672e8401642585be7df4c539d45ce6f4e14a1bf71f62036bc58e42", + "d6beae8f274e7e57f3b98ce04feb1833696db9e8a69a35dcea91e8ef5cbc771d", + "c9d9317a4d35d9860e64a971987a803957eadf6f239147a91888590a9038b1bc", + "095614f9d489fd6b28049eb360a450e57355ff40d9e6cfdefb2c47680f3d409c", + "bf2d2713e557afae108d6d92e1e9610d9a690998628b387456f84c45e4fafbce", + "29019a65247f2198a01fc9d41fda2cdc46add8869e172c2826364882b216f8ab", + "bec3628eedeea8a8ef6ebba5cd88639196b497877b0280fc46928f59ca82202d", + "a71cffcb3bda73047f04861de6d09beaf227d312157f47298f0f5e26fa8d80ef", + "8ba391aa91c66b7001bed072ba8af9d84b43a74b7a9fea0d82d3de4851036ec6", + "8bf20ca4b378fba7d22f96b96f500c51c10aac0119cb68a36f5bd0c3683ded75", + "a3e70b3a58f97a19622a179d402a305de13feae313c49f9167a3fc0fb94e9c6e", + "830800cd3cd3615f8c5aec1479169ce344c1db82bd6732f12de6df32811d567f", + "9daf9e85e9d49ea4362e14daf009a8b893d774b928be8c6a1efd771387450180", + "a3a5f02d1857c0ca1d3c82708e757b4454bab3a0ce8bb348605aca654cbf3f03", + "15e8d686f6f0a43d643dc6719efd52f2cdc8071ad0a3946bf936a0994fb725df", + "aeb24bab5ddfdfa3f133092612cf456034257bdd7824cb3e9b84540eb631024f", + "751afeb338eff260467f6f198476251d4b3d559755ad761b26b002c18ea0b9b2", + "a204bd2e196cd15be2aec095deac31b4783ea416506ea670ab3387e68c3dd855", + "b214f20923e79d476dc57dca306d5b0558bdf094d1908a643f8ba7c326a21a47", + "3bd82b97ce0188f0789ae455f8a943e3f63d23ab6d0c83158a36f8cfed7eeb00", + "1b5e9cccf85e1f5010e8cee51107977688ded75a2c5a397d9f41597b97bceb13", + "e60984003a761c37608db979e068f8645e1b8d0b55316a064710794cb4b16bee", + "f4ed1688093506f3029f5229b7ceb9f8769221947450b1209144c5aca1763cdb", + "e014b084b34509bc3c8b72b5142e69ea0a364d4b252520b8478a092b74e8a2f4", + "2893cab3bf3c556482289ecbf0c2ca9237bd9f8f325ffc726438286893d3f051", + "9cb0ef9510246913531ff478ba4540f457a52b775271c2ada25883be7a72e132", + "3e49ac0f3a23b95915349c1a6653abb57278e9d8bccea11671d5bab1b234a355", + "4df90913429103135d7e5ff2e2a507b1583a5351da90d298b63aaad01178a519", + "657978730668529681621ca71ac29b0ab8571fa68be542120c1e49ef4d42342e", + "87881d215f95a82b22a4b61f6b1bd1820dc4385bfe306a8fbe36ee9116e27f9d", + "aa108877ecf5880343943f8f61140a7074e7c6b76a448e2fd255141f38ac1f87", + "922031282cf91f74e940b02c687a7f8c2bbb153baaed2eab878cc1c3921d255a", + "462b027adc0b6284b2ac5733f9e8d3fb6b25525d8cdb60ce81302bf309b11f96", + "2338828b841c7d12bda6980f1ab4f0568dd1d687d02d778653f5165f48d153c6", + "1fb23bf20ccfb25376282f4f8309f8ff411fbcc86b4fa9e0386028684bb9d538", + "290d4b66f61ba8b8d5e15111a05e40e5fa3917b99be86a6b3ce6fe2add0a3afa", + "05c59712f7b6b58cd8c97c5c3746d47ec30d7ac7a2d1e12000b52b863661221f", + "66851d9a78b9a59a691b4485fbee60a27b9d2d0e43f314a9a79c7ac1b3e812a2", + "5fcbc2cc89edaab735d98beb528e487102a553857f216256c7754e6bb1b71edd", + "a532ac04ed3bd7b7e375ddcabc35985959229a85f6f461e43e45b1a237acea13", + "0a1bd0e6aa47a42af570c2eb5cbf2b117ad89fe1bd31381139d1ff526310ac6c", + "ac45f5486a6f0d09f404aece174c8319ea5f9998f40fc271ae00306133e10686", + "50764c078ee3c303013deca56e209cbcf05d2a7041819ac172bd92c0ee7ac057", + "8302c88e669b603b68d4e5dcf2e068f0baaf5e009fa07c8176c22b2e1db119fb", + "8f6df7374760ed7ac91b3fb22f22243ef37021e62133e9f1f982a066b9eaaa82", + "1a02d4df9b9ab4f3e0a508be8bcedc147257ba06efa855038ed19c7b4ad28505", + "3fb9563135aaa93e6d5a790a41dd07b862a34f7d60d40917288d706865eccad6", + "72dab0bc3e087d90858f75012201bc7140ab2836da9a8b9e944c8af07c57f3f2", + "5317ffcdd2b37c1453dce640b3b89f1f30e9464203d9b7541598eb2a88c762ef", + "ad0119687b9947cf78ed26c8a4bf873b58a4a66a8e6d8a0469273dba4cc4a942", + "c29400f61302e7fcede5fc06e2aeddac3a5bb17fa8965c8eef47fa3e54be3509", + "e628a00bc14bed339c68da1065711c32f975bc67be7ae6781c6fe50612485b98", + "5ab1438e674bfa9f4c5ad82b4118976b78c72c6c09b4423519f2563d3db23053", + "70b0a2ea7791ac8e1bea20570a0fab4f22a02f8e2c3429deae8182b8af65137f", + "ad9626f027eb711bf5646aaf2d813078b6488cf51002be3700557fd5d22d63e1", + "838a5f71b0ce8d3037dde3b72795ce7dec470525209b319431956bba237ac768", + "99b13fc6d8d4147fec005a0bd93cb72b8dceb33e65a645b65d14bcc8dfaa942d", + "d13385f81cf344984d073913a2045aec42a46a380a5fa664ae02b34d9d176594", + "1ecb7c5ae0f117fc7bfda24ecd7d3b87d6974bdd848e980a1886517286d36851", + "d9a7c6a9b1e4781c1034760940297b1df74b2f8df23a2ffbd3ea0325a3047827", + "55e49f66e1fae3a4c39cbe206785d28f9b165324a4351817f9618ed39d55caec", + "f88a71b3f2acc117ad025271bfc4a58305875f2f2ed0a5b958e80e01ed260f25", + "f83587b4f4aa0b1e4683ef10b36b9ecc43c2345a094f56530118ba49e7b8537d", + "288678686ef5bab520f3271a24066fcd06a37ae299b084750ae10d7b316c03be", + "00e8562dab48a730fabd71c9e5f725ac6ef7eb68c2977efdb658fa3ab3c960f7", + "c107a5c75523e34998eb4ba5a6460d644207728fdbaad33e09dbf9844e646a3e", + "9790ee47aef5ffac300d55a9c5d41777d2fdfb6635f0a89a22854715d140d713", + "e81c0c430d5cbb6d6da6bd2aa8095564d10e93d9efa2e109cffba440777efb8b", + "f9ea7e79356da9e82f9c56065985317229153738fd233175d6f1ac4b384cfee4", + "1b3a03878578f9efba965bc0d1454958e833940060b930aa80fe7ae7d9b70548", + "5bb850fb538cb7c344bcff57661183e0fb794bcc18fd41526e2e53165e1e7ef1", + "72f966c5a6a59650cf2c6f9777b4718c924dec43cf2c7e95f8a56bcdc519952c", + "28664177be00dfb016728a5183f3b5d25a5ce75e27057f57b2c30117619c437b", + "c36949ae29b10991b34b12acb347ebd806f6905342f6b724ddd38edd424081ea", + "25c79bb6fd9302e8d5ddaa9dca68fdbaa57307dc4473d1c36a3bd6d57c339e7d", + "d74cb0417d0961be1e11c162512845a25b9770e42a60ef822466fb00dda0c9ae", + "6d8dd363cb64a8cd9e983ae789e21047cdeac1ef0544246f3923d766c4df734a", + "69edca14a337881bcab82950363f197a230851c5e0035afae40ad39b7e616214", + "ed6153a27cba893083171013e03099cdcc4d81f1a7455839691b9b242b907d7d", + "73f89f0ec95e47e7dc2e00482094b9868a49b5e1e794fa4c7ddf91ebb6524973", + "e2dad1d6ed19bf9c38d3e3ca3289670c12aa483cddf9d45e791d15605910467e", + "51ed783d355d1aaf29df0fa98a7871aa2e24b58e7146b0c58873a572dac8c8e7", + "474dd02309d5500ecd1048a59dc079648da5cb7fab04790bf41fbc1dfa90d4b4", + "f5907faad2ae3bcc04a13e459ea68aa7f55b92b85456820d0c5e204e27093ed2", + "eebfc66ee05f06108d843b1181ace3d187dd0720eda8c91eb33f4699f0e37f76", + "e09c48a2b7e0a9cd18ec1fa1674e288413193c2c561e393ed4d6d73c51137331", + "46f796068fdb21ddea8588bc0794d42a14f52c47275f6102287118d2bfc2be08", + "09259220d2f3c4fa877c9ec3333e4b033cf461c33301e135c7219b064995fb4e", + "ff73c0f479f3ca2e6d596805cf4c72a0888e48215cf3084f0856745e560aca3f", + "307ebcfff65524c0a2689b71b448dca2e82d14b7e5921608365c1f28d46d8aa0", + "5af7c0d9a22328b111d472b3bd6d9cc0cae624e703d76d51fe3f526bf510aa94", + "0b0b5ebb29c566e03fa6ad524fb53fdc56ea72cde39b22bd38f491f6999976f7", + "cc3d363ddadf58b4ed1b6d1cff9b51d2a1110f7cd5a5f6963ec96b57e297ff2f", + "2cfc1804c4567a1378f0d1b7aadec978b71987505eb04646e029a8d15d84a37c", + "478733c017c0895a8a6d44aa3150bfab30d815d4ad1082eec5baf7e19b7593d0", + "549e0e235a4c7137baf2336e0a4175b3e966ba85b29893887e1df44d39e991d7", + "a9cc4dd21c2a3416666a87c06bb18d1ad9db47bdfad3c4a00a49f981e445c9b1", + "4c38349311446f6eb1103cbe64e79203e578be684738b72b67911c474b4b8b98", + "b6df5622a491908dd64386e5ce49d66ad60d1e5b818c428a9992ed9d06d4577f", + "cc0844aaee174943d079ed021f969cef2e59217e51f9b0a9b0fc380155fd2fc9", + "fcdf8e1f224240f549ff3916e6e6f4668c627ff47539ee174e2f5f5e0af8449f", + "9911f9f2b59f7028ea0aeaf15f25887bf9f893ca86f703c6653d71894da61df3", + "10b362ff66e26e102986a4700601e1b5f8ca0e1d54a6685fc341c609e1dc9e38", + "c18265949d77012099d0261021b3b21a2c0271f381c6f27f7d3617038a6b6705", + "5e7346a6e9d7e1b69d60b21c8f9f59e74bb1112a60db8c902028056358f9eb04", + "0de6b1870dcfa463912334f38805fbc1fec9f6ab5d8e95cf7c9052c7b19b7d54", + "91e9e7c5fd03e088fc20c4039f53bd99fb53c4a327ad755f961fad9b08375419", + "d8b0605cb561d275bd3ed5fa4e2c6d65d4740b66fa982c3b4561da4dde899643", + "d16eeaceb40ab53a94c2e028fce7f106519ad7ce05a5d636d3d7164ec9870043", + "85bf3a76c49bab28ca1f12f4c05f961dad919cb95dbcace53c1242b32461119b", + "ba879730884ead905747ae6eaa5219f9368d142953d47a5aa5dea6f02d97b7e7", + "0373bf9cd6d8cbec872d1d861e9829db87100c3c16d755187b914a774e2a0c86", + "0a691ae98dc8b38274aad40d5fedefb8e02d27fb8606d2ff0346461a0ccdc457", + "4387e7d7de5e777a701aefe7178727c12877d717be55b9893c38cf0c9637baf0", + "1b9ecb2e3c389529e54028e15d3888dfb9957c3c586ca7ad0fcc64b87112c9a0", + "8ee67ee03f806ddaf9fba77f3db58e1a9c5ed2e147f7958b331c7e2f23a713f7", + "4548bbed55d8662ce0a921fbd60dd844f1ebe850c2f5bf5c39fe6b83911350cb", + "4afc9f2b23bbfdc23feb1659da89117c8883d172c7efbde0ce1cee193988840a", + "e98b0c20e30ae6f532ab4bc043a0665576f3c0cb86136524f78115911acab764", + "1da65d34e71718d15375b976c953e7e2d93c093df018a3536eccd2af8d31cad1", + "0d545523954d772e0ddb00efe0cb39fe14053aea0689ee1df6123292dbded85c" + ] +} \ No newline at end of file diff --git a/testdata/dind/050_randomized_order_small_seed0003.eintlog b/testdata/dind/050_randomized_order_small_seed0003.eintlog new file mode 100644 index 00000000..ed3c364d Binary files /dev/null and b/testdata/dind/050_randomized_order_small_seed0003.eintlog differ diff --git a/testdata/dind/050_randomized_order_small_seed0003.hashes.json b/testdata/dind/050_randomized_order_small_seed0003.hashes.json new file mode 100644 index 00000000..823cec0e --- /dev/null +++ b/testdata/dind/050_randomized_order_small_seed0003.hashes.json @@ -0,0 +1,209 @@ +{ + "elog_version": 1, + "schema_hash_hex": "0427e9fd236e92dfc8c0765f7bd429fc233bfbfab3cb67c9d03b22a0f31e7f8a", + "hash_domain": "DIND_STATE_HASH_V1", + "hash_alg": "BLAKE3", + "hashes_hex": [ + "e0c2acac7f7eca867f8575a1861460425976de2fb399146b694de06507a7f890", + "b11d14bf2f2f4549961e294b0180ede0120a273fbd56859152c97aa685915899", + "0ffb6879a98bb711a3cb5d8680651f0433f7d2fee22f3251c596d0f6ed4902cf", + "d74615efa3e0373c6b4d0eb5822e05573f49940ff63322e9507127b0725d337a", + "89119cd0409ae3d3126e0feead2a1832b3eb4521647b98475d23291b8eb99e36", + "fd1b3dac7aede525f224cbf6a579e22401eaf83934a931c2af688700b08dc218", + "ba6081d469bd57aa2898df9d9ec3a596233ceaf146e2bb0f9e8b510c7655a6de", + "3c119213749accb5ef33d4daad035a9930d8b5639961f3975be69df4cd0d0d88", + "2aebab7243f634193302d94fed55226dc668c5de55824f3a760d7cb79f64363c", + "dd8339e9526dda6d8f55e91a5605c871fa73301f122646c0e89888f210a7e1ca", + "5a2157245f81196cc84023e12e4ec52cbb1239061e66d8605c22bb96bd35ea27", + "89ff0d7e67d1ef4e1b15cf82fc629cd21d07e865161f9f0505b9658baaadc2d6", + "a4326f6160c429c3b5d0953fb15123a17b3f7b6b9230b937774c6d8191dead44", + "70383a3cdc20e958d306e6fcc42ac3dce0ca5f2eb96fd07baa0a3458e428f85f", + "eed0c84de6fecdcbad5c6141c3cae76f3399fa92563c0f01edf03036bc5862c4", + "8881f53d4f80e1e4e8de704219a462353a78face7dc89633f66f241dc3b29d22", + "f717e588f5a6204fcb136848a017a73aab71556f52ec9f84b050e0b426822fc2", + "4dcc8623833aa14ec1bbdd9ee9db0e2b1fcfe480eb790bdd19e51ec3f12cdd73", + "fc1d350b1726942ba899878eb2bc8a190302faa957080e917947d78c968bc94b", + "b1125aa963c7293399cc930b8c909be54b2259be740c985e50ef04afa744603c", + "79f1033de2af77236af48cf570bd9c04706611592afbb26a37e677493314fcc9", + "ad4c40951621e685718b6605d68c0a58d7f443c19a4d3d3ab4b92c80ad2ac57f", + "2b6cde2bb7094967ed537630475ef2d99d2507673992d926d264a28f4b295ac2", + "6a5b986963396ea668afb6752f6c62979be8d643b8a85307b7dd605c07ce5527", + "4212ed4dab302320c086af49f87ad77b8f227583106401d863677583ccac95f4", + "e76eff9b798052a9e0ca28a1d2d5de8ed3f046bf4f8f1499019efa279d0ad9f1", + "d70a717858ac668e2988117cbcfdd352166688299e32e4ec92c2844c0085cf17", + "1b9447457c336b027930aff9574fc772a5b2172de64382d48320ee94e733d01d", + "af1651b25e941b181ebb286dc28f95c311a6699185416575371b0989ed3f2a7f", + "709334251e51dbd326d67ed66ac9855b95e388e971ace8c71d9f390926ffd236", + "fa6d02c698dc7f3bda25c994270dcd9d2e9cdb40cc674da75e54ff8759be5033", + "022192de36f5d1a0d63b57868121894fc93a583dc9fe8bd3ca2917109cdb95db", + "ea68f98b601d0c3e671166f42ae24a70e19380f0fd35180b370df974a2f3101b", + "f353466acac89a2835d5dfda81104e72c9737ac5be6cdfd246357c1f8586b727", + "1f2d07863b8a29064f5b6e6ab6e9ba13878a88c621b729afc2db5928791fa8ba", + "dd5f9abc86be126b958c1a9fb46557ceddf7b0aa89416ed7439d30aa8ba5064f", + "aef856fdd7665c759c9bcfd50219d0610f46847cff064bab91cf04c388996c28", + "dd94cfd5af8dfbf33f80676e6e9608f5c6ec5322be23905953bffe4f17d20c90", + "0a0138bb770d7cdd9cb60001978997c973f9e4ae481ab5305388dfe8b066956c", + "7705e0bbf9dcc428ead323e054c43c6674841678082ebb8beae268552f3a2fe4", + "120c95791aca2cc7fd3bf827023ac022d1df8d174a99c3f50a700807b0cc7d57", + "073fe2b2e894eb5d94e1a8c3404f202768ecbc97f7ad70437dd3698202284930", + "c61e2208fbaea514718ff5e82a8c9a91f5ed97af4caa69dbb292d8377fa5dbab", + "a90e7e6725556ba32b8834f432589a0d7e4d0a37cd592300444b54b144860e53", + "fd83493b37b5ffc7ac596a099ef84f138f7c4f65e0bcc8d569b45b387b174f4e", + "624b11fd6844a3bd77e93d78027a788fb08132756cb4d3703083daebc4dd9504", + "fc217c41d47472d81256188b1a495aa19daaa473ed97084bdcc1a3a728237d9a", + "b0d95126b91e3553652366c0be8feffabe4e6d5cf5691d6e91c0388d5b24df89", + "1ccd1506bfc034310e6ac694b98eb39cdb624c37c67835463c96cf2ee1d223cc", + "23514f6442e61e4d1a04d6e67d7141dd4b73972462b4013673881e43b82bf753", + "a7d34d39cc297c528b36cce364c8d56180584f5f5ae1c9a9390c635aa1cd4455", + "5ee610fdbebc8b7f5f658a114aa5a93a9d02ef8b52451e9c9c3affd19782d6d7", + "a216c580771ada1585df18f97fd03dacb5d9e4da055bbf935520d55bfdc1c5cd", + "8e35c8db77c33eb6df9e266ffb926a8d6f8db48f97b7503301466484462f94e7", + "71d4af3d3c6b110729f28a121777a00a926e446b6e7724a86dadc3fa86935c99", + "9d904608c82edf082ad922c17ff85f1ded62800cadecbcb4c5ab4ecd09e709eb", + "b91fe21f4894e8bbd017181eab8a19c60c5bb5739343216d82de9ff36b52d4a2", + "f8013a937ab6a7f64665f2afcef3e3f29b19c92891c2a8778801fe96e65aa1a3", + "0144d88d949edf7950bc49946154989f4ba55e1164a3f0016520b2f8fc5e97e7", + "4e9518dcd1444db2d6d73607c4fef3754d14863ce06c44318dbe69dbbb06aec4", + "d1fa321d3c6102f6d30553c7ab13efd19b8e479e4e04f25cdb1487651ee6f217", + "85f8efa6f1d8173c8b97d91405b96efdfe626e36fb00fdd3992ae9d1e5ad1bcc", + "81f7ea589c7671987050bd383064926fc6ff761e9cc71d0640e9d9d2fdbab0c1", + "74abf3c9e5986310b2b32e30476b5f8e9d2eac0ca8ce3b7883bef9af5a2e2a65", + "8da2dfe1968174d2fa89d5514fc73b64c903034824467324f21a7d24389c6ec6", + "c96940f5e45e71eb234d8509f274a0dd0e9d0d11215a280612e4a6b58a1043d2", + "5752e648c7202099bf75479dae265c4d8814a41651055e8e9feb0ebf7f69f3f0", + "9788a43d65aea0fef04adaf0b3652ff3670437002eb7eac81be8b06b64bb6b11", + "5810a783c2880c3e0a752e03df6ba20deae1ab04bccb8d9ca041b8f7530858f2", + "193f04eb885a2734efae6d753dcca46a3f566d9e12400ae80d4e20cc5c0d8fa8", + "c98fe0ee4d4823c2d979567fdc8d916b251e2b78df7104b912ae8f1260300009", + "9b0d454c565ab7faa81a8bf64bde15ac9e54228be5eaa26b4eab7f4fb4de920d", + "e4a7fe8181de953353a5288afa9443c0f84c475efd1f7adf12df7146f7fb84f1", + "4974976dbd99dd1f9536c00ff8297ebe5729d83f870fe41d42ac215aeb7c8f57", + "312f52922222b754de69afb567e6da876b59256e465bac845b2b5f452fd17af5", + "d78c95b85c0d02700587405604b239775606f9c4c8c7e25ddf2a83b36d0d3a85", + "dc415fcb62dec91680dbe7979fdb663b572d9b6321586ba00ac9489cddcc2bb4", + "268505f6f922bed99012056051d892566d97e4500e82c4e2f900e99f8986dcce", + "2cb04c806aa8093f54cb4496bed4d511bc25a38113765af21af127f44390f5ea", + "b92bdb293d892ba9eda7c9fb8badc88bc39e69a79aaae6ce53fd582cf3a8d24b", + "caf6fa58f8672e8401642585be7df4c539d45ce6f4e14a1bf71f62036bc58e42", + "d6beae8f274e7e57f3b98ce04feb1833696db9e8a69a35dcea91e8ef5cbc771d", + "c9d9317a4d35d9860e64a971987a803957eadf6f239147a91888590a9038b1bc", + "095614f9d489fd6b28049eb360a450e57355ff40d9e6cfdefb2c47680f3d409c", + "bf2d2713e557afae108d6d92e1e9610d9a690998628b387456f84c45e4fafbce", + "29019a65247f2198a01fc9d41fda2cdc46add8869e172c2826364882b216f8ab", + "bec3628eedeea8a8ef6ebba5cd88639196b497877b0280fc46928f59ca82202d", + "a71cffcb3bda73047f04861de6d09beaf227d312157f47298f0f5e26fa8d80ef", + "8ba391aa91c66b7001bed072ba8af9d84b43a74b7a9fea0d82d3de4851036ec6", + "8bf20ca4b378fba7d22f96b96f500c51c10aac0119cb68a36f5bd0c3683ded75", + "a3e70b3a58f97a19622a179d402a305de13feae313c49f9167a3fc0fb94e9c6e", + "830800cd3cd3615f8c5aec1479169ce344c1db82bd6732f12de6df32811d567f", + "9daf9e85e9d49ea4362e14daf009a8b893d774b928be8c6a1efd771387450180", + "a3a5f02d1857c0ca1d3c82708e757b4454bab3a0ce8bb348605aca654cbf3f03", + "15e8d686f6f0a43d643dc6719efd52f2cdc8071ad0a3946bf936a0994fb725df", + "aeb24bab5ddfdfa3f133092612cf456034257bdd7824cb3e9b84540eb631024f", + "751afeb338eff260467f6f198476251d4b3d559755ad761b26b002c18ea0b9b2", + "a204bd2e196cd15be2aec095deac31b4783ea416506ea670ab3387e68c3dd855", + "b214f20923e79d476dc57dca306d5b0558bdf094d1908a643f8ba7c326a21a47", + "3bd82b97ce0188f0789ae455f8a943e3f63d23ab6d0c83158a36f8cfed7eeb00", + "1b5e9cccf85e1f5010e8cee51107977688ded75a2c5a397d9f41597b97bceb13", + "e60984003a761c37608db979e068f8645e1b8d0b55316a064710794cb4b16bee", + "f4ed1688093506f3029f5229b7ceb9f8769221947450b1209144c5aca1763cdb", + "e014b084b34509bc3c8b72b5142e69ea0a364d4b252520b8478a092b74e8a2f4", + "2893cab3bf3c556482289ecbf0c2ca9237bd9f8f325ffc726438286893d3f051", + "9cb0ef9510246913531ff478ba4540f457a52b775271c2ada25883be7a72e132", + "3e49ac0f3a23b95915349c1a6653abb57278e9d8bccea11671d5bab1b234a355", + "4df90913429103135d7e5ff2e2a507b1583a5351da90d298b63aaad01178a519", + "657978730668529681621ca71ac29b0ab8571fa68be542120c1e49ef4d42342e", + "87881d215f95a82b22a4b61f6b1bd1820dc4385bfe306a8fbe36ee9116e27f9d", + "aa108877ecf5880343943f8f61140a7074e7c6b76a448e2fd255141f38ac1f87", + "922031282cf91f74e940b02c687a7f8c2bbb153baaed2eab878cc1c3921d255a", + "462b027adc0b6284b2ac5733f9e8d3fb6b25525d8cdb60ce81302bf309b11f96", + "2338828b841c7d12bda6980f1ab4f0568dd1d687d02d778653f5165f48d153c6", + "1fb23bf20ccfb25376282f4f8309f8ff411fbcc86b4fa9e0386028684bb9d538", + "290d4b66f61ba8b8d5e15111a05e40e5fa3917b99be86a6b3ce6fe2add0a3afa", + "05c59712f7b6b58cd8c97c5c3746d47ec30d7ac7a2d1e12000b52b863661221f", + "66851d9a78b9a59a691b4485fbee60a27b9d2d0e43f314a9a79c7ac1b3e812a2", + "5fcbc2cc89edaab735d98beb528e487102a553857f216256c7754e6bb1b71edd", + "a532ac04ed3bd7b7e375ddcabc35985959229a85f6f461e43e45b1a237acea13", + "0a1bd0e6aa47a42af570c2eb5cbf2b117ad89fe1bd31381139d1ff526310ac6c", + "ac45f5486a6f0d09f404aece174c8319ea5f9998f40fc271ae00306133e10686", + "50764c078ee3c303013deca56e209cbcf05d2a7041819ac172bd92c0ee7ac057", + "8302c88e669b603b68d4e5dcf2e068f0baaf5e009fa07c8176c22b2e1db119fb", + "8f6df7374760ed7ac91b3fb22f22243ef37021e62133e9f1f982a066b9eaaa82", + "1a02d4df9b9ab4f3e0a508be8bcedc147257ba06efa855038ed19c7b4ad28505", + "3fb9563135aaa93e6d5a790a41dd07b862a34f7d60d40917288d706865eccad6", + "72dab0bc3e087d90858f75012201bc7140ab2836da9a8b9e944c8af07c57f3f2", + "5317ffcdd2b37c1453dce640b3b89f1f30e9464203d9b7541598eb2a88c762ef", + "ad0119687b9947cf78ed26c8a4bf873b58a4a66a8e6d8a0469273dba4cc4a942", + "c29400f61302e7fcede5fc06e2aeddac3a5bb17fa8965c8eef47fa3e54be3509", + "e628a00bc14bed339c68da1065711c32f975bc67be7ae6781c6fe50612485b98", + "5ab1438e674bfa9f4c5ad82b4118976b78c72c6c09b4423519f2563d3db23053", + "70b0a2ea7791ac8e1bea20570a0fab4f22a02f8e2c3429deae8182b8af65137f", + "ad9626f027eb711bf5646aaf2d813078b6488cf51002be3700557fd5d22d63e1", + "838a5f71b0ce8d3037dde3b72795ce7dec470525209b319431956bba237ac768", + "99b13fc6d8d4147fec005a0bd93cb72b8dceb33e65a645b65d14bcc8dfaa942d", + "d13385f81cf344984d073913a2045aec42a46a380a5fa664ae02b34d9d176594", + "1ecb7c5ae0f117fc7bfda24ecd7d3b87d6974bdd848e980a1886517286d36851", + "d9a7c6a9b1e4781c1034760940297b1df74b2f8df23a2ffbd3ea0325a3047827", + "55e49f66e1fae3a4c39cbe206785d28f9b165324a4351817f9618ed39d55caec", + "f88a71b3f2acc117ad025271bfc4a58305875f2f2ed0a5b958e80e01ed260f25", + "f83587b4f4aa0b1e4683ef10b36b9ecc43c2345a094f56530118ba49e7b8537d", + "288678686ef5bab520f3271a24066fcd06a37ae299b084750ae10d7b316c03be", + "00e8562dab48a730fabd71c9e5f725ac6ef7eb68c2977efdb658fa3ab3c960f7", + "c107a5c75523e34998eb4ba5a6460d644207728fdbaad33e09dbf9844e646a3e", + "9790ee47aef5ffac300d55a9c5d41777d2fdfb6635f0a89a22854715d140d713", + "e81c0c430d5cbb6d6da6bd2aa8095564d10e93d9efa2e109cffba440777efb8b", + "f9ea7e79356da9e82f9c56065985317229153738fd233175d6f1ac4b384cfee4", + "1b3a03878578f9efba965bc0d1454958e833940060b930aa80fe7ae7d9b70548", + "5bb850fb538cb7c344bcff57661183e0fb794bcc18fd41526e2e53165e1e7ef1", + "72f966c5a6a59650cf2c6f9777b4718c924dec43cf2c7e95f8a56bcdc519952c", + "28664177be00dfb016728a5183f3b5d25a5ce75e27057f57b2c30117619c437b", + "c36949ae29b10991b34b12acb347ebd806f6905342f6b724ddd38edd424081ea", + "25c79bb6fd9302e8d5ddaa9dca68fdbaa57307dc4473d1c36a3bd6d57c339e7d", + "d74cb0417d0961be1e11c162512845a25b9770e42a60ef822466fb00dda0c9ae", + "6d8dd363cb64a8cd9e983ae789e21047cdeac1ef0544246f3923d766c4df734a", + "69edca14a337881bcab82950363f197a230851c5e0035afae40ad39b7e616214", + "ed6153a27cba893083171013e03099cdcc4d81f1a7455839691b9b242b907d7d", + "73f89f0ec95e47e7dc2e00482094b9868a49b5e1e794fa4c7ddf91ebb6524973", + "e2dad1d6ed19bf9c38d3e3ca3289670c12aa483cddf9d45e791d15605910467e", + "51ed783d355d1aaf29df0fa98a7871aa2e24b58e7146b0c58873a572dac8c8e7", + "474dd02309d5500ecd1048a59dc079648da5cb7fab04790bf41fbc1dfa90d4b4", + "f5907faad2ae3bcc04a13e459ea68aa7f55b92b85456820d0c5e204e27093ed2", + "eebfc66ee05f06108d843b1181ace3d187dd0720eda8c91eb33f4699f0e37f76", + "e09c48a2b7e0a9cd18ec1fa1674e288413193c2c561e393ed4d6d73c51137331", + "46f796068fdb21ddea8588bc0794d42a14f52c47275f6102287118d2bfc2be08", + "09259220d2f3c4fa877c9ec3333e4b033cf461c33301e135c7219b064995fb4e", + "ff73c0f479f3ca2e6d596805cf4c72a0888e48215cf3084f0856745e560aca3f", + "307ebcfff65524c0a2689b71b448dca2e82d14b7e5921608365c1f28d46d8aa0", + "5af7c0d9a22328b111d472b3bd6d9cc0cae624e703d76d51fe3f526bf510aa94", + "0b0b5ebb29c566e03fa6ad524fb53fdc56ea72cde39b22bd38f491f6999976f7", + "cc3d363ddadf58b4ed1b6d1cff9b51d2a1110f7cd5a5f6963ec96b57e297ff2f", + "2cfc1804c4567a1378f0d1b7aadec978b71987505eb04646e029a8d15d84a37c", + "478733c017c0895a8a6d44aa3150bfab30d815d4ad1082eec5baf7e19b7593d0", + "549e0e235a4c7137baf2336e0a4175b3e966ba85b29893887e1df44d39e991d7", + "a9cc4dd21c2a3416666a87c06bb18d1ad9db47bdfad3c4a00a49f981e445c9b1", + "4c38349311446f6eb1103cbe64e79203e578be684738b72b67911c474b4b8b98", + "b6df5622a491908dd64386e5ce49d66ad60d1e5b818c428a9992ed9d06d4577f", + "cc0844aaee174943d079ed021f969cef2e59217e51f9b0a9b0fc380155fd2fc9", + "fcdf8e1f224240f549ff3916e6e6f4668c627ff47539ee174e2f5f5e0af8449f", + "9911f9f2b59f7028ea0aeaf15f25887bf9f893ca86f703c6653d71894da61df3", + "10b362ff66e26e102986a4700601e1b5f8ca0e1d54a6685fc341c609e1dc9e38", + "c18265949d77012099d0261021b3b21a2c0271f381c6f27f7d3617038a6b6705", + "5e7346a6e9d7e1b69d60b21c8f9f59e74bb1112a60db8c902028056358f9eb04", + "0de6b1870dcfa463912334f38805fbc1fec9f6ab5d8e95cf7c9052c7b19b7d54", + "91e9e7c5fd03e088fc20c4039f53bd99fb53c4a327ad755f961fad9b08375419", + "d8b0605cb561d275bd3ed5fa4e2c6d65d4740b66fa982c3b4561da4dde899643", + "d16eeaceb40ab53a94c2e028fce7f106519ad7ce05a5d636d3d7164ec9870043", + "85bf3a76c49bab28ca1f12f4c05f961dad919cb95dbcace53c1242b32461119b", + "ba879730884ead905747ae6eaa5219f9368d142953d47a5aa5dea6f02d97b7e7", + "0373bf9cd6d8cbec872d1d861e9829db87100c3c16d755187b914a774e2a0c86", + "0a691ae98dc8b38274aad40d5fedefb8e02d27fb8606d2ff0346461a0ccdc457", + "4387e7d7de5e777a701aefe7178727c12877d717be55b9893c38cf0c9637baf0", + "1b9ecb2e3c389529e54028e15d3888dfb9957c3c586ca7ad0fcc64b87112c9a0", + "8ee67ee03f806ddaf9fba77f3db58e1a9c5ed2e147f7958b331c7e2f23a713f7", + "4548bbed55d8662ce0a921fbd60dd844f1ebe850c2f5bf5c39fe6b83911350cb", + "4afc9f2b23bbfdc23feb1659da89117c8883d172c7efbde0ce1cee193988840a", + "e98b0c20e30ae6f532ab4bc043a0665576f3c0cb86136524f78115911acab764", + "1da65d34e71718d15375b976c953e7e2d93c093df018a3536eccd2af8d31cad1", + "0d545523954d772e0ddb00efe0cb39fe14053aea0689ee1df6123292dbded85c" + ] +} \ No newline at end of file diff --git a/testdata/dind/051_randomized_convergent_seed0001.eintlog b/testdata/dind/051_randomized_convergent_seed0001.eintlog new file mode 100644 index 00000000..b7242e79 Binary files /dev/null and b/testdata/dind/051_randomized_convergent_seed0001.eintlog differ diff --git a/testdata/dind/051_randomized_convergent_seed0001.hashes.json b/testdata/dind/051_randomized_convergent_seed0001.hashes.json new file mode 100644 index 00000000..22b993c1 --- /dev/null +++ b/testdata/dind/051_randomized_convergent_seed0001.hashes.json @@ -0,0 +1,209 @@ +{ + "elog_version": 1, + "schema_hash_hex": "0427e9fd236e92dfc8c0765f7bd429fc233bfbfab3cb67c9d03b22a0f31e7f8a", + "hash_domain": "DIND_STATE_HASH_V1", + "hash_alg": "BLAKE3", + "hashes_hex": [ + "e0c2acac7f7eca867f8575a1861460425976de2fb399146b694de06507a7f890", + "b365b8482dc3f8c156d128fcaa57813d72fb3bd2fb472080efd2f27ba292c9c9", + "dea5e27435e69c4272f4547d9450b775f786c949444f9f08ec5a7a7da2dc6e16", + "af6d16142f8d7445d3536c398c0703036ad44128e4afbd739beba8efe8b82e1d", + "5ce5f896b5caf0032d8eb28d2660be7101758751847ced19d99d790ac8218331", + "93e14bf0caf03a7b7ce38f90314567ad7f8c956fe6f114621fee9fd577767da8", + "a94508aca956f958300b3680e20f05e30356143ccc556686d71cb7a1f73592ae", + "fb4ccf6585bd9561f6ebaaceca693d6bb3946422ef7bd5db394c278e271b801d", + "80c59181a8602640a3e9c75a1f9be2f476ee8be7213722852c43b92c4c4ef378", + "8d44842d240132a33ca49c8a88647b7be2f7a361e5aa4d4ae18fb8bdb38ca619", + "8248119aca0e214eba65b9bf895cf80e9c825962b364e293b20dadede72f31d9", + "75e6f92d6dbad6e61637c116b1c33e8dd7b773d39d7e319d04066217fa9a1cff", + "41b43cecb713ee500317782fd009e86b6bdd9cecbc9af8e6808fad426b13a76a", + "f0789b3d5379004fc116f663d66eaa6de16a877e46fe056c116236187d1098ce", + "0707ba06464a150ebb18f1dd6a7bebdc89a853a36e7bcafc9487687545a5b106", + "bb86bf569bc28a13ce356985575fc147c958a834fd769e62a045452b4362e7a0", + "f91e48a2da9099daa988b7868a4c311f9a024b511df869dd6f4592557863251f", + "3a6531b905ca47d092993c87dc7a38e11015b568c68a77934130cd9c9dbdb7cd", + "1912ddd864ccb84598238a35d449d53ee6147f0592a493f41d865d65b6f0fde8", + "36d43990041b0301b35f686767c37c8127b570c11e1f165238cde4db7b9de841", + "7be3efec1dd40fd4d2bd52cf830d2879c0a61197c1ca1c9cd724a1789fd46ab8", + "5740303eb6fa20813e6abe86f12a91eb10ae3ba40df07dd443bcbc405d1898c3", + "97a1d169ef7998c4fd2f0e4b010acabd89481f869776bb1c9f87a9cb5c435e4a", + "0e39e0b86542ac8356272efd7f02bd9d40880892acd7bde7d5e2f3fcfd0059e2", + "0385206c7ea684f99043e83d31194a1c9bb1802bf251ec6d260e5d56ef8beddd", + "2fca635758fdf7a1903b7d6db50f96170f6a85421a1d9e16cb8de8282de1d6ed", + "f55e449866aeaf00a22860c07e723404b7156b809d738fb5b96a68611ca9a852", + "774f92ab9e49b27fa27f0a8e58b84bd7b886237f113e0db8c0279cca56b0555e", + "a1fb18c910a709474dc8b5cf4deddc00c1f320a89cbe3e1faee1d3ee83f95407", + "e1ca90c3d6558db907af5fd8c9ebb9b5697e82e18542ca6e42c2ac464f971fbe", + "3a6b5f83893f07484bce196137b3eb78ddc9658dccefe971f44fd512839bd70c", + "49b1220fd877fc42656facb94e023591b8269040f3ad887e87d49f0ac86bf076", + "0c37f7e0704075e65700b9f7e5a487d3714a2139f7696dbff80e81e099b72500", + "7e3a66acb3a67afda8f4b6da1bcd337d7eeb9ec86ba6b359c39f1c8c2509a0c3", + "6926c84264e33df803c113f62271a235bf5d72d7404aecc5fa2529a11601f406", + "56c2cf3fd4dd99c4db610fd84baa0fa65fbbc40c9e7a97badda0cd92cce2d494", + "8373a0e6d35a522cb79cd32965ff50c3c9a64baaf1de8f6b5d0dd1a151925629", + "1edb31b870e86fa4f94030988e144a4bf73b96a0cd2cbb29ebfca1a3b40bd35e", + "4c1967f4e8d704482ef1c04304bb4b0bbf6611c62f4fa54eab24ea5001135dab", + "f17011fead92eb2f34b8e0962545ee4830fc466196dac1b15144caf422f58cc1", + "cd6f410497a89a7e0c5f156863c9254406cb1f654d2d53bd6966c044b3ec5115", + "510f208f4e4a102500f89c6a7acbe82d0c60c30619dc15a74249b84177673bf0", + "ca9be01bbc7e8bb79730c0a65c41dd7228a0f4287afb8113b1ce2adc405a74a2", + "6b1982b5fa4151c58f9c2e8f8169681051e7ca03bbcd815af2488330773a5494", + "68a50cb2365f63c712e138cb772bc3635b074b9ccbdcc3954dab3b1f2e0619c3", + "5aae56f59d7baa2fac52d1cedc97f65fc1157a2b64b1c004cf1014f12873440c", + "7f92b2075587d0b8b2dc1d6481a8c32bdb8734579d0a36f63fd958958bd9f1a2", + "5e1c505c665fba2525e5366aa66a7a09afcdaac0b628148ef54820d547658c59", + "327f41cf3c1dea9336a4af3ff43538a2561189f2a36da595b534f7e0160d8cda", + "855676204d0a1c605c525869dd35ba95203f4f6476705d22d90a467820a28cab", + "bbb8dc52e00d0afe66c2258537dd17c54adc226ed4e6f38b4af2384bdc3665d3", + "fac3f57dd4d76c63e124f07863b811a4c0e11c162bf252076d53d39530c28992", + "45722220c0a6c0497a6429ceb8b42f5d6ff6ccb634a47ebb0db61c893f94cf29", + "dea33f536b9118cc3ef614d3df26f17a68a83fff4ab9065a3fc117b861ff49f2", + "c345210c21ae9377d950750c2e38553dd3e2d40aa5ef20760f079f0d834cf6bc", + "9fa04d8a864b2b0d1014d9cdeaf01f5b7a6483897a16114d636a31497d001249", + "8d3471f6dcdb5b4609b5f39fce83a3bd6d7478997191e613ed60860bd8dc71ff", + "ba0ed6141c2914b6614723b837156af1a9e4bdcf50e014cc9e61b6cfa2d06c1a", + "3e8bd3e5bde2f44e3e042576f6ff5f44edc3f3fcabbeb42e8e925a4b2dec5f92", + "84a4ea17256cfec11da144c9d57fdc990e75038dd30f47b17bb87c223a0218e9", + "7fd468acf08605469d9e55163a417d52badba4a1ace76352aab12a26b181c5fa", + "5c864d24234c5d9bfb29eb3e885d0ca825db9cb2ad0604d7ffb6b05bc6cfa198", + "9c8ef499a12c4c84a7662cff7c2a94143bfdc91734cc6a28a8514c283e66568e", + "f7d22fef2b8af5d28182d62a69ad85806f45e20aaa4222451d2f01d2cc405e81", + "778a9f2ab504be3f95e59ee9452e63a67e8b74ee4ddba0245efb8b497049d1c7", + "a982dbbcbaf55cd75e79fba3a6c0b69e0093ccdd393fba71119bf885811403bf", + "c41ff2ba83cfeaa4e1bf8647dbe7de252bc3ad3dd5c6f8a78885cdfc2d60a90c", + "7866d39e04170b55c26e35e27a7867eb3239e56c8d0e5ae83ed2f650d9d8ff4e", + "497340ef242967aac27bbae55aeda2ec33dd275c1aa390448f2f8bfc657d533b", + "2f879132bd0392f3d8989b5fdaaed5b296132e4652d2888c3014da2ed4c0017f", + "d93259b9216505db16c9d17f873439468ce626ae242d52aa3f7145bc91363a41", + "afd58333283ccf35f71074e6f047371d84dcefe7fffc518f182dff60888ebd07", + "f28f6a499901b522d456c8889edd39840c6cc1d61aa986b6c363cc0cde8ba304", + "fb5cfae9f64609156c01e3ca894d4b17a910a18eaa7dd7ccd9bb3ae03336c82a", + "ab6a33cde17bc41efe0a00f4da139fc5eacdaca91a83d764cb69b2491f2ca4cf", + "4a12033c3f38d666899e63a10d1ef28118d8db88ff7697df3a12be98ba5b565c", + "6f398eb0171e45591d82d45638bdcac6c56e4d80aff28ad1ccd1e7100282196a", + "b0f6659d628347451366b4b77376211b77526065065a672d9b799fbc2d349b50", + "5b925c088c2637f274e540f2cb0164b27a0a8d611a385d43ea5396045a87cc8d", + "02284f7e9c8a5b7414cf83ecc51184b2ce19014914b22cd1ca2b1a8cdc6353af", + "7afebea74966f0f193d74d96c049fb060fae2b3b0ed135ee0de6a31c2b6252fe", + "81ade42eaccea03bbf1526565c2f560f79c34df5aa6adafee41f9a2003d0ebff", + "672f13eeb41550602a2652e50501493656c9a7238eba6522f51dc5eb06a08958", + "83caf3aead8719c7d05700185a2560d1deb549dadc529af6c799db007aa45b0e", + "bb3cf582e18cee7b44fd52e4e92d9c98a0f4a8e068856e923e9f596ecc016326", + "b13f65707320226bba23e07effe4f37d11316ff7281629429c7621082f74866f", + "32aa7a9f30c3ea02e1269eaf7bff197200f4a65ecac337c329c7977447d1ee79", + "118e0f05cb8b20c554526add459b213bd852075d586daa694af781cac207bf71", + "8d3d4f4e84dffe3b8dd0392abe248885e9ec5c7f13e666eeb17cacb3bbb1927c", + "7b9c01fed142932dab91584c5344ee51ac111f91467bbfce4b928b6c6e940e80", + "9f3a195c561e1fcf3597a7d01d9d08ce068d7dd3923a73707bb5b0c60b1aa274", + "9ed6f3821ed35744cd0309f2b980b7ef34bf790bd939d4fffac4db66b58502ba", + "565798d5407f54826a27b837fbdb9c8dbe341f88f868cf46dd18488704c4f29f", + "5f2a139a3494c3e5ffdb541b05996f118b7c6d3a97638c445de973e84f1351a6", + "2d78f27893334c46310bec747a50b267765d519852981280d1638822879b34b9", + "b134ac869b3181ba41cc01872dfe819bb9edb4213213a0e8c5bd5e042b50ecd5", + "95194e0c28c3c6b1e0ceeca11c7bc5667470fa56228cc6822b3b7dae5fa85808", + "3cfd4d9080b444e40d8512881a3138ef2a15040a6a246e7bef81d49cff5e756e", + "4c2f98e9579683d82f1a6c03f2b24882deb912cb4115fdfb46e8544dcc00a5a3", + "c1f5ee5ef284e2e30eec723b0c9d33425b304dc771f2f842ed6a2af48f38c77d", + "291c7f73acc537870c5f6da593f4b3e26a940bd9632f9c1ab136e585aa896c7c", + "8123840ce8ea151558bbf4c3bd282a29eb3807beac37909f231d98992016cd9f", + "0aa071d8eb8eac00eb2e036bb9f865bb084023434adb317bd83102784ba7f113", + "3d3a27470eeb3cdecff6b54bf385dabe2ad1709e52f6a4a6838729120f7f4a1d", + "9dd12f55714a9baa7182f31f8725f670243a6341ded9f235bcf684bf9d5710e8", + "1cfb075ba6d5e9c4411d2d2aeac19affa594f550503d64465a6e5e2999dc5d35", + "0e88cce56825b36c1ccad3d78df1ddcad70f7c0aa8e8fdd2d5f88743a416fdb7", + "bb942286afe921a3ca161bf63bde20588e9a06d1149d9018e2de25b06e10e900", + "286541a154b0682d1e89632cfa92f90721e1331d0bd9c5e40634e1050615f3cf", + "642cdb77cb921da81c48e2a7145127a26911e1f6d3d655f862704c03f567a965", + "69a7003155aaaf1044af7de711b88fda36c0204c059b0357312d24ec230291e0", + "5f48c579df998e20672f415497d00647f586f6331e0fc28dd19f2d9fb77ff466", + "3b4a68ac09e9f5bda507f0a36d5f7c5dc7d8d63782206ec1974869dd918ae90b", + "88b38dd7be49facaf72e0a4ab5eff02dc0040a7b3119a04b27eabeda73fd5db9", + "28283b7688de69a09a131c0a4dd5f9f84838b3e3416498408f540fcf4ae5c525", + "8970bef6ed34d0110bd2ab2c70f6de62a76c5d38024af31f34ee6b3d2505f9f9", + "a8f71cd868557eb0ed08a20c1ad6bc6ce540a94328b95c1409d54eb1de8b3aad", + "0013dca6e532a94037d643b4df87789eb9b8b95419ba967da28b5eb9b76310ea", + "cc5b5f6178de88569ebba04ad0cecef0d577cbe206e97a1a16ab5585fcbfaf1b", + "83c3c76955369ed7cdf336b1cd69fdd2784eb76bbccb51af050f268335109a1f", + "c0538d3bffd3c5951faa7d03000516892a45d4d4d8a10049b6322c2f0026bfb5", + "5f05e139e947162e31d623988ff6d33a27917fde9d262eae2c470371ae11bb4d", + "736c6bb8623c6d0af46f149a7ca65d2f142e473448adfccde6ea1c89e3cd0708", + "8cabea79b7c42894fb1d11fb0742a0b7546d8f505489808e9de4702303b5139b", + "a7f15e171af02c43b2675e4f60dd2eabd0b1d43f6efa22824c6a31acb5780301", + "ed9c2e44fbfb32b702f67d12892824dc8ce30819284e014a99b92f6581a17abb", + "a0b1072772b984cfa7132021a47e6f9dc8fe467cfdc1ad0430818467c97b722a", + "326df4221f536d04b3ef5d26c671efe10a8122c553cf9bbcf403d0e14efb9e06", + "bef47cce97864fafd150138f28bb9a257cf3ede9576f3e4d09bdbb3bf82be92a", + "0aa9a008fa3423e45f98dfe26a861290db04f0ecf515f146ac161ae3c8643e32", + "6d92154c50b28194d93b7fa2b29d7c11ddf8523897c7984afda49b3f9f00fa65", + "f12edcf461f344e8b3b31e3a20fadf80b479d3931db40ee89108f6ff83f939d6", + "035ab42a083d60994d7cd6afd12dc0cf5cd0dc02d8cd8ffbc09822eadcd18fb2", + "c7da41ebd455a93cc0cad98705e5d903720f64432a6aa43cb6c38eba59d8d840", + "c2620ddfa6cea2a116a2d1f0f67f7c1bd719938bab7fe74232102d30b43d9ef5", + "0411d2b5f4b8204705c85f89652bd9185e01bca4e0af4264092a47671ebd8d3f", + "64ad55912f55e6ddbad138dc0517d19385d9ba452db2677e7e78a3ada5715ef1", + "bf66905bb26fd8d51db173e498f15dc0c32a325d808bc00f682b9cdf7260510a", + "5e4da2eec5ee09e861d5c17c082ed0721f0e2e4e86fb73374d1b8b2acc2e3510", + "ed652276242195c56b190cfdfd087e00592292a9c690c5eb3254e2b9b50d02bb", + "f8c2948ad7132f794f4d50347d879813392930ac34e09ef8823b41e04dd136ec", + "90c1bdeb1828ac89de291792a694a9a2d296f6bb9e838058c52b47720aab6710", + "47e9b6afbe31e46cdd15f7e2a3d4c1ec35d4aed63ac08d9f1595839e2055e25c", + "12334afeb857a1c51a68af446b147b396e47cd2a203b3bf67cb2f956243656af", + "c10001f8fe72b2790b200f2d74e06b1ab2c994a072784795332721bda6eb66c7", + "ac669484ee3f4e3ee5a55379f0224c62bb8d8e50dd71a7c17006b30903554a61", + "3d87995871921153f35e42e7a02ea4a97a1a9226fc6c5d243eb1d1cbdf4ed5d7", + "3447647574c88f6dd9720d581b250ea270f257df828233afab4c116f415933cb", + "e9617e431bacfd69d0c774777bddfce0f5c67edb633d4ae27489fca9d4cc207e", + "2a517ae4365350890e708a043cd198d05f771a254e3a91098bddbb07f953f8fd", + "49d47cee16ce83626adb11d28a5fd158db7792f867e2c3ca9d21dad5ea2146a8", + "a8f66de1406e1647ef312cf6f1be5a90bcea29ceb6f66241688333f7dccf8bf8", + "08d4e748b95e605d4f3c1e25ad87621cc977c24059d951115ca3aaea5e3fd1e5", + "76414091eaccf2b22e882701b9946191f89e292f9ce806cc89b799331b47e253", + "60f9102dd3c285aae8bd8e1ad2ce1e2a4554d9c9c9943565a9195035489188b4", + "462f614fbb1322463244c3a786fd8b626194b395526a7524f0d79bbda30e168c", + "619b2c7545ec98ab6fe5696d21a3f401ae91571c594fd1b0136a6969301911fc", + "8870250d228c58b8fda02dc0fbddfa2094ddefd4074a18ee0009415d2700971c", + "5452ae146ccf8d7016089373db4e1beec77c2bb433a65ca5231faf844ea7ccee", + "86038ebf395226bff29c6d575567fb1a7390c9085801e9d0600514140bbadc04", + "903848180a704f19da88ae8cffe5bbb9c564dfa812f556b11257e2d7a43d4a92", + "e8f2aaa165583d4e1f381a22c6ef013e691fd594a32a56ca992c47ca9e1c9382", + "41d769a82a0b52d7a2ef4989be4da4ab56e8e9499aab69371a6837630c1d90ed", + "b184719b02f925b0442a87c737b91237545da1c3d7742948b67eed7b10e25769", + "febeb1e95fb64fd70448599096dbb53b428ff9eeaca7960d805d6145495538e0", + "428796729420f9150e48709f97b651b1070c20933ea4db8cc9c06f79c1b47416", + "555785f1de6562d4374f3c8063ebb4d0978c3af27afce4a6c54d6347039c42c4", + "8e7a9f8752f447385f6940b1e3564399acbc4449f38819de15c34b6822ed89f2", + "564448195304e04245491e961b46e9852bc69858872e59329b4206f0e8355ac5", + "00abbbaee89a634a38e3057b867dd02464efd539b977a9713480af673bb79b51", + "832c64d81afcf5e2d7d3211f2872e038c1151894f3f2e1573e51b2755e04b9a4", + "1470efa706e9868179c5faea65bf3ac83efa5e07a8648db619918951846f6c05", + "393c46d37cb4099e3153f819804e05cad42380a13e4d99812bd3bfa7a4056217", + "69f5a9ba5421ae9cdc00b84c0ea0586d82220c32583526da0416d2eb4b56dab5", + "92dbc472c4f59b59121658e38af3a86cbed76e047a968e6b4345c929fd1d3f36", + "0b31b552e70c9d1d07ebdc462a021c44e9abdf1592ec3d12761461306919e87b", + "ad0609aa9578cca27607d89265a2ceb029476b1e687b3438ca84b572ba4ddad0", + "8424795ec0b81c678d72bc4619b185a1817085932a2e3005f80a1afc99e1f146", + "a90923ac6fbae0bebd41059ac6e93d75496e8d2e44f49d5de845879ae7e7e875", + "0e5cd3e467447a8780c76336bd6a7fd88de547cd6435f22e310bb8515e88cdeb", + "f8850e72421c55dff070421bc24403e1a191a48a78e38e222a8f9b858aa883cf", + "e51b3ab77eff0ac139046ca19aa031ee41ba1f77f57bb7fd7823ae67ddd4ff90", + "fb443afd50cec980911f68dd45fd2bc0ea5bd56d52ed98067d6ff7cbdce476b4", + "4e87304e45206e405388307f6edb1147a90f3ede1dcc177a258ff784c9176b6b", + "75f6903800c7e9fdf92202aacbebb3219f5707a99a1d849fefb067383f90d483", + "9a74da3f17396514b7ccc987b56c0dcb070c4c703804c31c5377479104d9b66b", + "5dcafb95a24cb3cbbf6c1693ac14a2de774e2edcb31887a59c230feca9de4b5c", + "1fc817fe3250d83dfca0f3bdd3ec9daf5bdc871775675a2470adc1275ec4fa3b", + "28c4697b1ed4a614263a28f88dede4eb8db2b54b2f1d3d6dbf8e2b249eff3814", + "027c84744f285be955af0c7ecff55498c7394f0152b36b19b0d8d1304778b8b6", + "2751c8b1e69b9b6e6e3eeb2fdcc6aa0e59f49b4b84ba2483e60469be532821ae", + "39e63072d07a68479febb071c9440d724823d47aafa068a64344445a0a656076", + "40ef3e3946c25a3113e7dc199a9fe28b54e5780269cad319db4a7001007aac06", + "8a191bc606119159a6f8a34b34d8aaa6f81da82553eb004e22429c148e1e12a5", + "957c5f2f011544da28e7d117310cc3d6029eb52c27623a33e8a362637021978e", + "64936015ec6d979f0784a1e58c5d2e356e097b77197b317600218679449193c7", + "c9f633c6dc87c21400f1c7828b02f5b4771592e46632bf74f4beee5ea08ee1df", + "2e6d4dd7836c95363e48e982669841428bf3187bb2b567e0a06c10f7abdb2124", + "3496c483f32903037a6d7ec14e7d2997e0a688edc518810b9e56a6b8e75a5170", + "b252663ba155394ad1c0c0ec31e626a0782b1b636701c865dd5d52b28d005271", + "430ae8ac7d102188966e71b1c753acf056496e0330a60d4cbd97c4cc3e05f867" + ] +} \ No newline at end of file diff --git a/testdata/dind/051_randomized_convergent_seed0002.eintlog b/testdata/dind/051_randomized_convergent_seed0002.eintlog new file mode 100644 index 00000000..eecd1b9e Binary files /dev/null and b/testdata/dind/051_randomized_convergent_seed0002.eintlog differ diff --git a/testdata/dind/051_randomized_convergent_seed0002.hashes.json b/testdata/dind/051_randomized_convergent_seed0002.hashes.json new file mode 100644 index 00000000..306fe459 --- /dev/null +++ b/testdata/dind/051_randomized_convergent_seed0002.hashes.json @@ -0,0 +1,209 @@ +{ + "elog_version": 1, + "schema_hash_hex": "0427e9fd236e92dfc8c0765f7bd429fc233bfbfab3cb67c9d03b22a0f31e7f8a", + "hash_domain": "DIND_STATE_HASH_V1", + "hash_alg": "BLAKE3", + "hashes_hex": [ + "e0c2acac7f7eca867f8575a1861460425976de2fb399146b694de06507a7f890", + "95d7dfe392752bc8d94fcbb6e2918f17eb3046b2942a6f2d6e49876e3e982e54", + "feb13b9c1086a3581caa0e0672288cf8ceeed50288a304d2f13317f1b6cfdb83", + "b90458e8a1515dedc6ca680e3356b704fddca3653cb2b5c4c9f858061da2bfda", + "0b64dde9e89ff21dbfcbb048bf348f10f8170b056275359dbd83c65e994bed9d", + "3abd0620e487b319839a4043c4fa49d8bdab5d061bcd50bf987594779b9ea76e", + "5b71ed936f4e7bea1e18807d314e15551463b721edfc52eb87971221da268e68", + "c6863026893b42c7e71b3621351d21f7752e2df560a2270cf20fada03bd79ac5", + "6e89f24a288183f0b303266ffea54e9f98c91a725f1b98c86897c19ffebcdee6", + "6035947dc1a4f21c973a7be28861fcaf8e24b2aaa69442f1e8f56c990cdda170", + "141e190cd1d16a3f979f3ad463a748db7ac2b102076d765ec86d9c7c176d2498", + "30b0ef9629ab12d4ff7906ae8b273243fc9b53a67a00caa87fcd69046c6a8e5d", + "5ae22f905442e149839cc1c09c9ba99b8c0f74223e9e79b327de9f361397539b", + "4809dbe3616e15af545b6329eb5a0542e06d1e64cce5436309a38830c91e4be9", + "92ac6f24611fdc0b02a74308bbb588c487bbbe531970ad48f7cc9fc6bfda5244", + "3dadc9317850f39ebf74944b18f97f5d2e6afd7f43ec599fb87c1bce5ea63f97", + "c196c56ac92bbbc7c5781af3cba9492006af3142b8e9a7aabffc0e2a41d7bb35", + "c6ec7b9ff3f396166332166ba7b948b059d7e2731ab9688f4c9541c9204b0e97", + "e2031c78646ed0005023fe1910105d416a96182db0c6ab6630c1d76113cf0c8e", + "ba98be8c9c6b0ed8792df6b42b9846428568a462791dd9e55c17053f2b926d9f", + "ff93d67a8d823c47db440cb3c0a6bd0840240e57bad9bbff238834f3b27437de", + "840fb1e9e361899f85a8313181313c8cc998b2cd88959491bf95f20937fdb42c", + "1dc5811fd1b23cc242d34b9b841e43a98c4aec6edc80a7702337486ce7ae6b28", + "2a94fbcd944d1fa1bf23101b929bf3a2e3a77e98dc282088a958d635c35ceced", + "842f7d713223ed9c7d1c7d8c11ea667114526673d208ce13ef21759e7323c8be", + "5f9170baa52173139e9a101746716dc417c6cf0082880d94f06583b3bbef54d2", + "f5342f78842e4805c0dcecc8cc06bfeb8f2007215bd378349bd7c97db3e95bff", + "658fddad7e598b69b80946263adf0c75316633f5ab8480bc283a8df6cf423535", + "4c689e919751ae80284e5f463e03f420d64ed56b1407bcdbb51dafc7f5020bce", + "8755b0e48f42624443454677c1fd5952b778ee5ecc64bc394b21cb06bd933f04", + "a5e0686c053a08c17fc532e98320bb01ca82b1bbd86a3c568f76df0f96087372", + "bbd835d1c2b87bb0914a7bdc809154e962f3c4442cc0cf08d337d723d8740c96", + "dc3bc5dbc1528eecbbeb364cf8327d18b3dd05a0e5477caa31aaa5f51408f8d9", + "d545cdcdaf2ca4437a2e85f2850374e22a7686091007f02fb5189c85676d9258", + "19e822e14a66941d425cb81f8550045c599d1727286004cd1a3069875cba44dc", + "ca9ed0296f5baec4fdfd6012713c05baf0d6d40b0bd76882ab3a6e7c56287642", + "4dd9862a90beca0c3a914a5273a8bf7a38bedec73c4db4c6ca9846c490f6a946", + "cb2b6ae962bfcd283e79235be005e3a71d86647f00416e743e1f3f5e34bc345d", + "14bf77c25ebeb93d6769ec0d0eda658a15654a201b608ac6742c54596c1a8f51", + "d25ad6e95252acb09cc7d5591f50d1a1d72bf9a8ca8fa822c9a1918b84c38f86", + "8dc86185f99f95435dcfc728ff3e1d5703fec46c2c48c12f2be32b2dddf8465a", + "506c678e72fff996aaf03ef51265a56b725d1a03fc3b5c60f32a21e072307421", + "6bfcfdfdc07d5409cdb1852f4ef8c554509b88cb318fc6cb0c6bee22a4d5ee6b", + "965b8f71ff119637ff8ccf4b899961866cbec3ceaccca5c2578b16d8adb24d40", + "65cdf6044b11c73647c8f224e89ce68c769dbf41675b9cb8cb6cf7e60511de58", + "d7635dc9f3cc13605d3e83e574cc35d874b6279669d01069254872b578052649", + "0128290ffabef22b954054c71e3973a0468fac5156dd8e5788327cf1fa3e0989", + "6cf83975e352deb6f0a35648757eaa843365bae03273a9311e7175564a1dca5d", + "98c30b7429dde18b6e87f0253e03a7f9d403cbf898c26f62f9bcf86760d2e6e6", + "7160fb43ed1fdb2044baf5de11ec9c7d46565a919adef7ee22aba4a3e9fe1261", + "2c81baf4ac91bc92c68891233bc299b919b48f67125ebcf041c0ea6ee891e29a", + "5ff43420585fc9fb4b26cc65aba9cbb8630a874f53c2f4a8651b67a520c39f96", + "04efa4bc81f55d1daaac4bbb181c5c6a878839c32eaecf95fe1e2543adfc9b8a", + "bd42a5f6a20132066c17031dfa8a8f6f70621f809bfdb229a443c8925f4e05a3", + "06a41923a30c36048e25a5866b9a67a4c0187ea942ba724207fb6ab17e786dbd", + "f0cadeff4ab51cd5c162243232182ebbcce7306d4fd724a4c2a162b2e4ae862f", + "fa2cbb8993a292ec044afe3f2657cad0552b14b73f7c326b5aafb362fae4cfff", + "3dd7a53e30cd0161a7ee8535ea40d766957e401a713106719dd7a5dbaac3f35d", + "e8040363a3a99fbd0735e8a24486738f446224b613e622eb109c9733a79281fc", + "34ecd339943846ceac58f493e104c68d3a13903ed2d4ebadf5c631da5dfcacea", + "10a12a23ca3c8b5c15ddb1cfd0ae09626e63d00ef8542c425bc76fbf1e41ea78", + "99dd622bc5494880da03f17d17a6923719a3494997ba2b2cbbe2d6145dd0c643", + "5d765cb98ac62899d64710ab76a3d9c3b350921b1dfa36559c2af42236dc7be9", + "071b144072baad89fb8909e221adaa8615dd170e36e045fd24cfc5866dbaf08b", + "96e59c590317538c184f6767e6daed6f9de087941b269c68cc6ebd40f5c83850", + "e341fbbda5eb70abc15b46eede0e7df96350005919161c70d97969f2c6648410", + "73f4549450f6b5e002c434be158f4445471d992ebe94ec795e2859914455f579", + "89fcabde6363e8cb9343273b28a34e6f62ec3f7491d917f35e6d42761f0dbe2e", + "7aac88ffb95668ac5824347b02dbe0a90fe51d002551c5599be20a537f43b4ec", + "d0253f3e7667095d1fd836e592e94b65a6b9bfc4bbc79be55e144c6b4fafa7b7", + "87b9b68cab9b2b9e046f98907013da077a44975f2426ba8c2aac5073cd374279", + "ff5f5a8ae543e26e73fe8a5f805ba690ccb05e259fc289aafbe1dd473ae49029", + "9f9e3b64df9c3b04117af3e62e32f111a13c90723823b92114a4ed3fba50cbae", + "c2b9bd6065b050ec089cea4f9c334077be0add5f8c446fd14d7b1a96497c31ed", + "edc53b5311832f90e118402bff02327476bd564cb6d33065b12d88a4f52ea8fd", + "c4482b4bb44bac252517d46f33416cd55f061ac0b4e6def08fa1387ae316466c", + "a73c15cd4addf13eb8499198954545605e409274ef482c958e5438189d46e405", + "b08e8178d454b01db5647f4b19fe754e9b4a96743ff20d2fb8b7178f83a62e5f", + "9b63a19c64de5ce0aadcf7047e5ee037b23eed85ebc7e76bc638015a086ead17", + "7b3ab209a242872edf210dda57235373dcfedd586ed82a689285c01087b72aaf", + "5c1a1698b3c53606a739d808f0623278f805c4cd8d7f7387332690f521cc272c", + "abc4aec3ca4bed5588804b5766d71525c20b9926a43079e087ee9f00f1894e5d", + "3e2a2a8bc9185a1fd114ad1c837190e7b4b7151464ce0b38d18d14098cd5ff34", + "b5214f141a7a0346959be629bce90afac38da8e49b73dd539c3bfe69f8942a33", + "6c9cd851f31820255dd4419d49ffca3478b8270cc85b7b53eb8268abbedef8f5", + "2931c16e7b8031b98b69a81a8d69de65df1c937fe575a2fbd87d5323aacd762d", + "888fdde9274bac44bef26bd3780e9989323b41520f5fc10e51f167b7c012428f", + "d4248cde827775b354068d8535db865e4ccb5e97c17b3853bfcdeb7b25b4d6b4", + "3a06105d72a0064b237543226c443102dd9185cbf69e10b62c0c1d81f61ca87b", + "dd19c006e60350f1e104a8e9b60cb99a42c41e16e88e98b0ff0a5523d85d6f3f", + "bff1aaa5f2ed54ad6979b686e86a0e589d951cb1123f42f279d67b0b2173a1a1", + "457b519b293e48e39d04e8f6222cd28dbab7b39b77cccfba079fb14a69595ac1", + "8bcb9bd12aa1fdd8160f1dad675d48ba9660e204e9f886c5cc425d9afc96cf0e", + "a144bedbad3df66fdb74547833c06f90389bdca2533c6dcc716b920062683eb4", + "b9cb18925c85e23713c8747a25eb09576543d8d3b4d933f7b004db0a70ba3aa2", + "cfbb8273dd9cafdd8dd2402091afb0da5fd715c80e9996c6d399b70a1528e161", + "01e466b703670d46358e98e1bfbb6b4736b7739bc870eedce349463fd80b0f31", + "4378e11831bd9d7652c7862e6dcb5668168d93a73023bc616c673102d6cf93d1", + "e56a8c4ddd9bb6349e236611d0af28aca186d3c4fca6320bfbbc65e4d34b4348", + "3a593bee750707f55dd2955bdb6e9fa18c7b3357b1698808aa4a94991750e977", + "2b72c8de9a385f6230b0a65576711b733c300bf3578e64bb84af917821b04e93", + "eeadb33741f644a6b68ec32e0f2d89a372eb5ccef904eea39e0dd9b40b257f83", + "e322c83417d01ab8e26b6cf137071024bf6c128153cf6b664fd93629aa9d97cc", + "26f8ca54e9687ee113aa830830722398df0a71d1a005db9ab0c9f390fe80487b", + "f2efd3067be3c9e5c66dc3ef9bbdb91ba90b5aa6a109c4a8f376420297edd4f6", + "bd6b94753dfd127bca982b70bdb833c40975a684d7adbed9e1f4444d5086ae9f", + "2dd3e05a2ec43f6ab3334b8e4885d6e3bfb9b48b02e3254b23919a8dbb38cfe5", + "2cf63f2c679e40085a0edc39ffcbdc66dda2f77772dfda4eb75bf280f34db580", + "a16671490f8929497ec1fc678a62279796b6885703082f30d59d69f9b55bdbac", + "b819d362972556b537c260b66662086a17f3567b5a651665591b14e15ece66b7", + "5f904103a5a3508564aa58298c65d2f73ab91f8f3abbe8c1e4ab17b615978da0", + "a44038313480eb138cce1f598964a17934472e4c2b25638005fb2ad533ca1058", + "b550674df56910b656ee2b07a9f15f6cd561ffc178b0b6f5e5c0f545d8f86343", + "cc79e3cdf6adc8b95e68c0b58704b225178dafeaf06ecc6b2be2b60ff03c61cd", + "c4783a52c95dc05cb90bb71dfeb93754c6e52de9d644091d30b0a506704bece7", + "5a4a60c83b8a6a9cf0ad252a5bc4cedd1218e52e196961a47cb05bb5b3aea76a", + "abbdd75f239862d4285ac9a1df8f32ebebc993c4a2d7bcb1cd645992aad40830", + "4a5663ee89f2a1933ebeb751269c6bb77c610ae76968c47f8a62d6b4fa71a4bc", + "886c478781089d9a0925c9f6b80b483c3f76b68741cfbc41305bb151665427e3", + "f5385e84d2154ab31484314e86c9dc675fcbc8819772481d732e7fb8fdc89ff8", + "12780fc637995aea1f335ce95ccee5a3e832a16a0858339d311de0d8c65c059c", + "3df1f46fae1f0715d6de5858c18e35d830e05a9ecc633d1e9a4ccd50ff43a185", + "84d2e6256734ba2691deedb7030cf787ddd09b039cfe0f9079b831e414d96505", + "5a3588b20c31ec2d6e7391f839c6e86d20af1a8cbc4c8261ca26144327a2194f", + "30af927ed9c77992e0afa9e47979aefbf487833d128a70c65856cf02a8c8cca8", + "d61987c206fd6f702aa916c78669667c8bb4428f3903bbccccb18b568691af53", + "4d5edf65162bf685c224e7b53a17bb1025e1629eb7d1ab12e7dc2a84073f76cf", + "f827206125ae5fb9c61ccba018e9cd53bbee8d820925a2ebdade7f575e879dce", + "f28c9a78210d76d4a6779c6b50d2fa920174ee8727f5fec05827c41195597051", + "c0f911813a227f1746277e0f2d9bab70da175916abda545a5bc98e6f48029230", + "6b8752e651870def2d088ec2f8c8d550dde4b1bcbea03dcb7663705bb9b613a2", + "a097183561acbd4410888dc1dcf8993fcdfe6af97df128e87e0d2501f8050f79", + "07b8b6eee5ac04df6a183e4cc326fb9c8bc381b929d5141f89be97a2a3d5335f", + "dc0a93b5c9559c1b537af56bd750ab19afca20e9d04158085f9de9a90d71c84a", + "931e574416ee7e585ddd304ec629e66b3bf868f44780ca65ee5da92253fc1727", + "6945a5e2dd65cae25f812dac4ef03292b87a3fa00bee71e03a4fc34bb47b8553", + "106d53767377a5eafea4f733010e9f10d534cc954a0ec16c49596dcf8c7e13bd", + "a6a6f21cc907f88cbd26a92d4b68108e6e64ddf7650d94caf993cf55f98e52bd", + "5136c32a6a3df6a92d04ec43a65129f42cdd6db1307774a0592f917566a63ae5", + "9e25d2f4b7d385dd202856f8c910bb400ca02470c3497ce66dcd7e354fc97ce0", + "b2039bc239cac79cade998f5a64e078511cf1bdd17aa5275860069b6a9085b74", + "2d4813c09d6f61de1094befe858996286d6d2bcef6cd96924b255773c9445d5d", + "1755c3f86cb5461d302c58795ab716ed9e5c0f833fb3491ccd9ab689bd17407f", + "b86eebb174848c1561a661143e2f7c295f0ff76329cbb7518e2f93bf9396933b", + "f2629a858a845927d0dd3326b418b863fb5ba192753c3fb56b22325ba6c2bd1c", + "83999b6f8ee28493291033673e2bf9fd902730fa19de750ec410cd846a582343", + "005c0b30a8d5823a730c28ed35432425a62c26a0281b36a61c8639bfdfe9688f", + "7b440ccf37672ce697c64cef3bd0eb65f3dc23936808d673f706e096613f80b2", + "87ee7490968b676968333aa0667df9233fc5e705b00d532602817189d05b6d77", + "a82c14f4ae1a6e304290600ac9afe1b8c6136d6322744a2f9873d5788a0f1922", + "22ce659c91138135eca4cbedb8b7f7a53f01d9a5c0a2a5d4246c2a28d8175bc4", + "f227996b9224350f07cd0c0e2192704aae1521bc1c3cd72d80702e2411d0a528", + "2135b9268c680c7e0ca9a859ff98ae761c57c401a715e7c241f76adbfc3ef43e", + "7b962ecd5943c001b06963c78dc700753bbc8afabef4dca3b29466742ce3efa9", + "6e2647b9677eb3787f5553aaec6b21458d8c0b464a5343ef21e551e555744daa", + "dd64dbceb2513980178bbf3984574352f69c438079d2a1646c39b47f02ed09ea", + "01a1e2414582634e89d019286e08819f913f01bdeee6d913469839a2016d8a1d", + "e24f1b7db56722acd75a1292047654fbab81c6c7cb57b8dfac773e61768332de", + "0d10e58b6aa71e82f88dd5cbf78a063fc5e3268d03eda5b05c4e2bfbfa010810", + "172f9548faae522128c69f48bcacd6cad0532a6360b27b8bb0634a5a9b9e4a7d", + "73576609eba0fb905326e1d991f67779e6e528b4fb5c90c06a4c6ad300b65ca9", + "fc720e7d265053919fa03dacce75c3915168fb21012a55fcd4aab1385fb26a1b", + "22e52635d81fafa927923ba3a717c16a7cc21b82a383ef49d9fb5e658d732420", + "2832942c4bf47841f33f34b8a2dc4f6a0293734669f7fdd02f07b5dd6e7a145a", + "704d19ce99929ac304b1305a0f3994ee28e761df9e57972865e7b31ddbc3aa70", + "cbebcfd1df6c4b2f6c737d7b22d66607f345ba01a3d1f0599a4e284b12faee6d", + "b93408079f6dd9b394bb2b5ae6e19fa05431e116bb73767288570eb6e91d45ef", + "37ff9b663ec40661af68e99979f02ced9ef36036261258201418828ee6eb48b4", + "5e56c5e5ad68438a670c46e18105df98bb3ec013a5b91a08223b29bc9d88b26a", + "3d6638020197a09f273a6136480a80836a69ac14cd26b2512f3ce15ceebe4a26", + "05f446c71654a07016dd3ec7275ceaa2b9c81f02f28fbced14813c5db9536a1a", + "6cbb61db17bd146088baa27268d800435858451890f2b66cc2af08876f3ec26a", + "9b195db4512538e8626c756d2a9bbc5991c0c7d596a9418dfc6d399ac43fed5f", + "389e7702d94049ff688dbc9f0c7bfea3a27d6d2338494fecd72c1737ebba8724", + "e496c51d32f5aa040cf1a6ba6ccd966cddfc95b5afe4359fd50abfc7a5b56204", + "a967aa478ab8126241ff582ebfba80b29d228945e8b031f0ea7a6c7dfe4197a1", + "5baf2706fad6c8660014d10af8da109f584bf781c5df5e81b86d4351b92a6c69", + "8b251f730e23728a1434fec8bb8b3f8c567e3ae96cd9097201f233906f6b6e28", + "c76cce56e7b11a4e9d33a6b7facf1a5d7830673720f37c65ee9079a1658939a5", + "328a9b08b90e33897fee5e09cc34f5bd68e4af5a6ebf08e5d626b345102baa25", + "0f1f8e0424ef70968f9bdc82cb6345a0ddb10f106edc41e9bc19d9c5e452fcdd", + "765fd4d85ec0431b762ec0e3a2393118681e77949e916d9aac081149f02d8856", + "fbf4da26ddaa4e63300335268806e4caa28ab2f49b33a12ded5f24eac776f2ff", + "81d4310651b042265f1e737acecc55b322e3dd41d194e8d73009ebcaf18beacd", + "8c4c217e04a5344b7f71b0d3a3ff4007f4f4f63bbc34c8ce7f258114bb851f88", + "63073954daba65f3dfb1ba62076c123e3f30aacbf337d9628b4bdaaaf32f4980", + "2db4752198b06285d160054e6359f40bdb8153a8b7acaec3c620176ddf446a0e", + "311a2e57841aa9a304900f884fbe647c0a7a28310a97e14b6b151cd2abd58fe5", + "1e430c6e38840671285cf28d3ed99bd2b26678854757b5c24c27e7aefe7192e8", + "00434cd86565939f8c84e2d4f230a64f05c056cac4b5cecbbac6bb966ef065e8", + "19a654d70e60c16f5e5df3545262893d830721e283844770cc906694e32cb286", + "e92ee247349ed39abfb99908141b13e3cd58024450395e555ea05ddb9477c8bf", + "25ba706b26e9ba53321d822a96b8f4c96726dc0c26ea0dc26bfbabb8764c5c27", + "7a1814a68e5fb8b210a2352593d873cd4bc3fd20423318c2c0f7d581646b7b5b", + "315c1a59f85a27540874322f65017602299a3431c96eac20ae8cdd8d7ea30ee6", + "e251a82c6f0fa1ca6bb51619aeb8bde5661d9eab757b5c9570f2c1f242cd68b5", + "cd2ec8ca9b8103d16a3c614be78ab32599e7950e22b6f8258ef70bd3be1592df", + "27a256e85cac3d10e92989f876d2a447e0a48def8ca6a79db592b813691fd692", + "f3faea167fc57dec798c60b157268092516ecb9ef074a039783e2a6f89da40c7", + "f53b2a03bea8a3f98b1e0f43e1e1dee7f147188b66f3fe4cc3949b9ed6c384d5", + "430ae8ac7d102188966e71b1c753acf056496e0330a60d4cbd97c4cc3e05f867" + ] +} \ No newline at end of file diff --git a/testdata/dind/051_randomized_convergent_seed0003.eintlog b/testdata/dind/051_randomized_convergent_seed0003.eintlog new file mode 100644 index 00000000..6d87ca9b Binary files /dev/null and b/testdata/dind/051_randomized_convergent_seed0003.eintlog differ diff --git a/testdata/dind/051_randomized_convergent_seed0003.hashes.json b/testdata/dind/051_randomized_convergent_seed0003.hashes.json new file mode 100644 index 00000000..207da946 --- /dev/null +++ b/testdata/dind/051_randomized_convergent_seed0003.hashes.json @@ -0,0 +1,209 @@ +{ + "elog_version": 1, + "schema_hash_hex": "0427e9fd236e92dfc8c0765f7bd429fc233bfbfab3cb67c9d03b22a0f31e7f8a", + "hash_domain": "DIND_STATE_HASH_V1", + "hash_alg": "BLAKE3", + "hashes_hex": [ + "e0c2acac7f7eca867f8575a1861460425976de2fb399146b694de06507a7f890", + "d38a1584c8dd43fbcdb2e09ffc2ec810ef20c607e723b2295bf14e6827d12d29", + "029ed7d75d0d0d5dfba1f7be6ad280e5eb2e4cd0002914592cfb6e3075a42e6d", + "7e21ef29562024d780b274cc89e60befeeae9b84e2c8c3cd45a46ba3e4565469", + "376bd9bc97d2080b8fe03eac6a8a104b18ab3df7f8db25f5c9f2649470999a56", + "dba328b0573d1225b9e8d0cf26be7c36ad0968fb18609cd10589e801241d60cb", + "9c91fd2f46d6bee4e50c113fb75f367dd9e79328210bbd83d8ef3c867cc8346a", + "185969dfd5a1c3c5b161f819aedc2366f5415cd2e8b4a6d881609994b23f28d1", + "cb5d02f59ea765746040a53d9b9348e684ac66266cf2eedcc5f051ab8594d232", + "963ed8b2bb59edd25bfef16a9a85b960632ed4134a96b6489ff6cb7347d14a2e", + "a6108e1f2d30c88ee89a70d1a678db57b5997bf284313af03d1207c8753477b2", + "7e1e35db2b23f3798d277120008d0e5251245da3b3c520d532f1d32631f52135", + "fe969a25b372b24228d516f3cb2b7f388dedbd4523bce10f164efe727215c525", + "cd2dd06bedc29fe350bad0f5d0b412c386912a20105a5e1c54db0d13330e5f3d", + "15b27e3778172fb2f4f865b7ad7ac85778580b07eb8a9cc7d8623725cf5796d9", + "8da84e3266e0b94571f64a306972977c8ad0848f88cead155916bdd3506ecee6", + "4c01fa8f70bd976a1814c4a3984144dcfc37dea434e0898223dd088ff5a5a2fa", + "a277bf89df5143aec082d1bc2831369009e34c7db21980afc90522be9fba2d12", + "2716e6258e9dc0721eac1d5fa9c7c6d128fc2a56f17169d58857649179d703c4", + "9f0c0049eb9dce5b17c15c70eff64ccfc07fa200c43c9749d824325076d895eb", + "fae2aad72844bfeb90688e9805279dc320e3c1c311f5008a1f744f4eb4d63bc9", + "e9a0d2fe60eef52ee73e60c0dd9cb49213e6d31f13f7e4274ad38a7ae24a5ed8", + "6b7ea6939a7d31c317f7b6b2d9ccbc773d067fb49e51c24afb96d674b1ede00f", + "c60e12806e24ad9460280355803fb39635b89be0a5b7725c2d6cad0fcdf395e5", + "d6f04c5fb7904376504b6780c3c66de9fb5b2ed74cfae76cb2789b69710e8ef0", + "620e14116e9a5e5cd547856bf88fcd41e8d591c43afc36b550fe37f6d83505ec", + "202b631dbe721e07b712ece0e3c9b17d08aa5d189f631106983425e9d20b3f39", + "b142389cfb51cdf13cd8242650a69338287bfe7c6d92facbf006d23cdacf5362", + "85fbdc3bf338a64e6e98b13c18bbe02dc0ad22de4cf6ce9afa9e707b17692d2c", + "299324b35179eb7d3b3cd0a71c48c992bb214dd87e98c25aa56e5fb422b3ee5c", + "8181674e925bd507af9bf0d7d1c5ddd6c2a617080aa22570288d03d05869a59a", + "84526c2de3416f22a73fd6def09e3faa02bf6e0ceb403265f7498c36e7823a38", + "6cbf8693190a9fafc0df691a1b0255efe9d1850bb829d4cb1b6a408910cc945e", + "fcd380093b1484e39b67f7743d655d93e8e14237201b763603722d8e21346cd8", + "13b8e0588dd6684e36f2246982c144bcb0b6fe9467207756641d80ed43e389e2", + "466a500a5913b16865fa5f042e08920fcdc881c927d11da49398fc6251d5aa78", + "27401ca821d0e69dc8145e44a0ccd785a2af5e7db5a599f9c8a47db8df37b047", + "035e9470a080903d7e6c46524d3ae804f6aae2d8cf877769278f5deeaffe19f9", + "4dc1f47e392f6c262aed255f6ea0ffb79f979aff660fae94a19812130cf70c71", + "ad8ea2eb56159c00418abc1705b1cf0ca63ec20df2a68f34a7160ea15a98c01b", + "7e5a8bc34bac02624011407328faee77e8cf67ccf605010f192873dca11f281a", + "eec4bda285951dfd9bf5fdf5ff31750a31ddd7359829c2f46963eb0568a82731", + "b3b5a05f36e3136f7ebd6397bbb6154bdf7b0384655181c50470e008f2aff668", + "38d5ab66697a9bfbb5785b5252b5b5ca8d14da4fa814c4ec5dc042cc3bd35739", + "a8e3f2d3ad2bba2265a28cfdd3b340d627d273cf6743d9c9dfdab59dba7f129d", + "57f15e3feb700bc7c1cdde779c3f8c4a53b4fffa54b632bf64652e14a21f33d0", + "47bf0e54322b32daa25b8bae7d0db5b5a02092ac1e2f528ea3c1ea33607dc05c", + "999273ad24e5e198c924b9423e3e2bb5a4d96c638e126bbfc301fb465b3407f5", + "b542042bfbbd1637dfddb3c937826a49a408b50b39f0d7d04d60f828f715de63", + "4309046c5ff9fa77ae35a64911aebab74d2d4382ceab85801ae86fd837035b7a", + "c18cdaa1add8d9b6235fc7d9336d5d620e7e64a45aaeb7c0a43f8db750154eff", + "2ca15c2233b4b74f0324b18bc8fda8093f2988937cf2616a94d0c9c43fe17e82", + "de45ada34252032e5a085d01f8bc1572d46b252f8a2c9da797927d89e390d9ff", + "717f7bcb5c7c6ae12612873ff3d10e366046d4679d3030d761eaee6ebe89b5e8", + "aa88fc7a2494bf10f7d498d4b148ead7019e015c9c2ce7f5b3157f725f397535", + "dd27ad1248847dd2248caa13cb9ca644117582fb56cf281914495a4f287bf8dd", + "f015b2d35e04c263c78e94b8d2647f0011f19c56c39bc4cf197579c0440a9d12", + "7d7157e81063e0850ad549a68568085b9d48ca6138735305059950e3bdfa2cfe", + "86dd5261547775e5b8a0668048a1737cf606cc73d8e5b23f9d98c96e1160f819", + "094db8929b1ab144191018588a5a4cd14a6aa03d9f39b5e2ea59bd00b62f20a8", + "ef3d6693d94c4d4ece30ac5d9217cbb1f3da9babf72c85e856214230553200aa", + "2d30beed7f580184d969b05b6c72ebfd74b472a5faae0de89d9408238c7b11c8", + "af204141d3e58a1a056e12c62ee00f0d55d3eca5b00c5db233bf42680695f983", + "e36c2f36b728dfe90b55c3bb9c862a09c07afc012afb9428d1c2ed6331eeee55", + "da86018e30c0c417dd697c612b0591c7cb7b644832ed2df3499d35c935e80aeb", + "598a0e39f04cbd2eabd7ada5bd5bf8c3dabfb5768b04bffa0ea5e5af0ede535d", + "e0648c529d0035e71cf74d41f1b2637337b3fc9191827bad99a65f890bd90bbf", + "d40cdf285d3f10e63787c9c8fd0f6fef5c8299007a3520df1f53e5d568c31aaa", + "9749fcffbefb0e70c32da31b67e8cf97e3878de6a375d14744a07045dda1eb06", + "a78d6489f86908dc52e050a3be8cdd485ea1cedcc16480c963ae3716b1f763d8", + "f6638c95eae78cacf6bd04ed1ab03fbc0ef898d883b009facb40fa486b670b76", + "83a4fe094544d4987ed29b2ef5ca37b22bc69f89cda4b34871db34ebf4be6843", + "db0fb6c817746af89c15fa8979e0d093fa6a7205189077ee625a59e0abca98c7", + "f94bd926ad9f2332ef1ab245fc5374994d45289def42161db79990f56d56fcb0", + "69f0bda29da4f74820758385ed4798caf006638a8f3fe19f004494c349d2ece3", + "c5e674425bb4206ea27310d8be2fe2c726194b0735cf568b0111720b82802773", + "0c7abad5011ddfabc8851c7d792de2aea4b4afdf9d9451c8e1883fb61ed5809c", + "04c435851ce218cd912a433abf9df8128efdcd3114680fd28fd74d704c619fb0", + "2227afe5bade848eaf3f5e28bd386a20279050ef4b49818ce655ac3e6d706da1", + "dc8bde0c09d657e546f84b85feba9c16e4fa0a2a056e62b95153cf6db17fc79c", + "3693804650175ecdf64bea62f2af8036ab0505d5d672351ee011ba99e6ee8197", + "dece524533cb9827ba97252163b0b828007200b481885c82c58df96837dbc816", + "545cc87ef4009ef66bc3047386f32d17dd894fb9b64ea783ca5cbd0e4b0744c0", + "922442e90031841e44c3c0dd1fa40d8af776151b78d65832091cf343c164d83b", + "bb2487531e570a3ac45f73bea79ea27f22075f2afa6b3bb622434fdc346b94b0", + "75b7daee60d08965cbcf101a2b791caf96408dba5c91878b33a0937dd1af903a", + "53e254d16602f4d12821f55634b581b38a58adeeecdf4fcd3e9322700c9dac0c", + "cbb600eed79f53e233b1819005f9919dd0f8aeb45d63548b14fa1f7aadb72f89", + "8fafcc37ed03af7a0b3619e043db3ee37571b6407bc4aa8dadb989de3c92d56a", + "112b133ef3c9c6a7ff6ba2a4c7588aba2dfcc4e8e1d9e5e6071cc595b24ba80d", + "1e95a6ee11bb2f756aee6b9cbb95a2a5f26f078ec995b93eed0afda37cbc5464", + "df4b12b394a97de9d18c7a7bef598e5e8811ea57e58e174660352f2860735d5e", + "cc5411817f49a1a9c570a6535dc7cf158b8b2e4f32ba5494a6896e9ea0e834e5", + "a8eb5634c6d029cae16cfb975c0dd770c2635d807de8014a81035558874cb51f", + "5cbbbad42083bdbdfa5a837abea35e6b1ce0a76b500fb3151d5e2748de448858", + "dc7f4e0fd28f408241594d859b79af1633bc0faeee475aaaf60d71d266690188", + "9166f85bd7e02b0eaf4ef02ef18dfae564d0ed3854a232b6504105b37e80abc2", + "1195e11173514a858e8db8010c4fcd2937a2e778c9cacf2bb6c425807241130b", + "6ad93a79d7fcde6b995dd9ca6b728d7ed8b56e03a79f033b0bf7193c0da768ee", + "806b8cac2c413f6440554383a79687915df48d372a883c3cadf408051aa82de0", + "953392b1ae5853289d7b07218fb3c1f7c82e75a8373ae88e6c698ddb4fece9d7", + "404a84bd48ff6796d13912b3818cb145e223955af0bbd3710e9705de24951006", + "e7ee7850ac24286617c24a0ba89ce0d621797ba13b16ee8916645c8019965feb", + "eb318b57dc1f8ec9ac968cbeb37efcd9131c942c65a83b9be4bdeecab66e201a", + "9fbd56dfc3fbec9755ee88e27ced7fcedfe56788c1eed8b303d3b2abf6bef90c", + "b4e6bbe6f538505f4044e0139ae7fe22c0e5dd9a63ba40a2b57bd31240029d63", + "99e431ab37c542167f43cf8a9da2279b28af9c1073c329d4079f94649e18df1b", + "914a277f599d6cb1e83337c2c201fc1a1b78f6b7d9342d4b57edf445f0ef3577", + "8f11456d51e5e66b20e975195d9621a235095fd4e7e7fb870179414216405488", + "cf0758b65210459da7a78147be8ef56ff4ffcdc60cb6c05431617d8f7889cbcf", + "7b7b90bff2453256f1c728e926d2fa742b8c9dbbc77c849bd9f80f5c48f4ee47", + "d69f15b34596abb118476bc8498b060b74795778dcbfe5f9ae87a703c16c345c", + "04d0ff2563a39c366bf32a07f5938d8a3d83cbda7cc6ad35ad6e9bd3e55ca376", + "0816d0ea84b4997d01e93449d99cbca7b8168c5e6fb59f8253ecc48a46bfec33", + "4abff782717ffe056aa51d89ef9caa83add4ad7c350409f463269ea963e95ee9", + "11402ee6ef2dff06152b4c14f978cb3d5079634b36191b12db6dc01491015ad7", + "1fa7ae26c1fef141ed4f2f6dcb6161240c693e2f262d29f411221d70f3a30a67", + "67b9f5c002865eef301e871ddc524e03d7afddd839293f1cefe93b7320adf3cb", + "533fd4ad7afdb23727e55537e4897f59e7d4b2c4fbb4d80a8e63da81808f1e96", + "09abc0053ccfe72fdac81fdf3516077e17ad041ae26ecb58d2c658ff26cf98bb", + "ead96653dae13500e569743b6787ce919367776d8c01e9032ad9d289d0cd8209", + "afe5f7242800c27e8340bf8e128f63e1acca8fb46a1ac173dc52d7b78790bd10", + "0016a20a1fd1d556fe14cb0851ef4787be1e492e6a1f7a6d181f1576cc87883e", + "8b50879da44b6251154dfa01144cdcae09b398b5ccb917f01b051148956fb2c8", + "d344d2cf396a5c57d184b84df31791849c3edc0f3db25677a77fa7cc77147029", + "02abd121214ec5ed2bf5d745ae683b56bc4d0e7003ab400dfbf922ba1f91e48c", + "0c86c9cfcabfeee8bb52cf0d3ddb979f40204210f28b3b51fbd4f1879eccb590", + "8fe9f7763fcebd94a3e7ec6058d3ff2bae241e7e3d298526417d2aa488c04a12", + "8c0f370ee51b7669f2349ee9d92ce132ab329684092708dd1f42102f096f217d", + "64329788b8ea4b49d6875b520a4a7eb4458396923c9133bb7f32d266784c2fd0", + "a59aecc13ad64a9e48d1e91876b1b55886262b9e1cb21c1223ce01abc3390cc7", + "ed77faf154fd277c2643c775bfa63c6fc041c86d192b0e3f1408a8f015d88e32", + "034b4e03d3a032b25fe4e2b1fc4f552ed7d801885b226f6c61d6c0085486e6ae", + "16a41c7c27a4a8f0187ee6ffd270228d7038e5de65b6f668388ddd2b0ec492dc", + "421ebcf1bf6ab85ef58e7f985d7c92bcd5f701d7b9954dd0f74421d0bd5bc770", + "5e791ab5a646a2d50ec0c2947bec77922398468f6bb60848bc252bd72c23aadc", + "62c5903e356e3e9c8c14e4454c6ddd1b093554ba26cf03a4c19f799324fa8f5d", + "306a0e55820944e97e07ff54d93f16376b8e6932db8b331f3095c83d8e98f855", + "aaede012a0937b1898a50579f9ac074a6b879a3fa85aa7544147861130405eaf", + "fbd4fac74cb982b0fc11107871bedd2eacd00daafdf924ac1b414479b1d2201b", + "07f8ee22a9358607c10df6e9751189b93c7f7d2eeb828a090e4a6d3eb0087b7a", + "c6c9d7e8de5a0128d9170025a6fe0b993f41698a1e699e5b89600bb9c08f34af", + "9d89a1e0bae5e5a762667f855eca081a584c3ec44097316a8e58d3194365a2a9", + "6c6f39a67ccd40f8c6705dcd5cf941d4eb88029cb3e150da974d59d59bddffcd", + "96cc2028256fda35fbb17a31c13bb321d488ad417515c926fd97237112c22d19", + "393be156c29f9fc536c82a54445d4cd1dc33ccbb33297cc6b18c737ed29d4347", + "df9ae153b5bffcfb2d398279e2d5e26995d554f1ec284ba1e12a91122250fb91", + "03dc1877dcd307d3df7771a4fa6dae09d0faf5ba734875a9dbc668bf0b793e95", + "1db003dd31da78e6ae2d57545a5baef7302b6f2a06a07fb5d3aa3e4b644622c1", + "4fd0b99fd91fa58b11c5bdac9862cfdf492c33c99b294ff3b44ae0a64c48f137", + "1ef550e7c296e441c105df2cf4b1ac8005834c51fb71489c97f11a53936537e4", + "0c201be473ac54dd7a1fb054ae52edfc46fe4459a8d7fb84bcfb633aa3e86832", + "563c33a3b2a8e186c500165256909580b40c8d8c7ad962ecde012b9722790361", + "42de2fed22912258dc32751b6c75b1c1602a939a2fed509b5e86703a6780d112", + "7b27b7a49e64121e944282318633da56370b88eeb0cf5eeb01a3d7b610fdab28", + "eab637bdca449455c8c571743738faf6335b2b29978e17115fda2c2f1e204b87", + "0303c963208540cae5a7db3c519a2d132b03200b1e18b540d95cf0dbde145ac2", + "72141800b7cfe5a9b508a92eb91a0317f12463dda442c52c4bda11976a91e65f", + "ca0545623fdbad148729834a8897f4890630a1689edd874866afa875c856d8b2", + "0bbe3695398b3a05388b8f20be0493375d7261010d0516fc2c76837edee49095", + "97c541c319e6a5c4cb8148ed926e764248f8533038153c311430b923943ef525", + "0098b34304c4e1391e1cb642d4fa89893694e7d4cae14cf698679640c454b1c4", + "ee1c1759e675df5b05b728a102489cc46983ddd3c1c816894da807ca8d00e299", + "e175efdaf82a0b5db8cfcb2018772eb898b3e85942a54f480aef25ffdf869239", + "e5547f570603fef65962adac98e2c3e9eec12a45e141d319739b6651bac56315", + "d7ee4bb9909f300a0ac70c350d99603f772db4bbbccf4332614f1d24ae7bbe1f", + "704667b319f1059c7f4f8355b16780d8b4343bab35839014856edfd3a7c7270b", + "57977acb774993fb0ee0ae6ea99c8cadeb42b692426c02a8bbcbabf81e212cd9", + "02788698118202e173743e2ad02bb05668e8f99c7019b3a46178c4825effe13f", + "8906966b75d2d7c70daf465bf7e6e6ddf3cf583de41528fc86c1a867d9f6239b", + "f58c1fb1417839ab312b16eb372ab605cbeebaae988917be2111c6580c10f3d1", + "b87ebf0a459cae7e28f62419c6a8dd5537f08eadf6a68eef73beeda6daf5932f", + "bee458e2ce554a9a24e40131bb3ee750f9c1625c01b51c32efb5ed55f4173ce3", + "0f5e0cd8323772246851c4ffa285357750ee7d67e167027205ffda3e3a18017e", + "2379620e3bd15157f34906edbb606213bae9cc6b9e27318ec798c81d308e70c8", + "bb17fe2e6e3a19913045b42286a86a2ce45aeece50fb2a802ee81b2488e15fed", + "1de159fbe3318fc7056492dd0c5e0cb17914c08cf94e3e16345ffe5977f8ee47", + "42e0425f97c5cacc6006dc069c9383f2bd0e46bf8d1c2730ff2012ce9e71ef05", + "417db987ce1fa1d4058ba96f4d0213e7a2882e6460db79b869e048b3b57c7bfb", + "4a626abaaba6a4b8724bbd88ac7b0ee46ffd6b4b7315b399a0393d249acf6545", + "7aaff0dfffff469b9041bb4e5ddb23c6ba410087b95087a991537d4b6d723f9f", + "340fa5f8bf9fdda458736def80703b68106dbcc397f651439a4f4eea0dd48889", + "d768de67676f5865b0e44eaa1b89d96038871c225ec5476a6b17f0b3719f7e3a", + "22e00d99b1adc1df4fdf092d2f4578fed2a5cbbdd2d37e5dcfc4fe9a83faec98", + "3b55350ddafdde0e2f464769e2b715646a7d73e47cef3172d5760949fa1d6e05", + "3dae9450add81a4bb053f9046bcc8c1b23d43f5341e1bfe160535f98dead811a", + "3176e2dded65e8e22a258d10ea1a50d517c86eafdcdd922149196475c9297ae3", + "a6c651210a568ba61718f324c94a0d3764bed18bef5b54147e87ac99a5eccc68", + "808c960d7a4ede71373929afdf080b7eae205dd5babb533cf81887e61b1819a2", + "0687f6f84a861e1e3a063e435dd42772934c1a91ebf05f11045125307bceb061", + "1ae134ef8f3a7035824ced29f5ce6328618600dffef18effa589a53bb957c2d3", + "2c588a7733988b94aeb54da2927647cf47812df85611dd8fc6cfb022dcd987ac", + "66d83487e3d49e23c64ddd3bfde0226da5545ce52643476cad5a26528cea4729", + "8b7bb65431222a769e1ad0dbf573019854f657787df83966f8075871bc29513c", + "2d1d34ba532851fc8d65411ac21f82eba0f9c75a62d91d9e6f4c56b115aa993e", + "cdd118b36f442f1a5ad055ba95412e0930cf579f7b1ef34f593ebfcbe8dbb075", + "8b2de8a5b986bda154dc88e165d556a2540c678e6373e21f910b75198c5ff477", + "948bc535ecac20205127f9cf3ef8d173a8c773f0b13308d4ec1bdf9f159f7a7c", + "e9edbcefe681eea9b7cd146cae8ab5ea737658f895bc225277740430c86cfac3", + "01490b1fa8bd0d88c95d97d38a268883bb00ef93e07a7751a9c4566ba385c5b6", + "430ae8ac7d102188966e71b1c753acf056496e0330a60d4cbd97c4cc3e05f867" + ] +} \ No newline at end of file diff --git a/testdata/dind/060_math_determinism.eintlog b/testdata/dind/060_math_determinism.eintlog new file mode 100644 index 00000000..4cf714af Binary files /dev/null and b/testdata/dind/060_math_determinism.eintlog differ diff --git a/testdata/dind/060_math_determinism.hashes.json b/testdata/dind/060_math_determinism.hashes.json new file mode 100644 index 00000000..c5b22356 --- /dev/null +++ b/testdata/dind/060_math_determinism.hashes.json @@ -0,0 +1,60 @@ +{ + "elog_version": 1, + "schema_hash_hex": "0427e9fd236e92dfc8c0765f7bd429fc233bfbfab3cb67c9d03b22a0f31e7f8a", + "hash_domain": "DIND_STATE_HASH_V1", + "hash_alg": "BLAKE3", + "hashes_hex": [ + "e0c2acac7f7eca867f8575a1861460425976de2fb399146b694de06507a7f890", + "71f6f7214862367dc6ecb795adf45586260f251d4aeaedd637aeee0033f067d7", + "cb885bef8712477028ec999b268dd9b4c29555367082e39da3932d3410d39ddc", + "8b745d424da78947b6d1492ae0793ee530f55a4b7261a3a3bedb942947f58583", + "e5a2f10a2ec1858b061f8c56607d890d09af16cecb89f90ed026c6f51ad644cc", + "13c1d7854626c988b5e4162455b6ab12d7d32df5a0685f810058c653f807342d", + "7ac7cb8750f35541cb99fe78f1440006325274f474827361f3cd456bebfc8d55", + "2a5947ac9dcdd9269f7fa889fd0cdea699a56c2dfad74def453281b0685c9d94", + "9d6b7b739e441596c5d776c56db0b3a4b6a8ca8e97dac8692181c683f155b901", + "b1bbc9c505262c359fe51e8df9a22f1fc467d2f908e7586555b7611c11546c2c", + "93062258f5cd78aaea62ba5e852015620940c033089233428cdfe672ec30ea3c", + "debe960240df63a5b6bd0a1957abed4f61a2a8c96ac0c607ead0fb14542eebb8", + "b1ef78fb31051f6d6ff9930a0ff4b218d27c22a2832ce8d3c260648a94be8565", + "49d7ee6a68d5abefde4416f99cb92eb4c4b5a7a3993dabc9699587f8d78e1336", + "60a9f2dd3bf37fdf56576ed5bad05a01a3f7c4d8c77858ef31cd7fb630eae4b2", + "b3718afa3015ba3385e52e5ff047bf90af15e6e6b6cc41782a350f3563f6c16c", + "b35f3646be46de809b7355c7e203f7d87f26b82fab6498b2a95f8ec5dd47f261", + "b59c2197e27d99d2b16daf9f0187fc8e49e72622f2a148e8c2a0d03daebefdc0", + "699f1a9d07f894e6cf762b7320b3dd85f509e204f58d558b85ed0d6687725717", + "4b1bce6bbc772ae850d0920700b8964e8225fc1a422a460e15887698c8ea2a86", + "41d5c563aeb6a1dd0e3c023aeb50e5fa9e414f08c0a09452c8cefbbf8f0f4166", + "48926368dbd69d7d14a8c6a1cf326ae1b8054042993c5db962e0801930374843", + "bed196dcaec27fa8618af3f8867abcf3788708fa35df45ae2c77d28b4fd953b5", + "4c9617819d44d2669f5962336c45892bd48a43a4b7e2252265f2088215a2c349", + "ebd902d0a8172d4446e6c9055c16269ec29fb2e5fd5634a55add76875a6c1893", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e", + "a04b3544cfce8e7a8824ae49da6241ae20be995d9c428536ce2832daca33964e" + ] +} \ No newline at end of file diff --git a/testdata/dind/MANIFEST.json b/testdata/dind/MANIFEST.json new file mode 100644 index 00000000..12321bf8 --- /dev/null +++ b/testdata/dind/MANIFEST.json @@ -0,0 +1,85 @@ +[ + { + "path": "010_dense_rewrite_seed0001.eintlog", + "tags": [ + "rewrite", + "smoke", + "pr" + ], + "desc": "Dense rewrite smoke test" + }, + { + "path": "030_error_determinism.eintlog", + "tags": [ + "error", + "pr" + ], + "desc": "Error determinism check" + }, + { + "path": "050_randomized_order_small_seed0001.eintlog", + "tags": [ + "order-sensitive", + "randomized", + "nightly" + ], + "desc": "Randomized order seed 1" + }, + { + "path": "050_randomized_order_small_seed0002.eintlog", + "tags": [ + "order-sensitive", + "randomized", + "nightly" + ], + "desc": "Randomized order seed 2" + }, + { + "path": "050_randomized_order_small_seed0003.eintlog", + "tags": [ + "order-sensitive", + "randomized", + "nightly" + ], + "desc": "Randomized order seed 3" + }, + { + "path": "051_randomized_convergent_seed0001.eintlog", + "tags": [ + "kv-commutative", + "convergent", + "nightly" + ], + "desc": "Convergent seed 1", + "converge_scope": "sim/state" + }, + { + "path": "051_randomized_convergent_seed0002.eintlog", + "tags": [ + "kv-commutative", + "convergent", + "nightly" + ], + "desc": "Convergent seed 2", + "converge_scope": "sim/state" + }, + { + "path": "051_randomized_convergent_seed0003.eintlog", + "tags": [ + "kv-commutative", + "convergent", + "nightly" + ], + "desc": "Convergent seed 3", + "converge_scope": "sim/state" + }, + { + "path": "060_math_determinism.eintlog", + "tags": [ + "math", + "physics", + "nightly" + ], + "desc": "Fixed-point math determinism (Motion)" + } +] diff --git a/tests/dind.bats b/tests/dind.bats new file mode 100644 index 00000000..66c94930 --- /dev/null +++ b/tests/dind.bats @@ -0,0 +1,62 @@ +#!/usr/bin/env bats + +setup() { + ECHO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + # Build the harness once (or ensure it's built) + (cd "$ECHO_ROOT" && cargo build -p echo-dind-harness --quiet) + + # Path to the binary + export HARNESS="$ECHO_ROOT/target/debug/echo-dind-harness" + + # Test data paths + export DATA_DIR="$ECHO_ROOT/testdata/dind" + export OUT_DIR="test-results/dind/bats" + mkdir -p "$OUT_DIR" +} + +@test "DIND: Smoke Test (Dense Rewrite) - Deterministic Run" { + run $HARNESS run "$DATA_DIR/010_dense_rewrite_seed0001.eintlog" \ + --golden "$DATA_DIR/010_dense_rewrite_seed0001.hashes.json" + + [ "$status" -eq 0 ] + [[ "$output" =~ "DIND: OK" ]] +} + +@test "DIND: Error Determinism - Stable Failure Modes" { + run $HARNESS run "$DATA_DIR/030_error_determinism.eintlog" \ + --golden "$DATA_DIR/030_error_determinism.hashes.json" + + [ "$status" -eq 0 ] + [[ "$output" =~ "DIND: OK" ]] +} + +@test "DIND: Torture Mode (Short) - 5 Runs" { + run $HARNESS torture "$DATA_DIR/010_dense_rewrite_seed0001.eintlog" --runs 5 + + [ "$status" -eq 0 ] + [[ "$output" =~ "Torture complete. 5 runs identical." ]] +} + +@test "DIND: Repro Bundle Generation on Failure" { + # Create a fake golden file with a bad hash to force failure + local fake_golden="$OUT_DIR/bad_golden.json" + cp "$DATA_DIR/010_dense_rewrite_seed0001.hashes.json" "$fake_golden" + # Sed replacement to corrupt the first hash (works on GNU and BSD sed) + sed -e 's/e0c2acac/deadbeef/g' "$fake_golden" > "$fake_golden.tmp" && mv "$fake_golden.tmp" "$fake_golden" + + local repro_dir="$OUT_DIR/repro_test" + rm -rf "$repro_dir" + + run $HARNESS run "$DATA_DIR/010_dense_rewrite_seed0001.eintlog" \ + --golden "$fake_golden" \ + --emit-repro "$repro_dir" + + [ "$status" -eq 1 ] + [[ "$output" =~ "Repro bundle emitted" ]] + + # Verify artifacts exist + [ -f "$repro_dir/scenario.eintlog" ] + [ -f "$repro_dir/actual.hashes.json" ] + [ -f "$repro_dir/expected.hashes.json" ] + [ -f "$repro_dir/diff.txt" ] +} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 2b333ce6..53345381 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -5,6 +5,7 @@ name = "xtask" version = "0.1.0" edition = "2021" +rust-version = "1.90.0" license = "Apache-2.0" publish = false