Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7daf735
docs(phase8): add runtime schema freeze inventory
flyingrobots Mar 22, 2026
219bb15
docs(phase8): define runtime schema artifact set
flyingrobots Mar 22, 2026
be8875c
feat(phase8): add runtime schema source fragments
flyingrobots Mar 22, 2026
e08a673
feat(phase8): add playback and scheduler schema fragments
flyingrobots Mar 22, 2026
1ec7dee
feat(phase8): validate local runtime schema fragments
flyingrobots Mar 22, 2026
fb32e42
docs(phase8): add runtime schema conformance audit
flyingrobots Mar 22, 2026
ec4478c
refactor(phase8): align writer head key naming
flyingrobots Mar 22, 2026
096f0c3
docs(phase8): add runtime schema mapping contract
flyingrobots Mar 22, 2026
637c707
refactor(phase8): type ABI runtime identifiers
flyingrobots Mar 22, 2026
fa95989
chore(tooling): track shared workspace settings
flyingrobots Mar 23, 2026
6ac3aba
fix(ci): restore tasks dag refresh inputs
flyingrobots Mar 23, 2026
5254e97
refactor(phase8): add shared runtime schema crate
flyingrobots Mar 23, 2026
ff7dcc0
docs(phase8): narrow shared runtime schema boundary
flyingrobots Mar 23, 2026
2af8725
docs(phase8): mark runtime schema freeze complete
flyingrobots Mar 23, 2026
0dc6f36
fix(phase8): restore schema feature gating and DAG defaults
flyingrobots Mar 23, 2026
2dfbc21
fix(phase8): harden schema freeze tooling
flyingrobots Mar 23, 2026
2521150
refactor(phase8): make worldline ids opaque
flyingrobots Mar 23, 2026
4cbeacb
docs(phase8): document review follow-ups
flyingrobots Mar 23, 2026
21a683e
docs: note phase 8 review fixes
flyingrobots Mar 23, 2026
67f57c7
docs(backlog): add review tooling follow-ups
flyingrobots Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules
dist
coverage
target
target-ra
target-fmt
target-clippy
target-test
Expand All @@ -21,6 +22,7 @@ docs/.vitepress/cache
.idea
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.obsidian
.claude/

Expand Down
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rust-analyzer.cargo.extraEnv": {
"CARGO_TARGET_DIR": "target-ra"
}
}
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@

## Unreleased

### Fixed (PR #306 follow-up)

- **Fixed** the Phase 8 runtime-schema/tooling follow-ups so workspace
Prettier usage is declared and lockfile-pinned, runtime schema validation now
fails clearly when `node` is unavailable, dependency DAG generation uses the
archived tasks DAG by default with UTC-stable fallback labels, and the
tracked Rust Analyzer workspace target dir is repo-local and cross-platform.
- **Fixed** shared Phase 8 type extraction so `WorldlineId` is actually opaque
like `HeadId`, `echo-wasm-abi` forwards `std`/`serde` into
`echo-runtime-schema` explicitly, `echo-wasm-abi --no-default-features`
avoids a stray `std` dependency, and positive-only scheduler/inbox schema
inputs are represented explicitly as `PositiveInt`.

### Fixed (PR #304 follow-up)

- **Fixed** the session WebSocket gateway TLS stack to use the Rustls ring
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize:
- rustup toolchain install 1.90.0
- rustup override set 1.90.0

### Shared Workspace Settings

- The repo tracks a minimal [.vscode/settings.json](/Users/james/git/echo/.vscode/settings.json) for project-safe tooling settings only.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace machine-specific absolute path with a repo-relative link.

Line 106 currently points to /Users/james/git/echo/.vscode/settings.json, which is broken for everyone except one local machine and leaks local path details.

🔧 Proposed fix
-- The repo tracks a minimal [.vscode/settings.json](/Users/james/git/echo/.vscode/settings.json) for project-safe tooling settings only.
+- The repo tracks a minimal [.vscode/settings.json](.vscode/settings.json) for project-safe tooling settings only.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- The repo tracks a minimal [.vscode/settings.json](/Users/james/git/echo/.vscode/settings.json) for project-safe tooling settings only.
- The repo tracks a minimal [.vscode/settings.json](.vscode/settings.json) for project-safe tooling settings only.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONTRIBUTING.md` at line 106, Replace the machine-specific absolute path
"/Users/james/git/echo/.vscode/settings.json" in the CONTRIBUTING.md sentence
with a repo-relative link to the tracked config, e.g. use
[.vscode/settings.json] (or ./ .vscode/settings.json) so the link works for all
users and does not expose a local filesystem path.

- Keep personal editor preferences such as theme, font family, and UI layout in your user-level VS Code settings, not the tracked workspace file.
- The tracked Rust Analyzer target dir uses the repo-local ignored `target-ra/` path to avoid fighting the default Cargo build directory during background checks.

## Communication

- Rely on GitHub discussions or issues for longer-form proposals.
Expand Down
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots>
[workspace]
members = [
"crates/echo-runtime-schema",
"crates/warp-core",

"crates/warp-wasm",
Expand Down Expand Up @@ -47,6 +48,7 @@ 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-runtime-schema = { version = "0.1.0", path = "crates/echo-runtime-schema", default-features = false }
echo-registry-api = { version = "0.1.0", path = "crates/echo-registry-api" }
echo-scene-codec = { version = "0.1.0", path = "crates/echo-scene-codec" }
echo-scene-port = { version = "0.1.0", path = "crates/echo-scene-port" }
Expand Down
2 changes: 1 addition & 1 deletion crates/echo-dind-harness/tests/digest_golden_vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ fn tick_commit_hash_v2_full_chain_golden_vector() {

// Step 3: Compute tick commit hash using the above digests
let schema_hash = make_hash(0xAB);
let worldline_id = WorldlineId(make_hash(0xCD));
let worldline_id = WorldlineId::from_bytes(make_hash(0xCD));
let tick = 42u64;
let parent = make_hash(0x11);
let patch_digest = make_hash(0x22);
Expand Down
24 changes: 24 additions & 0 deletions crates/echo-runtime-schema/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: Apache-2.0
# © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots>
[package]
name = "echo-runtime-schema"
version = "0.1.0"
edition = "2024"
rust-version = "1.90.0"
description = "Shared ADR-0008 runtime schema types for Echo"
license = "Apache-2.0"
repository = "https://github.com/flyingrobots/echo"
readme = "README.md"
keywords = ["echo", "runtime", "schema", "worldline"]
categories = ["data-structures"]

[dependencies]
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }

[features]
default = ["std", "serde"]
serde = ["dep:serde"]
std = ["serde?/std"]

[lints]
workspace = true
19 changes: 19 additions & 0 deletions crates/echo-runtime-schema/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 -->
<!-- © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots> -->

# echo-runtime-schema

Shared ADR-0008 runtime schema primitives for Echo.

This crate is the Echo-local shared owner for runtime-schema types that are not
inherently ABI-only:

- opaque runtime identifiers
- logical monotone counters
- structural runtime key types

`warp-core` consumes or re-exports these semantic types. `echo-wasm-abi`
converts to and from them where the host wire format differs.

Serde derives are feature-gated. Consumers that need serialization support must
enable this crate's `serde` feature explicitly.
Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify serde feature semantics to match current defaults.

This wording implies explicit opt-in is always required, but in this PR serde is documented as default-enabled. Make this conditional on default-features = false.

Suggested patch
-Serde derives are feature-gated. Consumers that need serialization support must
-enable this crate's `serde` feature explicitly.
+Serde derives are feature-gated. The `serde` feature is enabled by default;
+consumers using `default-features = false` must enable `serde` explicitly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/echo-runtime-schema/README.md` around lines 18 - 19, Update the README
sentence about serde feature gating to reflect the crate's default behavior:
state that serde derives are feature-gated and consumers must enable the crate's
`serde` feature unless they are using the crate with default features enabled
(or unless they compile with `default-features = false` to force explicit
opt-in); reference the `serde` feature name and the `default-features = false`
flag so readers know when explicit enabling is required.

200 changes: 200 additions & 0 deletions crates/echo-runtime-schema/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// SPDX-License-Identifier: Apache-2.0
// © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots>
//! Shared ADR-0008 runtime schema primitives.
//!
//! This crate is the Echo-local shared owner for generated-or-generation-ready
//! runtime schema types that are not inherently ABI-only:
//!
//! - opaque runtime identifiers
//! - logical monotone counters
//! - structural runtime key types
//!
//! Adapter crates such as `echo-wasm-abi` may still wrap these types when the
//! host wire format needs a different serialization contract.

#![cfg_attr(not(feature = "std"), no_std)]

use core::fmt;

macro_rules! logical_counter {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct $name(pub u64);

impl $name {
/// Zero value for this logical counter.
pub const ZERO: Self = Self(0);
/// Largest representable counter value.
pub const MAX: Self = Self(u64::MAX);

/// Builds the counter from its raw logical value.
#[must_use]
pub const fn from_raw(raw: u64) -> Self {
Self(raw)
}

/// Returns the raw logical value.
#[must_use]
pub const fn as_u64(self) -> u64 {
self.0
}

/// Adds `rhs`, returning `None` on overflow.
#[must_use]
pub fn checked_add(self, rhs: u64) -> Option<Self> {
self.0.checked_add(rhs).map(Self)
}

/// Subtracts `rhs`, returning `None` on underflow.
#[must_use]
pub fn checked_sub(self, rhs: u64) -> Option<Self> {
self.0.checked_sub(rhs).map(Self)
}

/// Increments by one, returning `None` on overflow.
#[must_use]
pub fn checked_increment(self) -> Option<Self> {
self.checked_add(1)
}
}

impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
};
}

/// Canonical 32-byte identifier payload used by shared runtime schema ids.
pub type RuntimeIdBytes = [u8; 32];

/// Opaque stable identifier for a worldline.
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
Comment on lines +79 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does #[serde(transparent)]on a newtype wrapping[u8; 32] serialize as a byte string or as a sequence of 32 integers by default?

💡 Result:

No, #[serde(transparent)] on a newtype wrapping [u8; 32] serializes as a sequence of 32 integers (array in JSON), not as a byte string, by default.

Citations:


🏁 Script executed:

# First, find and examine the lib.rs file to see the actual definitions at lines 79-80 and 100-101
cat -n crates/echo-runtime-schema/src/lib.rs | sed -n '75,105p'

Repository: flyingrobots/echo

Length of output: 1322


🏁 Script executed:

# Search for WorldlineId and HeadId definitions to understand their structure
rg -A 5 "struct WorldlineId|struct HeadId" crates/echo-runtime-schema/src/

Repository: flyingrobots/echo

Length of output: 974


🏁 Script executed:

# Look for any bytes(32) contract references in the codebase
rg "bytes\(32\)" --type rust

Repository: flyingrobots/echo

Length of output: 43


🏁 Script executed:

# Search for WriterHeadKey to see what it contains
rg -B 2 -A 5 "struct WriterHeadKey|WriterHeadKey" crates/

Repository: flyingrobots/echo

Length of output: 50373


🏁 Script executed:

# Check for any serde-related tests or specifications about serialization format
rg -B 2 -A 5 "bytes\(" crates/echo-runtime-schema/src/

Repository: flyingrobots/echo

Length of output: 3208


🏁 Script executed:

# Look for protobuf files
fd -e proto

Repository: flyingrobots/echo

Length of output: 43


🏁 Script executed:

# Search for any references to "bytes" in comments or code that might indicate a schema
rg -i "bytes.*32|schema.*32" --type rust --type toml

Repository: flyingrobots/echo

Length of output: 16793


🏁 Script executed:

# Check for any protocol or schema documentation
find . -name "*.md" -type f -exec grep -l "bytes(32)" {} \;

Repository: flyingrobots/echo

Length of output: 97


🏁 Script executed:

# Look for RuntimeIdBytes definition to understand the actual type
rg -B 3 -A 3 "type RuntimeIdBytes|RuntimeIdBytes\s*="

Repository: flyingrobots/echo

Length of output: 509


🏁 Script executed:

cat -n docs/spec/SPEC-0009-wasm-abi-v3.md | grep -A 5 -B 5 "bytes(32)\|bytes\(32\)"

Repository: flyingrobots/echo

Length of output: 9569


🏁 Script executed:

# Also search for any mention of serde contract in the spec
rg "serde|Serialize|Deserialize" docs/spec/ --type md

Repository: flyingrobots/echo

Length of output: 160


🏁 Script executed:

# Check the full lib.rs to see if WriterHeadKey has its own serde derive
cat -n crates/echo-runtime-schema/src/lib.rs | sed -n '1,200p'

Repository: flyingrobots/echo

Length of output: 8010


Add explicit byte-oriented Serialize/Deserialize impls to WorldlineId and HeadId, and test serde round-trip.

The #[serde(transparent)] on these newtypes wrapping [u8; 32] serializes as a 32-element integer array by default, not as bytes(32). This violates the ABI spec (SPEC-0009-wasm-abi-v3.md, lines 184–185), which requires these types to serialize as byte strings. WriterHeadKey inherits this mismatch when its fields serialize.

Implement custom Serialize and Deserialize for both ID types to produce canonical bytes(32) in CBOR/JSON, then add a test verifying the round-trip matches the spec format.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/echo-runtime-schema/src/lib.rs` around lines 79 - 80, WorldlineId and
HeadId newtypes currently use #[serde(transparent)] which serializes the inner
[u8;32] as a 32-element integer array instead of a byte string; implement
explicit serde::Serialize and serde::Deserialize for WorldlineId and HeadId to
serialize/deserialze as canonical bytes(32) (e.g., as a 32-byte byte string for
CBOR/JSON) and update WriterHeadKey usage to rely on those impls; add a unit
test that serializes and deserializes WorldlineId and HeadId (and a
WriterHeadKey instance) to verify round-trip produces the spec-compliant
bytes(32) representation.

pub struct WorldlineId(RuntimeIdBytes);

impl WorldlineId {
/// Reconstructs a worldline id from its canonical 32-byte representation.
#[must_use]
pub const fn from_bytes(bytes: RuntimeIdBytes) -> Self {
Self(bytes)
}

/// Returns the canonical byte representation of this id.
#[must_use]
pub const fn as_bytes(&self) -> &RuntimeIdBytes {
&self.0
}
}

/// Opaque stable identifier for a head.
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct HeadId(RuntimeIdBytes);

impl HeadId {
/// Inclusive minimum key used by internal `BTreeMap` range queries.
pub const MIN: Self = Self([0u8; 32]);
/// Inclusive maximum key used by internal `BTreeMap` range queries.
pub const MAX: Self = Self([0xff; 32]);

/// Reconstructs a head id from its canonical 32-byte representation.
#[must_use]
pub const fn from_bytes(bytes: RuntimeIdBytes) -> Self {
Self(bytes)
}

/// Returns the canonical byte representation of this id.
#[must_use]
pub const fn as_bytes(&self) -> &RuntimeIdBytes {
&self.0
}
}

logical_counter!(
/// Per-worldline append identity for committed history.
WorldlineTick
);

logical_counter!(
/// Runtime-cycle correlation stamp. No wall-clock semantics.
GlobalTick
);

logical_counter!(
/// Control-plane generation token for scheduler runs.
///
/// This value is not provenance, replay state, or hash input.
RunId
);

/// Composite key identifying a writer head within its worldline.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WriterHeadKey {
/// The worldline this head targets.
pub worldline_id: WorldlineId,
/// The head identity within that worldline.
pub head_id: HeadId,
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::{GlobalTick, HeadId, RunId, WorldlineId, WorldlineTick, WriterHeadKey};

macro_rules! assert_logical_counter_boundaries {
($ty:ty) => {{
assert_eq!(<$ty>::ZERO.as_u64(), 0);
assert_eq!(<$ty>::MAX.as_u64(), u64::MAX);
assert_eq!(<$ty>::from_raw(41).checked_add(1).unwrap().as_u64(), 42);
assert_eq!(<$ty>::MAX.checked_add(1), None);
assert_eq!(<$ty>::from_raw(42).checked_sub(1).unwrap().as_u64(), 41);
assert_eq!(<$ty>::ZERO.checked_sub(1), None);
assert_eq!(<$ty>::from_raw(7).checked_increment().unwrap().as_u64(), 8);
assert_eq!(<$ty>::MAX.checked_increment(), None);
}};
}

#[test]
fn worldline_tick_checked_arithmetic_boundaries() {
assert_logical_counter_boundaries!(WorldlineTick);
}

#[test]
fn global_tick_checked_arithmetic_boundaries() {
assert_logical_counter_boundaries!(GlobalTick);
}

#[test]
fn run_id_checked_arithmetic_boundaries() {
assert_logical_counter_boundaries!(RunId);
}

#[test]
fn opaque_ids_round_trip_bytes() {
let worldline = WorldlineId::from_bytes([3u8; 32]);
let head = HeadId::from_bytes([7u8; 32]);
assert_eq!(*worldline.as_bytes(), [3u8; 32]);
assert_eq!(*head.as_bytes(), [7u8; 32]);
}

#[test]
fn writer_head_key_preserves_typed_components() {
let key = WriterHeadKey {
worldline_id: WorldlineId::from_bytes([1u8; 32]),
head_id: HeadId::from_bytes([2u8; 32]),
};
assert_eq!(*key.worldline_id.as_bytes(), [1u8; 32]);
assert_eq!(*key.head_id.as_bytes(), [2u8; 32]);
}
}
2 changes: 2 additions & 0 deletions crates/echo-wasm-abi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ ciborium = { version = "0.2", default-features = false }
serde-value = { version = "0.7" }
half = { version = "2.4", default-features = false, features = ["alloc"] }
thiserror = { version = "2.0" }
echo-runtime-schema = { workspace = true, default-features = false, features = ["serde"] }

[features]
default = ["std"]
std = [
"serde/std",
"ciborium/std",
"half/std",
"echo-runtime-schema/std",
]
alloc = []

Expand Down
3 changes: 2 additions & 1 deletion crates/echo-wasm-abi/src/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! Minimal deterministic codec helpers (length-prefixed, LE scalars).

extern crate alloc;
use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec::Vec;
use core::str;
Expand Down Expand Up @@ -187,7 +188,7 @@ impl<'a> Reader<'a> {
pub fn read_string(&mut self, max_len: usize) -> Result<String, CodecError> {
let bytes = self.read_len_prefixed_bytes(max_len)?;
str::from_utf8(bytes)
.map(std::string::ToString::to_string)
.map(ToOwned::to_owned)
.map_err(|_| CodecError::InvalidUtf8)
}
}
Expand Down
Loading
Loading