diff --git a/.gitignore b/.gitignore index df346e52..bc5986b0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules dist coverage target +target-ra target-fmt target-clippy target-test @@ -21,6 +22,7 @@ docs/.vitepress/cache .idea .vscode/* !.vscode/extensions.json +!.vscode/settings.json .obsidian .claude/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..59c1c947 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.cargo.extraEnv": { + "CARGO_TARGET_DIR": "target-ra" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7144061e..47f629ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ ## 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** late Phase 8 review follow-ups so contributor docs use portable + workspace links, the runtime-schema README matches the default `serde` + contract, the schema audit/inventory docs reflect the typed-id migration, and + dependency-DAG docs use the correct GitHub workflow wording. + ### Fixed (PR #304 follow-up) - **Fixed** the session WebSocket gateway TLS stack to use the Rustls ring diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2fae73a5..f362f687 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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](.vscode/settings.json) for project-safe tooling settings only. +- 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. diff --git a/Cargo.lock b/Cargo.lock index edb0f9d8..e5cd4c98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1244,6 +1244,14 @@ dependencies = [ name = "echo-registry-api" version = "0.1.0" +[[package]] +name = "echo-runtime-schema" +version = "0.1.0" +dependencies = [ + "ciborium", + "serde", +] + [[package]] name = "echo-scene-codec" version = "0.1.0" @@ -1335,6 +1343,7 @@ name = "echo-wasm-abi" version = "0.1.0" dependencies = [ "ciborium", + "echo-runtime-schema", "half", "proptest", "serde", @@ -5303,6 +5312,7 @@ dependencies = [ "bytes", "ciborium", "echo-dry-tests", + "echo-runtime-schema", "echo-wasm-abi", "hex", "libm", diff --git a/Cargo.toml b/Cargo.toml index 30941b90..667f7138 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ # © James Ross Ω FLYING•ROBOTS [workspace] members = [ + "crates/echo-runtime-schema", "crates/warp-core", "crates/warp-wasm", @@ -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" } diff --git a/crates/echo-dind-harness/tests/digest_golden_vectors.rs b/crates/echo-dind-harness/tests/digest_golden_vectors.rs index a69ef12b..cd553b02 100644 --- a/crates/echo-dind-harness/tests/digest_golden_vectors.rs +++ b/crates/echo-dind-harness/tests/digest_golden_vectors.rs @@ -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); diff --git a/crates/echo-runtime-schema/Cargo.toml b/crates/echo-runtime-schema/Cargo.toml new file mode 100644 index 00000000..a123c060 --- /dev/null +++ b/crates/echo-runtime-schema/Cargo.toml @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +[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 } + +[dev-dependencies] +ciborium = "0.2" + +[features] +default = ["std", "serde"] +serde = ["dep:serde"] +std = ["serde?/std"] + +[lints] +workspace = true diff --git a/crates/echo-runtime-schema/README.md b/crates/echo-runtime-schema/README.md new file mode 100644 index 00000000..5d94d82e --- /dev/null +++ b/crates/echo-runtime-schema/README.md @@ -0,0 +1,19 @@ + + + +# 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. The `serde` feature is enabled by default; +consumers using `default-features = false` must enable `serde` explicitly. diff --git a/crates/echo-runtime-schema/src/lib.rs b/crates/echo-runtime-schema/src/lib.rs new file mode 100644 index 00000000..afdda6f9 --- /dev/null +++ b/crates/echo-runtime-schema/src/lib.rs @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! 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)] + +#[cfg(feature = "serde")] +extern crate alloc; + +use core::fmt; + +#[cfg(feature = "serde")] +use serde::{ + Deserialize, Deserializer, Serialize, Serializer, + de::{self, SeqAccess, Visitor}, +}; + +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.0.checked_add(rhs).map(Self) + } + + /// Subtracts `rhs`, returning `None` on underflow. + #[must_use] + pub fn checked_sub(self, rhs: u64) -> Option { + self.0.checked_sub(rhs).map(Self) + } + + /// Increments by one, returning `None` on overflow. + #[must_use] + pub fn checked_increment(self) -> Option { + 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)] +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)] +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(feature = "serde")] +fn serialize_runtime_id(bytes: &RuntimeIdBytes, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_bytes(bytes) +} + +#[cfg(feature = "serde")] +fn decode_runtime_id(bytes: &[u8]) -> Result +where + E: de::Error, +{ + bytes + .try_into() + .map_err(|_| E::invalid_length(bytes.len(), &"exactly 32 bytes")) +} + +#[cfg(feature = "serde")] +struct RuntimeIdVisitor { + type_name: &'static str, +} + +#[cfg(feature = "serde")] +impl RuntimeIdVisitor { + const fn new(type_name: &'static str) -> Self { + Self { type_name } + } +} + +#[cfg(feature = "serde")] +impl<'de> Visitor<'de> for RuntimeIdVisitor { + type Value = RuntimeIdBytes; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "exactly 32 bytes for {}", self.type_name) + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: de::Error, + { + decode_runtime_id(value) + } + + fn visit_borrowed_bytes(self, value: &'de [u8]) -> Result + where + E: de::Error, + { + self.visit_bytes(value) + } + + fn visit_byte_buf(self, value: alloc::vec::Vec) -> Result + where + E: de::Error, + { + self.visit_bytes(&value) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut bytes = [0u8; 32]; + for (index, byte) in bytes.iter_mut().enumerate() { + *byte = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(index, &self))?; + } + if seq.next_element::()?.is_some() { + return Err(de::Error::invalid_length(33, &self)); + } + Ok(bytes) + } +} + +#[cfg(feature = "serde")] +impl Serialize for WorldlineId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize_runtime_id(self.as_bytes(), serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for WorldlineId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let bytes = deserializer.deserialize_bytes(RuntimeIdVisitor::new("WorldlineId"))?; + Ok(Self::from_bytes(bytes)) + } +} + +#[cfg(feature = "serde")] +impl Serialize for HeadId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize_runtime_id(self.as_bytes(), serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for HeadId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let bytes = deserializer.deserialize_bytes(RuntimeIdVisitor::new("HeadId"))?; + Ok(Self::from_bytes(bytes)) + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::{GlobalTick, HeadId, RunId, WorldlineId, WorldlineTick, WriterHeadKey}; + #[cfg(feature = "serde")] + use ciborium::value::Value; + #[cfg(feature = "serde")] + use serde::{Serialize, de::DeserializeOwned}; + + 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]); + } + + #[cfg(feature = "serde")] + fn encode_cbor(value: &T) -> Vec { + let mut bytes = Vec::new(); + ciborium::into_writer(value, &mut bytes).unwrap(); + bytes + } + + #[cfg(feature = "serde")] + fn decode_cbor(bytes: &[u8]) -> T { + ciborium::from_reader(bytes).unwrap() + } + + #[cfg(feature = "serde")] + #[test] + fn runtime_ids_serialize_as_cbor_bytes() { + let worldline = WorldlineId::from_bytes([3u8; 32]); + let head = HeadId::from_bytes([7u8; 32]); + + let worldline_value: Value = decode_cbor(&encode_cbor(&worldline)); + let head_value: Value = decode_cbor(&encode_cbor(&head)); + + assert_eq!(worldline_value, Value::Bytes(vec![3u8; 32])); + assert_eq!(head_value, Value::Bytes(vec![7u8; 32])); + assert_eq!( + decode_cbor::(&encode_cbor(&worldline)), + worldline + ); + assert_eq!(decode_cbor::(&encode_cbor(&head)), head); + } + + #[cfg(feature = "serde")] + #[test] + fn writer_head_key_cbor_round_trip_preserves_byte_encoding() { + let key = WriterHeadKey { + worldline_id: WorldlineId::from_bytes([5u8; 32]), + head_id: HeadId::from_bytes([9u8; 32]), + }; + + let value: Value = decode_cbor(&encode_cbor(&key)); + assert!(matches!(value, Value::Map(_))); + let entries = match value { + Value::Map(entries) => entries, + _ => return, + }; + + let encoded_worldline = entries.iter().find_map(|(field, value)| match field { + Value::Text(name) if name == "worldline_id" => Some(value), + _ => None, + }); + let encoded_head = entries.iter().find_map(|(field, value)| match field { + Value::Text(name) if name == "head_id" => Some(value), + _ => None, + }); + + assert_eq!(encoded_worldline, Some(&Value::Bytes(vec![5u8; 32]))); + assert_eq!(encoded_head, Some(&Value::Bytes(vec![9u8; 32]))); + assert_eq!(decode_cbor::(&encode_cbor(&key)), key); + } +} diff --git a/crates/echo-wasm-abi/Cargo.toml b/crates/echo-wasm-abi/Cargo.toml index acd4f2c7..aa49704e 100644 --- a/crates/echo-wasm-abi/Cargo.toml +++ b/crates/echo-wasm-abi/Cargo.toml @@ -18,6 +18,7 @@ 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"] @@ -25,6 +26,7 @@ std = [ "serde/std", "ciborium/std", "half/std", + "echo-runtime-schema/std", ] alloc = [] diff --git a/crates/echo-wasm-abi/src/codec.rs b/crates/echo-wasm-abi/src/codec.rs index d8a9d9ee..7f6a7c2e 100644 --- a/crates/echo-wasm-abi/src/codec.rs +++ b/crates/echo-wasm-abi/src/codec.rs @@ -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; @@ -187,7 +188,7 @@ impl<'a> Reader<'a> { pub fn read_string(&mut self, max_len: usize) -> Result { 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) } } diff --git a/crates/echo-wasm-abi/src/kernel_port.rs b/crates/echo-wasm-abi/src/kernel_port.rs index 71f9c3e1..f1f5cb4e 100644 --- a/crates/echo-wasm-abi/src/kernel_port.rs +++ b/crates/echo-wasm-abi/src/kernel_port.rs @@ -28,7 +28,12 @@ extern crate alloc; use alloc::string::String; use alloc::vec::Vec; -use serde::{Deserialize, Serialize}; +use core::fmt; +pub use echo_runtime_schema::{GlobalTick, RunId, WorldlineTick}; +use serde::{ + Deserialize, Serialize, + de::{self, SeqAccess, Visitor}, +}; /// Current ABI version for the kernel port contract. /// @@ -36,49 +41,110 @@ use serde::{Deserialize, Serialize}; /// in a backward-incompatible way. pub const ABI_VERSION: u32 = 3; -macro_rules! logical_counter { +fn deserialize_opaque_id<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error> +where + D: serde::Deserializer<'de>, +{ + struct OpaqueIdVisitor; + + impl<'de> Visitor<'de> for OpaqueIdVisitor { + type Value = [u8; 32]; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("exactly 32 bytes") + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: de::Error, + { + value + .try_into() + .map_err(|_| E::invalid_length(value.len(), &self)) + } + + fn visit_byte_buf(self, value: Vec) -> Result + where + E: de::Error, + { + self.visit_bytes(&value) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut bytes = [0u8; 32]; + for (index, slot) in bytes.iter_mut().enumerate() { + *slot = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(index, &self))?; + } + if seq.next_element::()?.is_some() { + return Err(de::Error::invalid_length(33, &self)); + } + Ok(bytes) + } + } + + deserializer.deserialize_bytes(OpaqueIdVisitor) +} + +macro_rules! opaque_id { ($(#[$meta:meta])* $name:ident) => { $(#[$meta])* #[repr(transparent)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] - pub struct $name(pub u64); + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name([u8; 32]); + + impl $name { + /// Reconstructs an id from its canonical 32-byte representation. + #[must_use] + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Returns the canonical 32-byte representation. + #[must_use] + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + } + + impl Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(&self.0) + } + } + + impl<'de> Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserialize_opaque_id(deserializer).map(Self) + } + } }; } -logical_counter!( - /// Per-worldline logical coordinate in host-visible metadata. - /// - /// The meaning of `0` depends on the surface carrying it: - /// - /// - In historical coordinates such as [`ObservationAt::Tick`], `0` names - /// the first committed append. - /// - In frontier/head metadata such as [`HeadInfo`] and - /// [`HeadObservation`], `0` paired with `commit_global_tick = None` - /// means the worldline is still at `U0` and has not committed anything. - WorldlineTick -); - -logical_counter!( - /// Runtime-cycle correlation stamp in host-visible metadata. +opaque_id!( + /// Opaque stable identifier for a worldline. /// - /// `GlobalTick` is monotonic for the lifetime of one initialized kernel and - /// spans all worldlines managed by that kernel. It is not a per-worldline - /// append id and it resets when the host creates a fresh kernel via - /// `init()`. When exposed through [`SchedulerStatus`] as an `Option`, `None` - /// means the referenced event has not happened yet in this kernel lifetime - /// (for example, no cycle has completed or no commit has occurred). - GlobalTick + /// This is the canonical 32-byte worldline-id hash, carried as typed + /// metadata rather than a generic byte vector. + WorldlineId ); -logical_counter!( - /// Control-plane run generation token. +opaque_id!( + /// Opaque stable identifier for a head within a worldline. /// - /// A fresh `RunId` is minted for every accepted `Start` request. It stays - /// stable across all host observations and `dispatch_intent(...)` calls - /// during that run, then remains visible in [`SchedulerStatus::run_id`] - /// after completion until another `Start` replaces it or the kernel is - /// re-initialized. - RunId + /// This is the canonical 32-byte head-id hash, carried as typed metadata + /// rather than a generic byte vector. + HeadId ); // --------------------------------------------------------------------------- @@ -287,14 +353,13 @@ pub struct SchedulerStatus { pub last_run_completion: Option, } -/// Stable head key used by control intents. +/// Stable writer-head key used by control intents. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct HeadKey { - /// Worldline that owns the head as a canonical 32-byte worldline-id hash. - pub worldline_id: Vec, - /// Stable head identifier within that worldline as a canonical 32-byte - /// domain-separated head-id hash payload. - pub head_id: Vec, +pub struct WriterHeadKey { + /// Worldline that owns the head. + pub worldline_id: WorldlineId, + /// Stable head identifier within that worldline. + pub head_id: HeadId, } /// Privileged control intents routed through the same intent intake surface. @@ -315,7 +380,7 @@ pub enum ControlIntentV1 { /// Change declarative head admission. SetHeadEligibility { /// Target head whose eligibility should change. - head: HeadKey, + head: WriterHeadKey, /// New declarative eligibility for that head. eligibility: HeadEligibility, }, @@ -334,7 +399,7 @@ pub struct ChannelData { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ObservationCoordinate { /// Worldline to observe. - pub worldline_id: Vec, + pub worldline_id: WorldlineId, /// Requested coordinate within the worldline. pub at: ObservationAt, } @@ -405,7 +470,7 @@ pub struct ResolvedObservationCoordinate { /// Observation contract version. pub observation_version: u32, /// Worldline actually observed. - pub worldline_id: Vec, + pub worldline_id: WorldlineId, /// Original coordinate selector from the request. pub requested_at: ObservationAt, /// Concrete resolved worldline coordinate. diff --git a/crates/echo-wasm-abi/src/lib.rs b/crates/echo-wasm-abi/src/lib.rs index d4e2a148..6fbc5f3e 100644 --- a/crates/echo-wasm-abi/src/lib.rs +++ b/crates/echo-wasm-abi/src/lib.rs @@ -363,6 +363,7 @@ pub struct Rewrite { #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { use super::*; + use alloc::vec; fn hex_encode(bytes: &[u8]) -> String { let mut out = String::with_capacity(bytes.len() * 2); @@ -437,7 +438,9 @@ mod tests { #[test] fn test_control_intent_round_trip() { - use crate::kernel_port::{ControlIntentV1, SchedulerMode}; + use crate::kernel_port::{ + ControlIntentV1, HeadId, SchedulerMode, WorldlineId, WriterHeadKey, + }; let packed = pack_control_intent_v1(&ControlIntentV1::Start { mode: SchedulerMode::UntilIdle { @@ -455,6 +458,90 @@ mod tests { }, } ); + + let packed = pack_control_intent_v1(&ControlIntentV1::SetHeadEligibility { + head: WriterHeadKey { + worldline_id: WorldlineId::from_bytes([1u8; 32]), + head_id: HeadId::from_bytes([2u8; 32]), + }, + eligibility: crate::kernel_port::HeadEligibility::Dormant, + }) + .unwrap(); + + let unpacked = unpack_control_intent_v1(&packed).unwrap(); + assert_eq!( + unpacked, + ControlIntentV1::SetHeadEligibility { + head: WriterHeadKey { + worldline_id: WorldlineId::from_bytes([1u8; 32]), + head_id: HeadId::from_bytes([2u8; 32]), + }, + eligibility: crate::kernel_port::HeadEligibility::Dormant, + } + ); + } + + #[test] + fn test_worldline_id_round_trip_uses_cbor_bytes() { + use crate::kernel_port::WorldlineId; + use ciborium::value::Value; + + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Wrapper { + id: WorldlineId, + } + + let bytes = encode_cbor(&Wrapper { + id: WorldlineId::from_bytes([7u8; 32]), + }) + .unwrap(); + let value = decode_value(&bytes).unwrap(); + assert!(matches!(value, Value::Map(_))); + let Value::Map(entries) = value else { + unreachable!(); + }; + let (_, encoded_id) = entries + .into_iter() + .find(|(key, _)| matches!(key, Value::Text(text) if text == "id")) + .expect("id entry should exist"); + assert_eq!(encoded_id, Value::Bytes(vec![7u8; 32])); + + let decoded: Wrapper = decode_cbor(&bytes).unwrap(); + assert_eq!( + decoded, + Wrapper { + id: WorldlineId::from_bytes([7u8; 32]), + } + ); + } + + #[test] + fn test_worldline_id_rejects_non_32_byte_payloads() { + use crate::kernel_port::WorldlineId; + use ciborium::value::Value; + + #[derive(Debug, PartialEq, Eq, Deserialize)] + struct Wrapper { + id: WorldlineId, + } + + let bytes = encode_value(&Value::Map(vec![( + Value::Text("id".into()), + Value::Bytes(vec![9u8; 31]), + )])) + .unwrap(); + + let err = decode_cbor::(&bytes).unwrap_err(); + assert!(err.to_string().contains("32 bytes")); + + let bytes = encode_value(&Value::Map(vec![( + Value::Text("id".into()), + Value::Bytes(vec![9u8; 33]), + )])) + .unwrap(); + + let err = decode_cbor::(&bytes).unwrap_err(); + assert!(err.to_string().contains("32 bytes")); } #[test] diff --git a/crates/ttd-browser/src/lib.rs b/crates/ttd-browser/src/lib.rs index e9a51ffd..27eb281a 100644 --- a/crates/ttd-browser/src/lib.rs +++ b/crates/ttd-browser/src/lib.rs @@ -780,7 +780,7 @@ impl TtdEngine { version: echo_session_proto::TTDR_VERSION, flags, schema_hash, - worldline_id: cursor.worldline_id.0, + worldline_id: *cursor.worldline_id.as_bytes(), tick: cursor.current_tick().as_u64(), commit_hash: expected.commit_hash, state_root: expected.state_root, @@ -825,7 +825,7 @@ impl TtdEngine { .ok_or_else(|| JsError::new("cursor not found"))?; Ok(BrowserSnapshot { - worldline_id: bytes_to_hex(&cursor.worldline_id.0), + worldline_id: bytes_to_hex(cursor.worldline_id.as_bytes()), tick: cursor.current_tick().as_u64(), }) } @@ -854,7 +854,7 @@ impl TtdEngine { let source_wl_bytes = hex_to_bytes(&snap.worldline_id) .map_err(|e| JsError::new(&format!("invalid worldlineId: {e}")))?; - let source_wl = WorldlineId(source_wl_bytes); + let source_wl = WorldlineId::from_bytes(source_wl_bytes); let new_wl = parse_worldline_id(new_worldline_id)?; let tick = WorldlineTick::from_raw(snap.tick); @@ -875,7 +875,7 @@ impl TtdEngine { } // Create cursor for the new worldline - self.create_cursor(&new_wl.0) + self.create_cursor(new_wl.as_bytes()) } // ─── Compliance (Stubs) ────────────────────────────────────────────────── @@ -988,7 +988,7 @@ fn parse_worldline_id_inner(bytes: &[u8]) -> Result { let arr: [u8; 32] = bytes .try_into() .map_err(|_| ParseError("worldline_id must be 32 bytes"))?; - Ok(WorldlineId(arr)) + Ok(WorldlineId::from_bytes(arr)) } fn parse_warp_id_inner(bytes: &[u8]) -> Result { @@ -1206,7 +1206,7 @@ mod tests { fn test_parse_worldline_id_inner_valid() { let wl = parse_worldline_id_inner(&[0u8; 32]); assert!(wl.is_ok()); - assert_eq!(wl.unwrap().0, [0u8; 32]); + assert_eq!(*wl.unwrap().as_bytes(), [0u8; 32]); } #[test] @@ -1374,7 +1374,7 @@ mod tests { cursor: CursorReceipt { session_id: s1_id, cursor_id: CursorId([0u8; 32]), - worldline_id: WorldlineId([0u8; 32]), + worldline_id: WorldlineId::from_bytes([0u8; 32]), warp_id: WarpId([0u8; 32]), worldline_tick: WorldlineTick::ZERO, commit_global_tick: None, @@ -1389,7 +1389,7 @@ mod tests { cursor: CursorReceipt { session_id: s2_id, cursor_id: CursorId([0u8; 32]), - worldline_id: WorldlineId([0u8; 32]), + worldline_id: WorldlineId::from_bytes([0u8; 32]), warp_id: WarpId([0u8; 32]), worldline_tick: WorldlineTick::ZERO, commit_global_tick: None, @@ -1501,10 +1501,10 @@ mod tests { }; let mut engine = TtdEngine::new(); - let wl_id = WorldlineId([1u8; 32]); + let wl_id = WorldlineId::from_bytes([1u8; 32]); let warp_id = WarpId([2u8; 32]); engine - .register_empty_worldline(&wl_id.0, &warp_id.0) + .register_empty_worldline(wl_id.as_bytes(), &warp_id.0) .unwrap(); // Manually add a tick with outputs to provenance. @@ -1564,7 +1564,7 @@ mod tests { engine.provenance.append_local_commit(entry).unwrap(); - let cursor_id = engine.create_cursor(&wl_id.0).unwrap(); + let cursor_id = engine.create_cursor(wl_id.as_bytes()).unwrap(); // Advance cursor to tick 1 so we can commit (cannot commit at tick 0). let mut cursor = engine.cursors.remove(&cursor_id).unwrap(); cursor diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index 41397104..8694ea3b 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -24,6 +24,7 @@ rustc-hash = "2.1.1" serde = { version = "1.0", features = ["derive"], optional = true } libm = "0.2" echo-wasm-abi = { workspace = true } +echo-runtime-schema = { workspace = true } [dev-dependencies] serde = { version = "1.0", features = ["derive"] } @@ -40,7 +41,7 @@ golden_prng = [] trig_audit_print = [] # 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"] +serde = ["dep:serde", "bytes/serde", "echo-runtime-schema/serde"] # Scalar backend lanes (CI/test orchestration). # - `det_float`: default float32-backed lane using `F32Scalar`. diff --git a/crates/warp-core/src/clock.rs b/crates/warp-core/src/clock.rs index 99d33784..31cab645 100644 --- a/crates/warp-core/src/clock.rs +++ b/crates/warp-core/src/clock.rs @@ -1,110 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Logical clock and generation identifiers for runtime metadata. +//! Re-export of shared logical clock and generation identifiers for runtime metadata. //! //! Echo's internal clocks are logical monotone counters only. They carry no //! wall-clock or elapsed-time semantics. -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))] - pub struct $name(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.0.checked_add(rhs).map(Self) - } - - /// Subtracts `rhs`, returning `None` on underflow. - #[must_use] - pub fn checked_sub(self, rhs: u64) -> Option { - self.0.checked_sub(rhs).map(Self) - } - - /// Increments by one, returning `None` on overflow. - #[must_use] - pub fn checked_increment(self) -> Option { - self.checked_add(1) - } - } - - impl core::fmt::Display for $name { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - self.0.fmt(f) - } - } - }; -} - -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 -); - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod tests { - use super::{GlobalTick, RunId, WorldlineTick}; - - 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); - } -} +pub use echo_runtime_schema::{GlobalTick, RunId, WorldlineTick}; diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index bcb11810..eb754243 100644 --- a/crates/warp-core/src/coordinator.rs +++ b/crates/warp-core/src/coordinator.rs @@ -569,7 +569,7 @@ mod tests { }; fn wl(n: u8) -> WorldlineId { - WorldlineId([n; 32]) + WorldlineId::from_bytes([n; 32]) } fn wt(raw: u64) -> WorldlineTick { diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index 665743e9..7c748dba 100644 --- a/crates/warp-core/src/engine_impl.rs +++ b/crates/warp-core/src/engine_impl.rs @@ -3092,7 +3092,7 @@ mod tests { let admitted = [IngressEnvelope::local_intent( crate::head_inbox::IngressTarget::DefaultWriter { - worldline_id: crate::worldline::WorldlineId([7; 32]), + worldline_id: crate::worldline::WorldlineId::from_bytes([7; 32]), }, crate::head_inbox::make_intent_kind("test/runtime"), b"rollback-root".to_vec(), @@ -3147,7 +3147,7 @@ mod tests { let original_root = *state.root(); let env = IngressEnvelope::local_intent( crate::head_inbox::IngressTarget::DefaultWriter { - worldline_id: crate::worldline::WorldlineId([5; 32]), + worldline_id: crate::worldline::WorldlineId::from_bytes([5; 32]), }, crate::head_inbox::make_intent_kind("test/runtime-panic"), b"panic".to_vec(), @@ -3240,7 +3240,7 @@ mod tests { let env = IngressEnvelope::local_intent( crate::head_inbox::IngressTarget::DefaultWriter { - worldline_id: crate::worldline::WorldlineId([6; 32]), + worldline_id: crate::worldline::WorldlineId::from_bytes([6; 32]), }, crate::head_inbox::make_intent_kind("test/runtime-marker"), b"duplicate-ingress".to_vec(), @@ -3347,7 +3347,7 @@ mod tests { let Ok(mut state) = state_result else { return; }; - let worldline_id = crate::worldline::WorldlineId([9; 32]); + let worldline_id = crate::worldline::WorldlineId::from_bytes([9; 32]); let kind_a = crate::head_inbox::make_intent_kind("test/runtime-a"); let kind_b = crate::head_inbox::make_intent_kind("test/runtime-b"); let bytes = b"same-bytes".to_vec(); diff --git a/crates/warp-core/src/head.rs b/crates/warp-core/src/head.rs index b7b66f99..ea03b6ab 100644 --- a/crates/warp-core/src/head.rs +++ b/crates/warp-core/src/head.rs @@ -23,69 +23,19 @@ use std::collections::BTreeMap; +pub use echo_runtime_schema::{HeadId, WriterHeadKey}; + use crate::head_inbox::{HeadInbox, InboxAddress, InboxPolicy}; -use crate::ident::Hash; use crate::playback::PlaybackMode; use crate::worldline::WorldlineId; -// ============================================================================= -// HeadId -// ============================================================================= - -/// Opaque stable identifier for a head (writer or reader). -/// -/// Derived from a domain-separated BLAKE3 hash of the head's creation label -/// (`"head:" || label`). Never derived from mutable runtime structure. -#[repr(transparent)] -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct HeadId(Hash); - -impl HeadId { - /// Inclusive minimum key used by internal `BTreeMap` range queries. - pub(crate) const MIN: Self = Self([0u8; 32]); - /// Inclusive maximum key used by internal `BTreeMap` range queries. - pub(crate) const MAX: Self = Self([0xff; 32]); - - /// Reconstructs a head id from its canonical 32-byte hash representation. - /// - /// This is for round-trip deserialization and persistence only. It bypasses - /// the domain-separated construction path used by [`make_head_id`], so - /// callers should use [`make_head_id`] when deriving fresh ids from names or - /// untrusted input. - #[must_use] - pub fn from_bytes(bytes: Hash) -> Self { - Self(bytes) - } - - /// Returns the canonical byte representation of this id. - #[must_use] - pub fn as_bytes(&self) -> &Hash { - &self.0 - } -} - /// Produces a stable, domain-separated head identifier (prefix `b"head:"`) using BLAKE3. #[must_use] pub fn make_head_id(label: &str) -> HeadId { let mut hasher = blake3::Hasher::new(); hasher.update(b"head:"); hasher.update(label.as_bytes()); - HeadId(hasher.finalize().into()) -} - -// ============================================================================= -// WriterHeadKey -// ============================================================================= - -/// Composite key identifying a writer head within its worldline. -/// -/// Ordering is `(worldline_id, head_id)` for canonical scheduling. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct WriterHeadKey { - /// The worldline this head targets. - pub worldline_id: WorldlineId, - /// The head identity within that worldline. - pub head_id: HeadId, + HeadId::from_bytes(hasher.finalize().into()) } /// Declarative scheduler admission for a writer head. @@ -385,7 +335,7 @@ mod tests { use super::*; fn wl(n: u8) -> WorldlineId { - WorldlineId([n; 32]) + WorldlineId::from_bytes([n; 32]) } fn hd(label: &str) -> HeadId { diff --git a/crates/warp-core/src/head_inbox.rs b/crates/warp-core/src/head_inbox.rs index ce7e78d3..e2bbf2cb 100644 --- a/crates/warp-core/src/head_inbox.rs +++ b/crates/warp-core/src/head_inbox.rs @@ -274,7 +274,7 @@ impl Default for HeadInbox { fn default() -> Self { Self { head_key: WriterHeadKey { - worldline_id: WorldlineId([0u8; 32]), + worldline_id: WorldlineId::from_bytes([0u8; 32]), head_id: crate::head::HeadId::MIN, }, pending: BTreeMap::new(), @@ -419,7 +419,7 @@ mod tests { use super::*; fn wl(n: u8) -> WorldlineId { - WorldlineId([n; 32]) + WorldlineId::from_bytes([n; 32]) } fn test_kind() -> IntentKind { diff --git a/crates/warp-core/src/observation.rs b/crates/warp-core/src/observation.rs index 143ef2db..f8f8f924 100644 --- a/crates/warp-core/src/observation.rs +++ b/crates/warp-core/src/observation.rs @@ -210,7 +210,7 @@ impl ResolvedObservationCoordinate { fn to_abi(&self) -> abi::ResolvedObservationCoordinate { abi::ResolvedObservationCoordinate { observation_version: self.observation_version, - worldline_id: self.worldline_id.0.to_vec(), + worldline_id: abi::WorldlineId::from_bytes(*self.worldline_id.as_bytes()), requested_at: self.requested_at.to_abi(), resolved_worldline_tick: abi::WorldlineTick(self.resolved_worldline_tick.as_u64()), commit_global_tick: self @@ -659,7 +659,7 @@ mod tests { }; fn wl(n: u8) -> WorldlineId { - WorldlineId([n; 32]) + WorldlineId::from_bytes([n; 32]) } fn wt(raw: u64) -> WorldlineTick { @@ -681,7 +681,7 @@ mod tests { ); let engine = EngineBuilder::new(store, root).workers(1).build(); - let default_worldline = WorldlineId(engine.root_key().warp_id.0); + let default_worldline = WorldlineId::from_bytes(engine.root_key().warp_id.0); let mut runtime = WorldlineRuntime::new(); let default_state = WorldlineState::try_from(engine.state().clone()).unwrap(); let mut provenance = ProvenanceService::new(); diff --git a/crates/warp-core/src/playback.rs b/crates/warp-core/src/playback.rs index 6db5daa8..16a0bdb4 100644 --- a/crates/warp-core/src/playback.rs +++ b/crates/warp-core/src/playback.rs @@ -965,7 +965,7 @@ mod tests { let receipt1 = CursorReceipt { session_id: SessionId([1u8; 32]), cursor_id: CursorId([2u8; 32]), - worldline_id: WorldlineId([3u8; 32]), + worldline_id: WorldlineId::from_bytes([3u8; 32]), warp_id: crate::ident::WarpId([4u8; 32]), worldline_tick: wt(42), commit_global_tick: Some(gt(7)), @@ -974,7 +974,7 @@ mod tests { let receipt2 = CursorReceipt { session_id: SessionId([1u8; 32]), cursor_id: CursorId([2u8; 32]), - worldline_id: WorldlineId([3u8; 32]), + worldline_id: WorldlineId::from_bytes([3u8; 32]), warp_id: crate::ident::WarpId([4u8; 32]), worldline_tick: wt(42), commit_global_tick: Some(gt(7)), @@ -988,7 +988,7 @@ mod tests { let cursor = CursorReceipt { session_id: SessionId([1u8; 32]), cursor_id: CursorId([2u8; 32]), - worldline_id: WorldlineId([3u8; 32]), + worldline_id: WorldlineId::from_bytes([3u8; 32]), warp_id: crate::ident::WarpId([4u8; 32]), worldline_tick: wt(42), commit_global_tick: Some(gt(7)), @@ -1018,7 +1018,7 @@ mod tests { let receipt = CursorReceipt { session_id: session_a, cursor_id: CursorId([0u8; 32]), - worldline_id: WorldlineId([0u8; 32]), + worldline_id: WorldlineId::from_bytes([0u8; 32]), warp_id: WarpId([0u8; 32]), worldline_tick: wt(1), commit_global_tick: None, diff --git a/crates/warp-core/src/provenance_store.rs b/crates/warp-core/src/provenance_store.rs index 5ed6636e..05c365a0 100644 --- a/crates/warp-core/src/provenance_store.rs +++ b/crates/warp-core/src/provenance_store.rs @@ -1964,7 +1964,11 @@ mod tests { use crate::worldline::{AtomWrite, WorldlineTickHeaderV1}; fn test_worldline_id() -> WorldlineId { - WorldlineId([1u8; 32]) + WorldlineId::from_bytes([1u8; 32]) + } + + fn fork_target_worldline_id() -> WorldlineId { + WorldlineId::from_bytes([99u8; 32]) } fn test_warp_id() -> WarpId { @@ -2561,7 +2565,7 @@ mod tests { fn fork_copies_entry_prefix_and_checkpoints() { let mut store = LocalProvenanceStore::new(); let source = test_worldline_id(); - let target = WorldlineId([99u8; 32]); + let target = fork_target_worldline_id(); let initial_state = test_initial_state(test_warp_id()); let root = *initial_state.root(); let initial_boundary = @@ -2611,7 +2615,7 @@ mod tests { fn fork_rewrites_same_worldline_parent_refs_to_target_worldline() { let mut store = LocalProvenanceStore::new(); let source = test_worldline_id(); - let target = WorldlineId([99u8; 32]); + let target = fork_target_worldline_id(); let warp = test_warp_id(); store.register_worldline(source, warp).unwrap(); @@ -2779,7 +2783,7 @@ mod tests { fn service_fork_copies_entry_prefix_and_checkpoints() { let mut service = ProvenanceService::new(); let source = test_worldline_id(); - let target = WorldlineId([99u8; 32]); + let target = fork_target_worldline_id(); let state = WorldlineState::empty(); let root = *state.root(); @@ -3332,11 +3336,11 @@ mod tests { #[test] fn btr_validation_rejects_mixed_worldlines() { let entry = ProvenanceEntry::local_commit( - WorldlineId([2u8; 32]), + WorldlineId::from_bytes([2u8; 32]), wt(0), gt(0), WriterHeadKey { - worldline_id: WorldlineId([2u8; 32]), + worldline_id: WorldlineId::from_bytes([2u8; 32]), head_id: make_head_id("b"), }, Vec::new(), diff --git a/crates/warp-core/src/snapshot.rs b/crates/warp-core/src/snapshot.rs index 46f1a781..9530d17f 100644 --- a/crates/warp-core/src/snapshot.rs +++ b/crates/warp-core/src/snapshot.rs @@ -829,7 +829,7 @@ mod tests { use crate::worldline::WorldlineId; fn make_worldline_id(n: u8) -> WorldlineId { - WorldlineId(make_hash(n)) + WorldlineId::from_bytes(make_hash(n)) } #[test] @@ -1187,7 +1187,7 @@ mod tests { // requires a version bump or migration plan. let schema_hash = [0xABu8; 32]; - let worldline_id = WorldlineId([0xCDu8; 32]); + let worldline_id = WorldlineId::from_bytes([0xCDu8; 32]); let tick = 42u64; let parent = [0x11u8; 32]; let patch_digest = [0x22u8; 32]; diff --git a/crates/warp-core/src/worldline.rs b/crates/warp-core/src/worldline.rs index eba1019d..41c68b02 100644 --- a/crates/warp-core/src/worldline.rs +++ b/crates/warp-core/src/worldline.rs @@ -17,30 +17,14 @@ use thiserror::Error; +pub use echo_runtime_schema::WorldlineId; + use crate::clock::GlobalTick; use crate::ident::{EdgeKey, Hash, NodeKey, WarpId}; use crate::materialization::ChannelId; use crate::tick_patch::{apply_ops_to_state, SlotId, TickPatchError, WarpOp}; use crate::worldline_state::WorldlineState; -/// Unique identifier for a worldline. -/// -/// A worldline ID is typically derived from the initial state hash of the warp, -/// ensuring that worldlines with different starting points have distinct IDs. -#[repr(transparent)] -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct WorldlineId(pub Hash); - -impl WorldlineId { - /// Returns the canonical byte representation of this id. - #[inline] - #[must_use] - pub fn as_bytes(&self) -> &Hash { - &self.0 - } -} - /// Three-way cryptographic commitment for worldline verification. /// /// This triplet commits to the state, the patch that produced it, and the @@ -318,8 +302,8 @@ mod tests { #[test] fn worldline_id_is_transparent_wrapper() { let hash = [42u8; 32]; - let id = WorldlineId(hash); - assert_eq!(id.0, hash); + let id = WorldlineId::from_bytes(hash); + assert_eq!(*id.as_bytes(), hash); assert_eq!(id.as_bytes(), &hash); } diff --git a/crates/warp-core/src/worldline_registry.rs b/crates/warp-core/src/worldline_registry.rs index 18fab5da..5fc14f94 100644 --- a/crates/warp-core/src/worldline_registry.rs +++ b/crates/warp-core/src/worldline_registry.rs @@ -134,7 +134,7 @@ mod tests { use crate::{blake3_empty, TxId}; fn wl(n: u8) -> WorldlineId { - WorldlineId([n; 32]) + WorldlineId::from_bytes([n; 32]) } #[test] diff --git a/crates/warp-core/src/worldline_state.rs b/crates/warp-core/src/worldline_state.rs index 21e3d63c..473ee63b 100644 --- a/crates/warp-core/src/worldline_state.rs +++ b/crates/warp-core/src/worldline_state.rs @@ -457,7 +457,7 @@ mod tests { use crate::warp_state::WarpState; fn wl(n: u8) -> WorldlineId { - WorldlineId([n; 32]) + WorldlineId::from_bytes([n; 32]) } #[test] @@ -669,7 +669,7 @@ mod tests { }); state.tx_counter = 42; let head_key = WriterHeadKey { - worldline_id: WorldlineId([17u8; 32]), + worldline_id: WorldlineId::from_bytes([17u8; 32]), head_id: crate::head::make_head_id("checkpoint"), }; state.record_committed_ingress(head_key, [[18u8; 32]]); @@ -768,7 +768,7 @@ mod tests { }); state.tx_counter = 11; let head_key = WriterHeadKey { - worldline_id: WorldlineId([12u8; 32]), + worldline_id: WorldlineId::from_bytes([12u8; 32]), head_id: crate::head::make_head_id("replay-base"), }; state.record_committed_ingress(head_key, [[13u8; 32]]); diff --git a/crates/warp-core/tests/checkpoint_fork_tests.rs b/crates/warp-core/tests/checkpoint_fork_tests.rs index ca7e24a2..e190a525 100644 --- a/crates/warp-core/tests/checkpoint_fork_tests.rs +++ b/crates/warp-core/tests/checkpoint_fork_tests.rs @@ -26,7 +26,7 @@ use warp_core::{ /// Creates a deterministic worldline ID for the forked worldline. fn forked_worldline_id() -> WorldlineId { - WorldlineId([2u8; 32]) + WorldlineId::from_bytes([2u8; 32]) } fn wt(raw: u64) -> WorldlineTick { diff --git a/crates/warp-core/tests/common/mod.rs b/crates/warp-core/tests/common/mod.rs index bb982236..59d075b0 100644 --- a/crates/warp-core/tests/common/mod.rs +++ b/crates/warp-core/tests/common/mod.rs @@ -739,7 +739,7 @@ pub fn key_sub(scope: u8, rule: u32, subkey: u32) -> EmitKey { /// Creates a deterministic worldline ID for testing. pub fn test_worldline_id() -> WorldlineId { - WorldlineId([1u8; 32]) + WorldlineId::from_bytes([1u8; 32]) } /// Creates a deterministic cursor ID for testing. diff --git a/crates/warp-core/tests/golden_vectors_phase0.rs b/crates/warp-core/tests/golden_vectors_phase0.rs index f415a75d..71c50089 100644 --- a/crates/warp-core/tests/golden_vectors_phase0.rs +++ b/crates/warp-core/tests/golden_vectors_phase0.rs @@ -208,7 +208,7 @@ fn gv003_fork_reproducibility() { ]; let (mut provenance, _initial_store, _warp_id, worldline_id) = setup_worldline_with_ticks(10); - let forked_id = WorldlineId([2u8; 32]); + let forked_id = WorldlineId::from_bytes([2u8; 32]); provenance .fork(worldline_id, wt(5), forked_id) diff --git a/crates/warp-core/tests/inbox.rs b/crates/warp-core/tests/inbox.rs index 2c9fc2b4..c91c13ed 100644 --- a/crates/warp-core/tests/inbox.rs +++ b/crates/warp-core/tests/inbox.rs @@ -12,7 +12,7 @@ use warp_core::{ }; fn wl(n: u8) -> WorldlineId { - WorldlineId([n; 32]) + WorldlineId::from_bytes([n; 32]) } fn wt(raw: u64) -> WorldlineTick { diff --git a/crates/warp-core/tests/invariant_property_tests.rs b/crates/warp-core/tests/invariant_property_tests.rs index 54714c44..40f2972f 100644 --- a/crates/warp-core/tests/invariant_property_tests.rs +++ b/crates/warp-core/tests/invariant_property_tests.rs @@ -182,7 +182,7 @@ proptest! { for w in 0..num_worldlines { for h in 0..num_heads_per { keys.push(WriterHeadKey { - worldline_id: WorldlineId([w as u8; 32]), + worldline_id: WorldlineId::from_bytes([w as u8; 32]), head_id: make_head_id(&format!("h-{h}")), }); } @@ -292,8 +292,8 @@ proptest! { #[test] fn inv004_no_cross_worldline_leakage() { let warp_id = test_warp_id(); - let worldline_a = WorldlineId([1u8; 32]); - let worldline_b = WorldlineId([2u8; 32]); + let worldline_a = WorldlineId::from_bytes([1u8; 32]); + let worldline_b = WorldlineId::from_bytes([2u8; 32]); let initial_state = create_initial_worldline_state(warp_id); let mut provenance = LocalProvenanceStore::new(); diff --git a/crates/warp-core/tests/outputs_playback_tests.rs b/crates/warp-core/tests/outputs_playback_tests.rs index ec8d76bf..d31d0ab0 100644 --- a/crates/warp-core/tests/outputs_playback_tests.rs +++ b/crates/warp-core/tests/outputs_playback_tests.rs @@ -500,7 +500,7 @@ fn truth_frames_encode_to_mbus_v2() { let v2_header = V2PacketHeader { session_id: frame.cursor.session_id.0, cursor_id: frame.cursor.cursor_id.0, - worldline_id: frame.cursor.worldline_id.0, + worldline_id: *frame.cursor.worldline_id.as_bytes(), warp_id: frame.cursor.warp_id, tick: frame.cursor.worldline_tick.as_u64(), commit_hash: frame.cursor.commit_hash, @@ -525,7 +525,7 @@ fn truth_frames_encode_to_mbus_v2() { // Verify roundtrip assert_eq!(decoded.header.session_id, session_id.0); assert_eq!(decoded.header.cursor_id, cursor.cursor_id.0); - assert_eq!(decoded.header.worldline_id, worldline_id.0); + assert_eq!(decoded.header.worldline_id, *worldline_id.as_bytes()); assert_eq!(decoded.header.warp_id, warp_id); assert_eq!(decoded.header.tick, 7); assert_eq!( diff --git a/crates/warp-core/tests/slice_theorem_proof.rs b/crates/warp-core/tests/slice_theorem_proof.rs index 6f8c5a7b..ad6faccb 100644 --- a/crates/warp-core/tests/slice_theorem_proof.rs +++ b/crates/warp-core/tests/slice_theorem_proof.rs @@ -530,7 +530,7 @@ fn phase_2_and_3_playback_replay_matches_execution() { // Build provenance store from execution. // IMPORTANT: We compute state_root from the same replay substrate as the cursor: // full WorldlineState materialization, not engine-specific incidental state. - let worldline_id = WorldlineId([0x42; 32]); + let worldline_id = WorldlineId::from_bytes([0x42; 32]); let cursor_id = CursorId([0x01; 32]); let replay_base = WorldlineState::from_root_store(store, root).expect("replay base"); let mut provenance = LocalProvenanceStore::new(); @@ -789,7 +789,7 @@ fn phase_6_semantic_correctness_dependent_chain() { // Replay: build provenance with the tick-2 patch, seek, verify same semantic result. // We only store the second patch since that's the one producing the G attachment. // The initial store for the cursor is the store AFTER tick 1 (R1's write is committed). - let worldline_id = WorldlineId([0x66; 32]); + let worldline_id = WorldlineId::from_bytes([0x66; 32]); let cursor_id = CursorId([0x77; 32]); let mut provenance = LocalProvenanceStore::new(); diff --git a/crates/warp-wasm/src/lib.rs b/crates/warp-wasm/src/lib.rs index 44bbe905..45c8f45a 100644 --- a/crates/warp-wasm/src/lib.rs +++ b/crates/warp-wasm/src/lib.rs @@ -526,7 +526,7 @@ mod init_tests { DispatchResponse, GlobalTick, HeadInfo, HeadObservation, ObservationArtifact, ObservationAt, ObservationFrame, ObservationPayload, ObservationProjection, RegistryInfo, ResolvedObservationCoordinate, RunCompletion, RunId, SchedulerMode, SchedulerState, - SchedulerStatus, WorkState, WorldlineTick, ABI_VERSION, + SchedulerStatus, WorkState, WorldlineId, WorldlineTick, ABI_VERSION, }; struct StubKernel; @@ -567,7 +567,7 @@ mod init_tests { Ok(ObservationArtifact { resolved: ResolvedObservationCoordinate { observation_version: 2, - worldline_id: vec![9; 32], + worldline_id: WorldlineId::from_bytes([9; 32]), requested_at: ObservationAt::Frontier, resolved_worldline_tick: head.worldline_tick, commit_global_tick: head.commit_global_tick, diff --git a/crates/warp-wasm/src/warp_kernel.rs b/crates/warp-wasm/src/warp_kernel.rs index ef755111..e4536200 100644 --- a/crates/warp-wasm/src/warp_kernel.rs +++ b/crates/warp-wasm/src/warp_kernel.rs @@ -11,11 +11,12 @@ use std::fmt; use echo_wasm_abi::kernel_port::{ error_codes, AbiError, ControlIntentV1, DispatchResponse, GlobalTick as AbiGlobalTick, - HeadEligibility as AbiHeadEligibility, HeadInfo, HeadKey as AbiHeadKey, KernelPort, + HeadEligibility as AbiHeadEligibility, HeadId as AbiHeadId, HeadInfo, KernelPort, ObservationArtifact as AbiObservationArtifact, ObservationFrame as AbiObservationFrame, ObservationProjection as AbiObservationProjection, ObservationRequest as AbiObservationRequest, RegistryInfo, RunCompletion, RunId as AbiRunId, SchedulerMode, SchedulerState, SchedulerStatus, - WorkState, WorldlineTick as AbiWorldlineTick, ABI_VERSION, + WorkState, WorldlineId as AbiWorldlineId, WorldlineTick as AbiWorldlineTick, + WriterHeadKey as AbiWriterHeadKey, ABI_VERSION, }; use echo_wasm_abi::{unpack_control_intent_v1, unpack_intent_v1, CONTROL_INTENT_V1_OP_ID}; use warp_core::{ @@ -129,7 +130,7 @@ impl WarpKernel { return Err(KernelInitError::NonFreshEngine); } let root = engine.root_key(); - let default_worldline = WorldlineId(root.warp_id.0); + let default_worldline = WorldlineId::from_bytes(root.warp_id.0); let mut runtime = WorldlineRuntime::new(); let default_state = WorldlineState::try_from(engine.state().clone())?; let mut provenance = ProvenanceService::new(); @@ -166,12 +167,12 @@ impl WarpKernel { }) } - fn parse_worldline_id(bytes: &[u8]) -> Result { - let hash: [u8; 32] = bytes.try_into().map_err(|_| AbiError { - code: error_codes::INVALID_WORLDLINE, - message: format!("worldline id must be exactly 32 bytes, got {}", bytes.len()), - })?; - Ok(WorldlineId(hash)) + fn to_core_worldline_id(worldline_id: &AbiWorldlineId) -> WorldlineId { + WorldlineId::from_bytes(*worldline_id.as_bytes()) + } + + fn to_core_head_id(head_id: &AbiHeadId) -> HeadId { + HeadId::from_bytes(*head_id.as_bytes()) } fn parse_channel_ids( @@ -229,7 +230,7 @@ impl WarpKernel { } fn to_core_request(request: AbiObservationRequest) -> Result { - let worldline_id = Self::parse_worldline_id(&request.coordinate.worldline_id)?; + let worldline_id = Self::to_core_worldline_id(&request.coordinate.worldline_id); let at = match request.coordinate.at { echo_wasm_abi::kernel_port::ObservationAt::Frontier => ObservationAt::Frontier, echo_wasm_abi::kernel_port::ObservationAt::Tick { worldline_tick } => { @@ -435,7 +436,7 @@ impl WarpKernel { self.clear_active_run_state(false); } ControlIntentV1::SetHeadEligibility { head, eligibility } => { - let key = Self::parse_head_key(&head)?; + let key = Self::to_core_head_key(&head); let eligibility = match eligibility { AbiHeadEligibility::Dormant => HeadEligibility::Dormant, AbiHeadEligibility::Admitted => HeadEligibility::Admitted, @@ -456,19 +457,11 @@ impl WarpKernel { Ok(()) } - fn parse_head_key(head: &AbiHeadKey) -> Result { - let worldline_id = Self::parse_worldline_id(&head.worldline_id)?; - let head_id_bytes: [u8; 32] = head.head_id.as_slice().try_into().map_err(|_| AbiError { - code: error_codes::INVALID_CONTROL, - message: format!( - "head id must be exactly 32 bytes, got {}", - head.head_id.len() - ), - })?; - Ok(WriterHeadKey { - worldline_id, - head_id: HeadId::from_bytes(head_id_bytes), - }) + fn to_core_head_key(head: &AbiWriterHeadKey) -> WriterHeadKey { + WriterHeadKey { + worldline_id: Self::to_core_worldline_id(&head.worldline_id), + head_id: Self::to_core_head_id(&head.head_id), + } } } @@ -563,12 +556,13 @@ mod tests { use echo_wasm_abi::{ kernel_port::{ ControlIntentV1, GlobalTick as AbiGlobalTick, HeadEligibility as AbiHeadEligibility, - HeadKey as AbiHeadKey, ObservationAt as AbiObservationAt, + HeadId as AbiHeadId, ObservationAt as AbiObservationAt, ObservationCoordinate as AbiObservationCoordinate, ObservationFrame as AbiObservationFrame, ObservationPayload as AbiObservationPayload, ObservationProjection as AbiObservationProjection, ObservationRequest as AbiObservationRequest, RunCompletion, SchedulerMode, - SchedulerState, WorkState, WorldlineTick as AbiWorldlineTick, + SchedulerState, WorkState, WorldlineId as AbiWorldlineId, + WorldlineTick as AbiWorldlineTick, WriterHeadKey as AbiWriterHeadKey, }, pack_control_intent_v1, pack_intent_v1, }; @@ -592,6 +586,14 @@ mod tests { kernel.dispatch_intent(&control) } + fn abi_worldline_id(worldline_id: WorldlineId) -> AbiWorldlineId { + AbiWorldlineId::from_bytes(*worldline_id.as_bytes()) + } + + fn abi_head_id(head_id: HeadId) -> AbiHeadId { + AbiHeadId::from_bytes(*head_id.as_bytes()) + } + #[test] fn new_kernel_has_zero_tick() { let kernel = WarpKernel::new().unwrap(); @@ -703,9 +705,9 @@ mod tests { fn set_head_eligibility_rejects_unknown_head_as_invalid_control() { let mut kernel = WarpKernel::new().unwrap(); let control = pack_control_intent_v1(&ControlIntentV1::SetHeadEligibility { - head: AbiHeadKey { - worldline_id: kernel.default_worldline.0.to_vec(), - head_id: make_head_id("missing").as_bytes().to_vec(), + head: AbiWriterHeadKey { + worldline_id: abi_worldline_id(kernel.default_worldline), + head_id: abi_head_id(make_head_id("missing")), }, eligibility: AbiHeadEligibility::Dormant, }) @@ -761,9 +763,9 @@ mod tests { kernel.dispatch_intent(&intent).unwrap(); let dormancy = pack_control_intent_v1(&ControlIntentV1::SetHeadEligibility { - head: AbiHeadKey { - worldline_id: kernel.default_worldline.0.to_vec(), - head_id: make_head_id("default").as_bytes().to_vec(), + head: AbiWriterHeadKey { + worldline_id: abi_worldline_id(kernel.default_worldline), + head_id: abi_head_id(make_head_id("default")), }, eligibility: AbiHeadEligibility::Dormant, }) @@ -850,7 +852,7 @@ mod tests { let err = kernel .observe(AbiObservationRequest { coordinate: AbiObservationCoordinate { - worldline_id: kernel.default_worldline.0.to_vec(), + worldline_id: abi_worldline_id(kernel.default_worldline), at: AbiObservationAt::Tick { worldline_tick: AbiWorldlineTick(999), }, @@ -871,7 +873,7 @@ mod tests { let artifact = kernel .observe(AbiObservationRequest { coordinate: AbiObservationCoordinate { - worldline_id: kernel.default_worldline.0.to_vec(), + worldline_id: abi_worldline_id(kernel.default_worldline), at: AbiObservationAt::Tick { worldline_tick: AbiWorldlineTick(0), }, @@ -896,7 +898,7 @@ mod tests { let artifact = kernel .observe(AbiObservationRequest { coordinate: AbiObservationCoordinate { - worldline_id: kernel.default_worldline.0.to_vec(), + worldline_id: abi_worldline_id(kernel.default_worldline), at: AbiObservationAt::Frontier, }, frame: AbiObservationFrame::CommitBoundary, @@ -920,7 +922,7 @@ mod tests { let artifact = kernel .observe(AbiObservationRequest { coordinate: AbiObservationCoordinate { - worldline_id: kernel.default_worldline.0.to_vec(), + worldline_id: abi_worldline_id(kernel.default_worldline), at: AbiObservationAt::Frontier, }, frame: AbiObservationFrame::CommitBoundary, @@ -950,7 +952,7 @@ mod tests { let _ = kernel .observe(AbiObservationRequest { coordinate: AbiObservationCoordinate { - worldline_id: kernel.default_worldline.0.to_vec(), + worldline_id: abi_worldline_id(kernel.default_worldline), at: AbiObservationAt::Frontier, }, frame: AbiObservationFrame::RecordedTruth, @@ -1000,7 +1002,7 @@ mod tests { let artifact = kernel .observe(AbiObservationRequest { coordinate: AbiObservationCoordinate { - worldline_id: kernel.default_worldline.0.to_vec(), + worldline_id: abi_worldline_id(kernel.default_worldline), at: AbiObservationAt::Tick { worldline_tick: AbiWorldlineTick(1), }, diff --git a/docs/ROADMAP/backlog/README.md b/docs/ROADMAP/backlog/README.md index 4cd93952..ddc1f06b 100644 --- a/docs/ROADMAP/backlog/README.md +++ b/docs/ROADMAP/backlog/README.md @@ -3,7 +3,7 @@ # Backlog -> **Priority:** Unscheduled | **Est:** ~219h +> **Priority:** Unscheduled | **Est:** ~224h Unscheduled work across all projects. Items here have no committed timeline and can be picked up opportunistically. git-mind NEXUS (formerly its own milestone) has been demoted here because it runs independently of Echo's critical path. @@ -19,7 +19,7 @@ Unscheduled work across all projects. Items here have no committed timeline and | Importer | [importer.md](importer.md) | ~2h | Not Started | | Deterministic Rhai | [deterministic-rhai.md](deterministic-rhai.md) | ~11h | Not Started | | Wesley Boundary Grammar | [wesley-boundary-grammar.md](wesley-boundary-grammar.md) | ~20h | Not Started | -| Tooling & Misc | [tooling-misc.md](tooling-misc.md) | ~33h | Not Started | +| Tooling & Misc | [tooling-misc.md](tooling-misc.md) | ~38h | Not Started | | Wesley Future | [wesley-future.md](wesley-future.md) | ~12h | Not Started | | Wesley Docs | [wesley-docs.md](wesley-docs.md) | ~10h | Not Started | | TTD Hardening | [ttd-hardening.md](ttd-hardening.md) | ~19h | Not Started | diff --git a/docs/ROADMAP/backlog/tooling-misc.md b/docs/ROADMAP/backlog/tooling-misc.md index 6d77477b..8bf934da 100644 --- a/docs/ROADMAP/backlog/tooling-misc.md +++ b/docs/ROADMAP/backlog/tooling-misc.md @@ -391,3 +391,103 @@ Housekeeping tasks: documentation, logging, naming consistency, and debugger UX **Est. Hours:** 4h **Expected Complexity:** ~120 LoC (scripts + docs + timing assertions) + +--- + +## T-10-8-10: Feature-Gate Contract Verification + +**User Story:** As a contributor, I want explicit feature-contract checks for +no-std / alloc-only crates so that feature-gating regressions are caught before +PR review or CI. + +**Requirements:** + +- R1: Identify crates whose manifests promise meaningful `--no-default-features` + or alloc-only support +- R2: Add a local and CI-visible verification path that exercises those feature + contracts directly +- R3: Keep the lane scoped so it stays fast enough to run during review-fix + loops +- R4: Document which crates are covered and what the lane is proving + +**Acceptance Criteria:** + +- [ ] AC1: At least the current runtime-schema / ABI crates have an explicit + `--no-default-features` check +- [ ] AC2: A deliberate `std` leak in a gated crate fails the lane +- [ ] AC3: Contributor docs explain when to run the lane and what it covers +- [ ] AC4: The covered crate list is easy to keep aligned with manifest truth + +**Definition of Done:** + +- [ ] Code reviewed and merged +- [ ] Tests pass (CI green) +- [ ] Documentation updated (if applicable) + +**Scope:** Feature-gate verification for crates that claim no-std or alloc-only +support. +**Out of Scope:** Broad workspace-wide no-std support or changing crate feature +semantics. + +**Test Plan:** + +- **Goldens:** n/a +- **Failures:** Intentionally introduce a `std` dependency in a gated path and + verify the lane fails +- **Edges:** `default-features = false`, alloc-only mode, transitive feature + forwarding +- **Fuzz/Stress:** n/a + +**Blocked By:** none +**Blocking:** none + +**Est. Hours:** 2h +**Expected Complexity:** ~60 LoC (lane wiring + docs) + +--- + +## T-10-8-11: PR Review Thread Resolution Helper + +**User Story:** As a reviewer, I want a safe helper for resolving already-fixed +PR review threads so that GitHub thread state does not lag behind the branch +state after review-fix pushes. + +**Requirements:** + +- R1: Enumerate unresolved review threads for a PR with pagination +- R2: Support resolving selected or all unresolved threads after a verified push +- R3: Keep the helper explicit and human-driven; it must not auto-resolve based + on heuristics alone +- R4: Show enough context (path, author, URL) for a reviewer to confirm the + action before mutating GitHub state + +**Acceptance Criteria:** + +- [ ] AC1: One command can list unresolved review threads with exact counts +- [ ] AC2: One command can resolve chosen thread ids after human confirmation +- [ ] AC3: The helper works with the existing `gh`-based workflow +- [ ] AC4: Contributor docs explain when to use it and when to reply manually + +**Definition of Done:** + +- [ ] Code reviewed and merged +- [ ] Tests pass (CI green) +- [ ] Documentation updated (if applicable) + +**Scope:** Local tooling for review-thread listing and explicit resolution. +**Out of Scope:** Auto-reply generation, auto-merging, or policy decisions about +which comments deserve direct replies. + +**Test Plan:** + +- **Goldens:** n/a +- **Failures:** Bad PR number, missing `gh` auth, invalid thread id +- **Edges:** More than 100 review threads, mixed resolved/unresolved state, + outdated but unresolved threads +- **Fuzz/Stress:** n/a + +**Blocked By:** none +**Blocking:** none + +**Est. Hours:** 3h +**Expected Complexity:** ~90 LoC (script + docs) diff --git a/docs/assets/dags/tasks-dag.dot b/docs/assets/dags/tasks-dag.dot index 57433c45..d039c509 100644 --- a/docs/assets/dags/tasks-dag.dot +++ b/docs/assets/dags/tasks-dag.dot @@ -2,194 +2,163 @@ digraph tasks_dag { graph [rankdir=LR, labelloc="t", fontsize=18, fontname="Helvetica", newrank=true, splines=true]; node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10, margin="0.10,0.06"]; edge [fontname="Helvetica", fontsize=9, arrowsize=0.8]; - label="Echo — Tasks DAG (from TASKS-DAG.md) -Generated by scripts/generate-tasks-dag.js"; + label="Echo — Tasks DAG (from docs/archive/tasks/TASKS-DAG.md)\nGenerated by scripts/generate-tasks-dag.js"; subgraph cluster_legend { label="Legend"; color="gray70"; fontcolor="gray30"; style="rounded"; - LG [label="confirmed in issue body", color="green", fontcolor="green"]; + LG [label="confirmed in docs/archive/tasks/TASKS-DAG.md", color="green", fontcolor="green"]; } subgraph cluster_Spec { label="Spec"; style="rounded"; color="gray70"; - node [fillcolor="#f3f4f6"]; - i19 [label="#19 -Spec: Persistent Store (on-disk)", URL="https://github.com/flyingrobots/echo/issues/19", tooltip="Spec: Persistent Store (on-disk)"]; - i20 [label="#20 -Spec: Commit/Manifest Signing", URL="https://github.com/flyingrobots/echo/issues/20", tooltip="Spec: Commit/Manifest Signing"]; - i21 [label="#21 -Spec: Security Contexts (FFI/WASM/CLI)", URL="https://github.com/flyingrobots/echo/issues/21", tooltip="Spec: Security Contexts (FFI/WASM/CLI)"]; + node [fillcolor="#dbeafe"]; + i19 [label="#19 Spec: Persistent Store\\n(on-disk)", URL="https://github.com/flyingrobots/echo/issues/19", tooltip="Spec: Persistent Store (on-disk)"]; + i20 [label="#20 Spec: Commit/Manifest\\nSigning", URL="https://github.com/flyingrobots/echo/issues/20", tooltip="Spec: Commit/Manifest Signing"]; + i21 [label="#21 Spec: Security Contexts\\n(WASM/CLI)", URL="https://github.com/flyingrobots/echo/issues/21", tooltip="Spec: Security Contexts (WASM/CLI)"]; } subgraph cluster_Draft { label="Draft"; style="rounded"; color="gray70"; - node [fillcolor="#dcfce7"]; - i28 [label="#28 -Draft spec document (header/ULEB128/property/string-pool)", URL="https://github.com/flyingrobots/echo/issues/28", tooltip="Draft spec document (header/ULEB128/property/string-pool)"]; - i32 [label="#32 -Draft signing spec", URL="https://github.com/flyingrobots/echo/issues/32", tooltip="Draft signing spec"]; - i37 [label="#37 -Draft security contexts spec", URL="https://github.com/flyingrobots/echo/issues/37", tooltip="Draft security contexts spec"]; + node [fillcolor="#dbeafe"]; + i28 [label="#28 Draft spec document\\n(header/ULEB128/property/strin\\ng-pool)", URL="https://github.com/flyingrobots/echo/issues/28", tooltip="Draft spec document (header/ULEB128/property/string-pool)"]; + i32 [label="#32 Draft signing spec", URL="https://github.com/flyingrobots/echo/issues/32", tooltip="Draft signing spec"]; + i37 [label="#37 Draft security contexts\\nspec", URL="https://github.com/flyingrobots/echo/issues/37", tooltip="Draft security contexts spec"]; } subgraph cluster_Misc { label="Misc"; style="rounded"; color="gray70"; - node [fillcolor="#fef9c3"]; - i33 [label="#33 -CI: sign release artifacts (dry run)", URL="https://github.com/flyingrobots/echo/issues/33", tooltip="CI: sign release artifacts (dry run)"]; - i34 [label="#34 -CLI verify path", URL="https://github.com/flyingrobots/echo/issues/34", tooltip="CLI verify path"]; - i35 [label="#35 -Key management doc", URL="https://github.com/flyingrobots/echo/issues/35", tooltip="Key management doc"]; - i36 [label="#36 -CI: verify signatures", URL="https://github.com/flyingrobots/echo/issues/36", tooltip="CI: verify signatures"]; - i38 [label="#38 -FFI limits and validation", URL="https://github.com/flyingrobots/echo/issues/38", tooltip="FFI limits and validation"]; - i39 [label="#39 -WASM input validation", URL="https://github.com/flyingrobots/echo/issues/39", tooltip="WASM input validation"]; - i40 [label="#40 -Unit tests for denials", URL="https://github.com/flyingrobots/echo/issues/40", tooltip="Unit tests for denials"]; + node [fillcolor="#dcfce7"]; + i33 [label="#33 CI: sign release artifacts\\n(dry run)", URL="https://github.com/flyingrobots/echo/issues/33", tooltip="CI: sign release artifacts (dry run)"]; + i34 [label="#34 CLI verify path", URL="https://github.com/flyingrobots/echo/issues/34", tooltip="CLI verify path"]; + i35 [label="#35 Key management doc", URL="https://github.com/flyingrobots/echo/issues/35", tooltip="Key management doc"]; + i36 [label="#36 CI: verify signatures", URL="https://github.com/flyingrobots/echo/issues/36", tooltip="CI: verify signatures"]; + i39 [label="#39 WASM input validation", URL="https://github.com/flyingrobots/echo/issues/39", tooltip="WASM input validation"]; + i40 [label="#40 Unit tests for denials", URL="https://github.com/flyingrobots/echo/issues/40", tooltip="Unit tests for denials"]; + i38 [label="#38 FFI limits and validation", URL="https://github.com/flyingrobots/echo/issues/38", tooltip="FFI limits and validation"]; + i202 [label="#202 Provenance Payload (PP)\\nv1 — spec + implementation", URL="https://github.com/flyingrobots/echo/issues/202", tooltip="Provenance Payload (PP) v1 — spec + implementation"]; + i270 [label="#270 Hardening: Fuzz the\\nScenePort boundary (proptest)", URL="https://github.com/flyingrobots/echo/issues/270", tooltip="Hardening: Fuzz the ScenePort boundary (proptest)"]; + i286 [label="#286 CI: Add unit tests for\\nclassify_changes.cjs and\\nmatches()", URL="https://github.com/flyingrobots/echo/issues/286", tooltip="CI: Add unit tests for classify_changes.cjs and matches()"]; + i287 [label="#287 Docs: Document\\nban-nondeterminism.sh\\nallowlist process in\\nRELEASE_POLICY.md", URL="https://github.com/flyingrobots/echo/issues/287", tooltip="Docs: Document ban-nondeterminism.sh allowlist process in RELEASE_POLICY.md"]; } subgraph cluster_TT1 { label="TT1"; style="rounded"; color="gray70"; - node [fillcolor="#dcfce7"]; - i170 [label="#170 -TT1: StreamsFrame inspector support\n(backlog + cursors + admission decisions)", URL="https://github.com/flyingrobots/echo/issues/170", tooltip="TT1: StreamsFrame inspector support (backlog + cursors + admission decisions)"]; - i246 [label="#246 -TT1: Security/capabilities for\nfork/rewind/merge in multiplayer", URL="https://github.com/flyingrobots/echo/issues/246", tooltip="TT1: Security/capabilities for fork/rewind/merge in multiplayer"]; - i245 [label="#245 -TT1: Merge semantics for admitted\nstream facts across worldlines", URL="https://github.com/flyingrobots/echo/issues/245", tooltip="TT1: Merge semantics for admitted stream facts across worldlines"]; - i244 [label="#244 -TT1: TimeStream retention + spool\ncompaction + wormhole density", URL="https://github.com/flyingrobots/echo/issues/244", tooltip="TT1: TimeStream retention + spool compaction + wormhole density"]; - i243 [label="#243 -TT1: dt policy (fixed timestep\nvs admitted dt stream)", URL="https://github.com/flyingrobots/echo/issues/243", tooltip="TT1: dt policy (fixed timestep vs admitted dt stream)"]; + node [fillcolor="#fef9c3"]; + i170 [label="#170 TT1: StreamsFrame\\ninspector support (backlog +\\ncursors + admission decisions)", URL="https://github.com/flyingrobots/echo/issues/170", tooltip="TT1: StreamsFrame inspector support (backlog + cursors + admission decisions)"]; + i246 [label="#246 TT1:\\nSecurity/capabilities for\\nfork/rewind/merge in\\nmultiplayer", URL="https://github.com/flyingrobots/echo/issues/246", tooltip="TT1: Security/capabilities for fork/rewind/merge in multiplayer"]; + i245 [label="#245 TT1: Merge semantics for\\nadmitted stream facts across\\nworldlines", URL="https://github.com/flyingrobots/echo/issues/245", tooltip="TT1: Merge semantics for admitted stream facts across worldlines"]; + i244 [label="#244 TT1: TimeStream retention\\n+ spool compaction + wormhole\\ndensity", URL="https://github.com/flyingrobots/echo/issues/244", tooltip="TT1: TimeStream retention + spool compaction + wormhole density"]; + i243 [label="#243 TT1: dt policy (fixed\\ntimestep vs admitted dt\\nstream)", URL="https://github.com/flyingrobots/echo/issues/243", tooltip="TT1: dt policy (fixed timestep vs admitted dt stream)"]; } subgraph cluster_TT2 { label="TT2"; style="rounded"; color="gray70"; - node [fillcolor="#ffedd5"]; - i171 [label="#171 -TT2: Time Travel MVP (pause/rewind/buffer/catch-up)", URL="https://github.com/flyingrobots/echo/issues/171", tooltip="TT2: Time Travel MVP (pause/rewind/buffer/catch-up)"]; - i205 [label="#205 -TT2: Reliving debugger MVP (scrub\ntimeline + causal slice + fork branch)", URL="https://github.com/flyingrobots/echo/issues/205", tooltip="TT2: Reliving debugger MVP (scrub timeline + causal slice + fork branch)"]; + node [fillcolor="#fee2e2"]; + i171 [label="#171 TT2: Time Travel MVP\\n(pause/rewind/buffer/catch-up)", URL="https://github.com/flyingrobots/echo/issues/171", tooltip="TT2: Time Travel MVP (pause/rewind/buffer/catch-up)"]; + i205 [label="#205 TT2: Reliving debugger\\nMVP (scrub timeline + causal\\nslice + fork branch)", URL="https://github.com/flyingrobots/echo/issues/205", tooltip="TT2: Reliving debugger MVP (scrub timeline + causal slice + fork branch)"]; } subgraph cluster_TT3 { label="TT3"; style="rounded"; color="gray70"; - node [fillcolor="#f3f4f6"]; - i172 [label="#172 -TT3: Rulial diff / worldline compare MVP", URL="https://github.com/flyingrobots/echo/issues/172", tooltip="TT3: Rulial diff / worldline compare MVP"]; - i204 [label="#204 -TT3: Provenance heatmap (blast\nradius / cohesion over time)", URL="https://github.com/flyingrobots/echo/issues/204", tooltip="TT3: Provenance heatmap (blast radius / cohesion over time)"]; - i199 [label="#199 -TT3: Wesley worldline diff (compare\nquery outputs/proofs across ticks)", URL="https://github.com/flyingrobots/echo/issues/199", tooltip="TT3: Wesley worldline diff (compare query outputs/proofs across ticks)"]; + node [fillcolor="#ccfbf1"]; + i172 [label="#172 TT3: Rulial diff /\\nworldline compare MVP", URL="https://github.com/flyingrobots/echo/issues/172", tooltip="TT3: Rulial diff / worldline compare MVP"]; + i204 [label="#204 TT3: Provenance heatmap\\n(blast radius / cohesion over\\ntime)", URL="https://github.com/flyingrobots/echo/issues/204", tooltip="TT3: Provenance heatmap (blast radius / cohesion over time)"]; + i199 [label="#199 TT3: Wesley worldline\\ndiff (compare query\\noutputs/proofs across ticks)", URL="https://github.com/flyingrobots/echo/issues/199", tooltip="TT3: Wesley worldline diff (compare query outputs/proofs across ticks)"]; } subgraph cluster_Demo_2 { label="Demo 2"; style="rounded"; color="gray70"; - node [fillcolor="#fee2e2"]; - i222 [label="#222 -Demo 2: Splash Guy — deterministic\nrules + state model", URL="https://github.com/flyingrobots/echo/issues/222", tooltip="Demo 2: Splash Guy — deterministic rules + state model"]; - i226 [label="#226 -Demo 2: Splash Guy — docs: networking-first\ncourse modules", URL="https://github.com/flyingrobots/echo/issues/226", tooltip="Demo 2: Splash Guy — docs: networking-first course modules"]; - i223 [label="#223 -Demo 2: Splash Guy — lockstep input\nprotocol + two-peer harness", URL="https://github.com/flyingrobots/echo/issues/223", tooltip="Demo 2: Splash Guy — lockstep input protocol + two-peer harness"]; - i224 [label="#224 -Demo 2: Splash Guy — controlled\ndesync lessons (make it fail on purpose)", URL="https://github.com/flyingrobots/echo/issues/224", tooltip="Demo 2: Splash Guy — controlled desync lessons (make it fail on purpose)"]; - i225 [label="#225 -Demo 2: Splash Guy — minimal rendering\n/ visualization path", URL="https://github.com/flyingrobots/echo/issues/225", tooltip="Demo 2: Splash Guy — minimal rendering / visualization path"]; + node [fillcolor="#fef9c3"]; + i222 [label="#222 Demo 2: Splash Guy —\\ndeterministic rules + state\\nmodel", URL="https://github.com/flyingrobots/echo/issues/222", tooltip="Demo 2: Splash Guy — deterministic rules + state model"]; + i226 [label="#226 Demo 2: Splash Guy —\\ndocs: networking-first course\\nmodules", URL="https://github.com/flyingrobots/echo/issues/226", tooltip="Demo 2: Splash Guy — docs: networking-first course modules"]; + i223 [label="#223 Demo 2: Splash Guy —\\nlockstep input protocol +\\ntwo-peer harness", URL="https://github.com/flyingrobots/echo/issues/223", tooltip="Demo 2: Splash Guy — lockstep input protocol + two-peer harness"]; + i224 [label="#224 Demo 2: Splash Guy —\\ncontrolled desync lessons\\n(make it fail on purpose)", URL="https://github.com/flyingrobots/echo/issues/224", tooltip="Demo 2: Splash Guy — controlled desync lessons (make it fail on purpose)"]; + i225 [label="#225 Demo 2: Splash Guy —\\nminimal rendering /\\nvisualization path", URL="https://github.com/flyingrobots/echo/issues/225", tooltip="Demo 2: Splash Guy — minimal rendering / visualization path"]; } subgraph cluster_Demo_3 { label="Demo 3"; style="rounded"; color="gray70"; - node [fillcolor="#dbeafe"]; - i231 [label="#231 -Demo 3: Tumble Tower — Stage 0\nphysics (2D AABB stacking)", URL="https://github.com/flyingrobots/echo/issues/231", tooltip="Demo 3: Tumble Tower — Stage 0 physics (2D AABB stacking)"]; - i238 [label="#238 -Demo 3: Tumble Tower — docs course\n(physics ladder)", URL="https://github.com/flyingrobots/echo/issues/238", tooltip="Demo 3: Tumble Tower — docs course (physics ladder)"]; - i232 [label="#232 -Demo 3: Tumble Tower — Stage 1\nphysics (rotation + angular, OBB contacts)", URL="https://github.com/flyingrobots/echo/issues/232", tooltip="Demo 3: Tumble Tower — Stage 1 physics (rotation + angular, OBB contacts)"]; - i233 [label="#233 -Demo 3: Tumble Tower — Stage 2\nphysics (friction + restitution)", URL="https://github.com/flyingrobots/echo/issues/233", tooltip="Demo 3: Tumble Tower — Stage 2 physics (friction + restitution)"]; - i234 [label="#234 -Demo 3: Tumble Tower — Stage 3\nphysics (sleeping + stack stability)", URL="https://github.com/flyingrobots/echo/issues/234", tooltip="Demo 3: Tumble Tower — Stage 3 physics (sleeping + stack stability)"]; - i235 [label="#235 -Demo 3: Tumble Tower — lockstep\nharness + per-tick fingerprinting", URL="https://github.com/flyingrobots/echo/issues/235", tooltip="Demo 3: Tumble Tower — lockstep harness + per-tick fingerprinting"]; - i236 [label="#236 -Demo 3: Tumble Tower — controlled\ndesync breakers (physics edition)", URL="https://github.com/flyingrobots/echo/issues/236", tooltip="Demo 3: Tumble Tower — controlled desync breakers (physics edition)"]; - i237 [label="#237 -Demo 3: Tumble Tower — visualization\n(2D view + debug overlays)", URL="https://github.com/flyingrobots/echo/issues/237", tooltip="Demo 3: Tumble Tower — visualization (2D view + debug overlays)"]; + node [fillcolor="#f3f4f6"]; + i231 [label="#231 Demo 3: Tumble Tower —\\nStage 0 physics (2D AABB\\nstacking)", URL="https://github.com/flyingrobots/echo/issues/231", tooltip="Demo 3: Tumble Tower — Stage 0 physics (2D AABB stacking)"]; + i238 [label="#238 Demo 3: Tumble Tower —\\ndocs course (physics ladder)", URL="https://github.com/flyingrobots/echo/issues/238", tooltip="Demo 3: Tumble Tower — docs course (physics ladder)"]; + i232 [label="#232 Demo 3: Tumble Tower —\\nStage 1 physics (rotation +\\nangular, OBB contacts)", URL="https://github.com/flyingrobots/echo/issues/232", tooltip="Demo 3: Tumble Tower — Stage 1 physics (rotation + angular, OBB contacts)"]; + i233 [label="#233 Demo 3: Tumble Tower —\\nStage 2 physics (friction +\\nrestitution)", URL="https://github.com/flyingrobots/echo/issues/233", tooltip="Demo 3: Tumble Tower — Stage 2 physics (friction + restitution)"]; + i234 [label="#234 Demo 3: Tumble Tower —\\nStage 3 physics (sleeping +\\nstack stability)", URL="https://github.com/flyingrobots/echo/issues/234", tooltip="Demo 3: Tumble Tower — Stage 3 physics (sleeping + stack stability)"]; + i235 [label="#235 Demo 3: Tumble Tower —\\nlockstep harness + per-tick\\nfingerprinting", URL="https://github.com/flyingrobots/echo/issues/235", tooltip="Demo 3: Tumble Tower — lockstep harness + per-tick fingerprinting"]; + i236 [label="#236 Demo 3: Tumble Tower —\\ncontrolled desync breakers\\n(physics edition)", URL="https://github.com/flyingrobots/echo/issues/236", tooltip="Demo 3: Tumble Tower — controlled desync breakers (physics edition)"]; + i237 [label="#237 Demo 3: Tumble Tower —\\nvisualization (2D view + debug\\noverlays)", URL="https://github.com/flyingrobots/echo/issues/237", tooltip="Demo 3: Tumble Tower — visualization (2D view + debug overlays)"]; } - i28 -> i19 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; - i32 -> i20 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; - i33 -> i20 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i34 -> i20 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i35 -> i20 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i36 -> i20 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i37 -> i21 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; - i38 -> i21 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i39 -> i21 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i40 -> i21 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i28 -> i19 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; - i32 -> i20 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; - i33 -> i20 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i34 -> i20 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i35 -> i20 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i36 -> i20 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i37 -> i21 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; - i38 -> i21 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i39 -> i21 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i40 -> i21 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; - i170 -> i171 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT2 task depends on TT1 Inspector scaffolding"]; - i170 -> i205 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT2 task depends on TT1 Inspector scaffolding"]; - i246 -> i170 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; - i245 -> i170 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; - i244 -> i170 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; - i243 -> i170 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; - i171 -> i172 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT3 task depends on TT2 MVP"]; - i171 -> i204 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT3 task depends on TT2 MVP"]; - i171 -> i199 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT3 task depends on TT2 MVP"]; - i170 -> i171 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT2 task depends on TT1 Inspector scaffolding"]; - i171 -> i172 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT3 task depends on TT2 MVP"]; - i171 -> i199 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT3 task depends on TT2 MVP"]; - i171 -> i204 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT3 task depends on TT2 MVP"]; - i170 -> i205 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT2 task depends on TT1 Inspector scaffolding"]; - i222 -> i226 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i223 -> i226 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i224 -> i226 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i225 -> i226 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i222 -> i226 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i223 -> i226 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i224 -> i226 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i225 -> i226 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i231 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i231 -> i232 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Stage 1 physics depends on Stage 0"]; - i232 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i232 -> i233 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Stage 2 physics depends on Stage 1"]; - i231 -> i232 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Stage 1 physics depends on Stage 0"]; - i233 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i233 -> i234 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Stage 3 physics depends on Stage 2"]; - i232 -> i233 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Stage 2 physics depends on Stage 1"]; - i234 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i233 -> i234 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Stage 3 physics depends on Stage 2"]; - i235 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i236 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i237 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i231 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i232 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i233 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i234 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i235 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i236 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i237 -> i238 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; - i243 -> i170 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; - i244 -> i170 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; - i245 -> i170 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; - i246 -> i170 [color="green3", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; + i28 -> i19 [color="green3", penwidth=2.5, style="solid", tooltip="`crates/echo-config-fs` exists for tool preferences, but no dedicated graph store crate (e.g. `echo-store`) exists yet."]; + i32 -> i20 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; + i33 -> i20 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; + i34 -> i20 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; + i35 -> i20 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; + i36 -> i20 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; + i37 -> i21 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; + i39 -> i21 [color="green3", penwidth=2.5, style="solid", tooltip="`crates/warp-wasm/src/lib.rs` implements `validate_object_against_args` with 4 test cases."]; + i40 -> i21 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on constituent task (scoped to WASM/CLI denials)"]; + i28 -> i19 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; + i32 -> i20 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; + i33 -> i20 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; + i34 -> i20 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; + i35 -> i20 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; + i36 -> i20 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; + i37 -> i21 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on Draft Spec task"]; + i38 -> i21 [color="green3", penwidth=2.5, style="solid", tooltip=""]; + i39 -> i21 [color="green3", penwidth=2.5, style="solid", tooltip="`crates/warp-wasm/src/lib.rs` implements `validate_object_against_args` with full schema validation + 4 test cases. GitHub issue closed."]; + i40 -> i21 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Epic completion depends on constituent task"]; + i170 -> i171 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT2 task depends on TT1 Inspector scaffolding"]; + i170 -> i205 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT2 task depends on TT1 Inspector scaffolding"]; + i246 -> i170 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; + i245 -> i170 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; + i244 -> i170 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; + i243 -> i170 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; + i171 -> i172 [color="red", penwidth=1.0, style="dashed", tooltip="Inferred: TT3 task depends on TT2 MVP"]; + i171 -> i204 [color="red", penwidth=1.0, style="dashed", tooltip="Inferred: TT3 task depends on TT2 MVP"]; + i171 -> i199 [color="red", penwidth=1.0, style="dashed", tooltip="Inferred: TT3 task depends on TT2 MVP"]; + i170 -> i171 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT2 task depends on TT1 Inspector scaffolding"]; + i171 -> i172 [color="red", penwidth=1.0, style="dashed", tooltip="Inferred: TT3 task depends on TT2 MVP"]; + i171 -> i199 [color="red", penwidth=1.0, style="dashed", tooltip="Inferred: TT3 task depends on TT2 MVP"]; + i202 -> i170 [color="green3", penwidth=2.5, style="solid", tooltip="Time travel debugging requires provenance payloads for replay, slicing, and causal cone analysis."]; + i171 -> i204 [color="red", penwidth=1.0, style="dashed", tooltip="Inferred: TT3 task depends on TT2 MVP"]; + i170 -> i205 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT2 task depends on TT1 Inspector scaffolding"]; + i222 -> i226 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i223 -> i226 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i224 -> i226 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i225 -> i226 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i222 -> i226 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i223 -> i226 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i224 -> i226 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i225 -> i226 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i231 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i231 -> i232 [color="green3", penwidth=2.5, style="solid", tooltip="`crates/warp-geom` implements geometric primitives (AABB, Transform, broad-phase detection) but no physics simulation code exists: zero gravity, zero solver, zero contact resolution. Status corrected from \"In Progress\" to \"Open\" (2026-03-03)."]; + i232 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i232 -> i233 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Stage 2 physics depends on Stage 1"]; + i231 -> i232 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Stage 1 physics depends on Stage 0"]; + i233 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i233 -> i234 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Stage 3 physics depends on Stage 2"]; + i232 -> i233 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Stage 2 physics depends on Stage 1"]; + i234 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i233 -> i234 [color="green3", penwidth=2.5, style="solid", tooltip="Inferred: Stage 3 physics depends on Stage 2"]; + i235 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i236 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i237 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i231 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i232 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i233 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i234 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i235 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i236 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i237 -> i238 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: Docs follow Implementation"]; + i243 -> i170 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; + i244 -> i170 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; + i245 -> i170 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; + i246 -> i170 [color="orange", penwidth=2.0, style="solid", tooltip="Inferred: TT1 Implementation blocks on TT1 Spec clarifications"]; + i270 -> i21 [color="orange", penwidth=2.0, style="solid", tooltip="Hardening the port boundary provides evidence for security context enforcement."]; + i286 -> i287 [color="orange", penwidth=2.0, style="solid", tooltip="CodeRabbit's ASSERTIVE review mode ran `grep` and `git log` scripts on the current codebase to verify CHANGELOG claims, but the verification ran AFTER the fix deleted the evidence (replaced `DIND_STATE_HASH_V2` strings, rewrote 3,185-line file). This produced a Critical false positive claiming \"fabricated\" line counts and reference counts. Consider adding a `.coderabbitignore` pattern or CHANGELOG annotation convention that prevents post-hoc verification of claims about deleted/replaced content."]; } \ No newline at end of file diff --git a/docs/assets/dags/tasks-dag.svg b/docs/assets/dags/tasks-dag.svg index a79aecf0..aae7f07a 100644 --- a/docs/assets/dags/tasks-dag.svg +++ b/docs/assets/dags/tasks-dag.svg @@ -1,74 +1,73 @@ - - - + + tasks_dag - -Echo — Tasks DAG (from TASKS-DAG.md) -Generated by scripts/generate-tasks-dag.js + +Echo — Tasks DAG (from docs/archive/tasks/TASKS-DAG.md) +Generated by scripts/generate-tasks-dag.js cluster_legend - -Legend + +Legend cluster_Spec - -Spec + +Spec cluster_Draft - -Draft + +Draft cluster_Misc - -Misc + +Misc cluster_TT1 - -TT1 + +TT1 cluster_TT2 - -TT2 + +TT2 cluster_TT3 - -TT3 + +TT3 cluster_Demo_2 - -Demo 2 + +Demo 2 cluster_Demo_3 - -Demo 3 + +Demo 3 LG - -confirmed in issue body + +confirmed in docs/archive/tasks/TASKS-DAG.md i19 - -#19 -Spec: Persistent Store (on-disk) + +#19 Spec: Persistent Store\n(on-disk) @@ -76,19 +75,17 @@ i20 - -#20 -Spec: Commit/Manifest Signing + +#20 Spec: Commit/Manifest\nSigning i21 - - -#21 -Spec: Security Contexts (FFI/WASM/CLI) + + +#21 Spec: Security Contexts\n(WASM/CLI) @@ -96,27 +93,26 @@ i28 - -#28 -Draft spec document (header/ULEB128/property/string-pool) + +#28 Draft spec document\n(header/ULEB128/property/strin\ng-pool) i28->i19 - - - + + + - + i28->i19 - - - + + + @@ -124,9 +120,8 @@ i32 - -#32 -Draft signing spec + +#32 Draft signing spec @@ -134,17 +129,17 @@ i32->i20 - - + + - + i32->i20 - - - + + + @@ -152,9 +147,8 @@ i37 - -#37 -Draft security contexts spec + +#37 Draft security contexts\nspec @@ -162,17 +156,17 @@ i37->i21 - - + + - + i37->i21 - - - + + + @@ -180,9 +174,8 @@ i33 - -#33 -CI: sign release artifacts (dry run) + +#33 CI: sign release artifacts\n(dry run) @@ -190,17 +183,17 @@ i33->i20 - - + + - + i33->i20 - - - + + + @@ -208,9 +201,8 @@ i34 - -#34 -CLI verify path + +#34 CLI verify path @@ -218,17 +210,17 @@ i34->i20 - - + + - + i34->i20 - - - + + + @@ -236,9 +228,8 @@ i35 - -#35 -Key management doc + +#35 Key management doc @@ -246,17 +237,17 @@ i35->i20 - - + + - + i35->i20 - - - + + + @@ -264,9 +255,8 @@ i36 - -#36 -CI: verify signatures + +#36 CI: verify signatures @@ -274,160 +264,203 @@ i36->i20 - - + + - + i36->i20 - - - + + + - + -i38 - - -#38 -FFI limits and validation +i39 + + +#39 WASM input validation - + -i38->i21 - - - +i39->i21 + + + - + -i38->i21 - - - +i39->i21 + + + - + -i39 - - -#39 -WASM input validation +i40 + + +#40 Unit tests for denials - + -i39->i21 - - - +i40->i21 + + + - + -i39->i21 +i40->i21 - - + + - + -i40 - - -#40 -Unit tests for denials +i38 + + +#38 FFI limits and validation - - -i40->i21 - - - - - + + +i38->i21 + + - - -i40->i21 - - - + + +i202 + + +#202 Provenance Payload (PP)\nv1 — spec + implementation - + i170 - - -#170 -TT1: StreamsFrame inspector support -(backlog + cursors + admission decisions) + + +#170 TT1: StreamsFrame\ninspector support (backlog +\ncursors + admission decisions) + + + + + +i202->i170 + + + + + + + + +i270 + + +#270 Hardening: Fuzz the\nScenePort boundary (proptest) + + + + + +i270->i21 + + + + + + + + +i286 + + +#286 CI: Add unit tests for\nclassify_changes.cjs and\nmatches() + + + + + +i287 + + +#287 Docs: Document\nban-nondeterminism.sh\nallowlist process in\nRELEASE_POLICY.md + + + + + +i286->i287 + + + - + i171 - - -#171 -TT2: Time Travel MVP (pause/rewind/buffer/catch-up) + + +#171 TT2: Time Travel MVP\n(pause/rewind/buffer/catch-up) - + i170->i171 - - - + + + - + i170->i171 - - - + + + - + i205 - - -#205 -TT2: Reliving debugger MVP (scrub -timeline + causal slice + fork branch) + + +#205 TT2: Reliving debugger\nMVP (scrub timeline + causal\nslice + fork branch) - + i170->i205 - - - + + + @@ -435,28 +468,26 @@ i170->i205 - - + + - + i246 - - -#246 -TT1: Security/capabilities for -fork/rewind/merge in multiplayer + + +#246 TT1:\nSecurity/capabilities for\nfork/rewind/merge in\nmultiplayer - + i246->i170 - - - + + + @@ -464,28 +495,26 @@ i246->i170 - - + + - + i245 - - -#245 -TT1: Merge semantics for admitted -stream facts across worldlines + + +#245 TT1: Merge semantics for\nadmitted stream facts across\nworldlines - + i245->i170 - - - + + + @@ -493,28 +522,26 @@ i245->i170 - - + + - + i244 - - -#244 -TT1: TimeStream retention + spool -compaction + wormhole density + + +#244 TT1: TimeStream retention\n+ spool compaction + wormhole\ndensity - + i244->i170 - - - + + + @@ -522,28 +549,26 @@ i244->i170 - - + + - + i243 - - -#243 -TT1: dt policy (fixed timestep -vs admitted dt stream) + + +#243 TT1: dt policy (fixed\ntimestep vs admitted dt\nstream) - + i243->i170 - - - + + + @@ -551,56 +576,53 @@ i243->i170 - - + + - + i172 - - -#172 -TT3: Rulial diff / worldline compare MVP + + +#172 TT3: Rulial diff /\nworldline compare MVP - + i171->i172 - - - + + + - + i171->i172 - - - + + + - + i204 - - -#204 -TT3: Provenance heatmap (blast -radius / cohesion over time) + + +#204 TT3: Provenance heatmap\n(blast radius / cohesion over\ntime) - + i171->i204 - - - + + + @@ -608,59 +630,53 @@ i171->i204 - - + + - + i199 - - -#199 -TT3: Wesley worldline diff (compare -query outputs/proofs across ticks) + + +#199 TT3: Wesley worldline\ndiff (compare query\noutputs/proofs across ticks) - + i171->i199 - - - + + + - + i171->i199 - - - + + + - + i222 - - -#222 -Demo 2: Splash Guy — deterministic -rules + state model + + +#222 Demo 2: Splash Guy —\ndeterministic rules + state\nmodel - + i226 - - -#226 -Demo 2: Splash Guy — docs: networking-first -course modules + + +#226 Demo 2: Splash Guy —\ndocs: networking-first course\nmodules @@ -668,8 +684,8 @@ i222->i226 - - + + @@ -677,19 +693,17 @@ i222->i226 - - + + - + i223 - - -#223 -Demo 2: Splash Guy — lockstep input -protocol + two-peer harness + + +#223 Demo 2: Splash Guy —\nlockstep input protocol +\ntwo-peer harness @@ -697,8 +711,8 @@ i223->i226 - - + + @@ -706,19 +720,17 @@ i223->i226 - - + + - + i224 - - -#224 -Demo 2: Splash Guy — controlled -desync lessons (make it fail on purpose) + + +#224 Demo 2: Splash Guy —\ncontrolled desync lessons\n(make it fail on purpose) @@ -726,8 +738,8 @@ i224->i226 - - + + @@ -735,19 +747,17 @@ i224->i226 - - + + - + i225 - - -#225 -Demo 2: Splash Guy — minimal rendering -/ visualization path + + +#225 Demo 2: Splash Guy —\nminimal rendering /\nvisualization path @@ -755,8 +765,8 @@ i225->i226 - - + + @@ -764,30 +774,26 @@ i225->i226 - - + + - + i231 - - -#231 -Demo 3: Tumble Tower — Stage 0 -physics (2D AABB stacking) + + +#231 Demo 3: Tumble Tower —\nStage 0 physics (2D AABB\nstacking) - + i238 - - -#238 -Demo 3: Tumble Tower — docs course -(physics ladder) + + +#238 Demo 3: Tumble Tower —\ndocs course (physics ladder) @@ -795,8 +801,8 @@ i231->i238 - - + + @@ -804,28 +810,26 @@ i231->i238 - - + + - + i232 - - -#232 -Demo 3: Tumble Tower — Stage 1 -physics (rotation + angular, OBB contacts) + + +#232 Demo 3: Tumble Tower —\nStage 1 physics (rotation +\nangular, OBB contacts) i231->i232 - - - + + + @@ -833,8 +837,8 @@ i231->i232 - - + + @@ -842,8 +846,8 @@ i232->i238 - - + + @@ -851,19 +855,17 @@ i232->i238 - - + + - + i233 - - -#233 -Demo 3: Tumble Tower — Stage 2 -physics (friction + restitution) + + +#233 Demo 3: Tumble Tower —\nStage 2 physics (friction +\nrestitution) @@ -871,8 +873,8 @@ i232->i233 - - + + @@ -880,8 +882,8 @@ i232->i233 - - + + @@ -889,8 +891,8 @@ i233->i238 - - + + @@ -898,19 +900,17 @@ i233->i238 - - + + - + i234 - - -#234 -Demo 3: Tumble Tower — Stage 3 -physics (sleeping + stack stability) + + +#234 Demo 3: Tumble Tower —\nStage 3 physics (sleeping +\nstack stability) @@ -918,8 +918,8 @@ i233->i234 - - + + @@ -927,8 +927,8 @@ i233->i234 - - + + @@ -936,8 +936,8 @@ i234->i238 - - + + @@ -945,19 +945,17 @@ i234->i238 - - + + - + i235 - - -#235 -Demo 3: Tumble Tower — lockstep -harness + per-tick fingerprinting + + +#235 Demo 3: Tumble Tower —\nlockstep harness + per-tick\nfingerprinting @@ -965,8 +963,8 @@ i235->i238 - - + + @@ -974,19 +972,17 @@ i235->i238 - - + + - + i236 - - -#236 -Demo 3: Tumble Tower — controlled -desync breakers (physics edition) + + +#236 Demo 3: Tumble Tower —\ncontrolled desync breakers\n(physics edition) @@ -994,8 +990,8 @@ i236->i238 - - + + @@ -1003,19 +999,17 @@ i236->i238 - - + + - + i237 - - -#237 -Demo 3: Tumble Tower — visualization -(2D view + debug overlays) + + +#237 Demo 3: Tumble Tower —\nvisualization (2D view + debug\noverlays) @@ -1023,8 +1017,8 @@ i237->i238 - - + + @@ -1032,8 +1026,8 @@ i237->i238 - - + + diff --git a/docs/dependency-dags.md b/docs/dependency-dags.md index e879e6dd..b7aa6497 100644 --- a/docs/dependency-dags.md +++ b/docs/dependency-dags.md @@ -79,18 +79,18 @@ cargo xtask dags --snapshot-label 2026-01-02 --- -## Tasks DAG (derived from TASKS-DAG.md) +## Tasks DAG (derived from `docs/archive/tasks/TASKS-DAG.md`) ![Tasks DAG](assets/dags/tasks-dag.svg) Sources: -- Source data: `TASKS-DAG.md` -- Generator: `scripts/generate-tasks-dag.js` (scheduled by `.github/workflows/refresh-dependency-dags.yml` to keep the rendered output aligned with `TASKS-DAG.md`) +- Source data: `docs/archive/tasks/TASKS-DAG.md` +- Generator: `scripts/generate-tasks-dag.js` (scheduled by the GitHub workflow `.github/workflows/refresh-dependency-dags.yml` to keep the rendered output aligned with `docs/archive/tasks/TASKS-DAG.md`) - DOT: `docs/assets/dags/tasks-dag.dot` - SVG: `docs/assets/dags/tasks-dag.svg` -This DAG visualizes inferred issue dependencies that contributors log in `TASKS-DAG.md`, offering a quick comparison point against the curated milestone/issue graphs above. +This DAG visualizes inferred issue dependencies that contributors log in `docs/archive/tasks/TASKS-DAG.md`, offering a quick comparison point against the curated milestone/issue graphs above. By design, isolated nodes (no incoming/outgoing edges) are filtered out to reduce clutter; the generator computes `connectedNodeIds` / `filteredNodes` and logs the drop counts during render. ## Regenerating the Tasks DAG diff --git a/docs/guide/cargo-features.md b/docs/guide/cargo-features.md index 79b40540..aa4ff813 100644 --- a/docs/guide/cargo-features.md +++ b/docs/guide/cargo-features.md @@ -77,10 +77,17 @@ Enabling both `footprint_enforce_release` and `unsafe_graph` is a compile error. ## echo-wasm-abi -| Feature | Default | Description | -| ------- | ------- | --------------------------------------------------------------------------- | -| `std` | **yes** | Standard library support (enables `serde/std`, `ciborium/std`, `half/std`). | -| `alloc` | no | Alloc-only mode for `no_std` environments. | +| Feature | Default | Description | +| ------- | ------- | ---------------------------------------------------------------------------------------------------------- | +| `std` | **yes** | Standard library support (enables `serde/std`, `ciborium/std`, `half/std`, and `echo-runtime-schema/std`). | +| `alloc` | no | Alloc-only mode for `no_std` environments. | + +## echo-runtime-schema + +| Feature | Default | Description | +| ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `std` | **yes** | Standard library support. | +| `serde` | **yes** | Enables serde derives for shared runtime-schema ids, counters, and structural keys. Consumers opt in explicitly when default features are disabled. | ## echo-registry-api diff --git a/docs/plans/adr-0008-and-0009.md b/docs/plans/adr-0008-and-0009.md index ec0d9696..a74a609e 100644 --- a/docs/plans/adr-0008-and-0009.md +++ b/docs/plans/adr-0008-and-0009.md @@ -4,7 +4,7 @@ # Implementation Plan: ADR-0008 and ADR-0009 -- **Status:** Living implementation plan; Phases 0-7 implemented +- **Status:** Living implementation plan; Phases 0-8 implemented - **Date:** 2026-03-19 - **Primary ADRs:** ADR-0008 (Worldline Runtime Model), ADR-0009 (Inter-Worldline Communication) - **Companion ADRs in this change set:** @@ -274,25 +274,25 @@ coherent entry object instead of N side vectors that must stay index-aligned for ## Phase Map -| Phase | ADR | Summary | Depends On | -| ----- | ------------------ | ------------------------------------------------------------------- | ------------ | -| 0 | — | Invariant harness, golden vectors, ADR-exception ledger | — | -| 1 | 0008 §1 | Runtime primitives: heads, worldlines, registries, `WorldlineState` | 0 | -| 2 | 0008 §2 | `SchedulerCoordinator` with serial canonical scheduling | 1 | -| 3 | 0008 §3 | Deterministic ingress + per-head inbox policy | 1, 2 | -| 4 | 0008 §4 | Provenance entry model, DAG parents, local commit append | 2, 3 | -| 5 | 0008 §5 + ADR-0010 | Head-local observation APIs, snapshot, fork, admin rewind | 4 | -| 6 | 0008 §6 | Split `worldline_tick` / `global_tick` semantics | 4 | -| 7 | 0008 §7 | Multi-warp replay using full `WorldlineState` | 5, 6 | -| 8 | 0008 §8 | Wesley schema freeze for ADR-0008 runtime types | 1–7 stable | -| 9A | 0009 near | Mechanical footprint extension | 4 | -| 9B | 0009 near | Semantic footprint audit: deletes, anchors, witnesses | 9A | -| 9C | 0009 near | Conflict policy interface + fixtures | 9B | -| 10 | 0009 near | Application-level cross-worldline messages | 3, 4, 9C | -| 11 | 0009 mid | Import pipeline, causal frontier, suffix transport | 5, 6, 9C, 10 | -| 12 | 0009 mid | Hierarchical footprint summaries | 11 | -| 13 | 0009 later | Distributed seams and authority hooks | 11 | -| 14 | — | Wesley schema freeze for ADR-0009 transport/conflict types | 9C–11 stable | +| Phase | ADR | Summary | Depends On | +| ----- | ------------------ | ---------------------------------------------------------------------- | ------------ | +| 0 | — | Invariant harness, golden vectors, ADR-exception ledger | — | +| 1 | 0008 §1 | Runtime primitives: heads, worldlines, registries, `WorldlineState` | 0 | +| 2 | 0008 §2 | `SchedulerCoordinator` with serial canonical scheduling | 1 | +| 3 | 0008 §3 | Deterministic ingress + per-head inbox policy | 1, 2 | +| 4 | 0008 §4 | Provenance entry model, DAG parents, local commit append | 2, 3 | +| 5 | 0008 §5 + ADR-0010 | Head-local observation APIs, snapshot, fork, admin rewind | 4 | +| 6 | 0008 §6 | Split `worldline_tick` / `global_tick` semantics | 4 | +| 7 | 0008 §7 | Multi-warp replay using full `WorldlineState` | 5, 6 | +| 8 | 0008 §8 | Runtime schema freeze for ADR-0008 types (Echo-local; Wesley deferred) | 1–7 stable | +| 9A | 0009 near | Mechanical footprint extension | 4 | +| 9B | 0009 near | Semantic footprint audit: deletes, anchors, witnesses | 9A | +| 9C | 0009 near | Conflict policy interface + fixtures | 9B | +| 10 | 0009 near | Application-level cross-worldline messages | 3, 4, 9C | +| 11 | 0009 mid | Import pipeline, causal frontier, suffix transport | 5, 6, 9C, 10 | +| 12 | 0009 mid | Hierarchical footprint summaries | 11 | +| 13 | 0009 later | Distributed seams and authority hooks | 11 | +| 14 | — | Wesley schema freeze for ADR-0009 transport/conflict types | 9C–11 stable | ## Phase 0: Invariant Harness and Exception Ledger @@ -768,11 +768,19 @@ and any supporting helpers needed to handle portal / instance ops. - Replay helpers never appear in live writer-advance codepaths - Historical snapshots match known-good receipts for mixed multi-warp histories -## Phase 8: Wesley Schema Freeze for ADR-0008 Runtime Types +## Phase 8: Runtime Schema Freeze for ADR-0008 Runtime Types **Goal** -Freeze the stable core runtime surface after the ADR-0008 phases settle. +Freeze the stable core runtime surface after the ADR-0008 phases settle, +without coupling Echo to Wesley generation while the upstream contract is still +moving. + +**Prep inventory** + +- [Phase 8 Prep Inventory: ADR-0008 Runtime Schema Freeze](phase-8-schema-freeze-inventory.md) +- [Phase 8 Runtime Schema Conformance Audit](phase-8-runtime-schema-conformance.md) +- [Phase 8 Runtime Schema Mapping Contract](phase-8-runtime-schema-mapping-contract.md) **Scope** @@ -786,7 +794,16 @@ Schema coverage includes stable runtime types such as: - `IntentKind` - `InboxPolicy` - `IngressTarget` -- `SuperTickResult` +- `SchedulerStatus` +- `SchedulerState` +- `WorkState` +- `RunCompletion` +- `HeadEligibility` +- `HeadDisposition` + +Phase 8 freezes the **actual** scheduler result surface exposed by the +implementation. The old `SuperTickResult` shorthand is retired here in favor of +the explicit control-plane types above. ### Rule @@ -794,8 +811,13 @@ Do **not** include ADR-0009 transport/conflict types here. They are not stable y ### Exit Criteria -- Generated types are structurally equivalent -- Hand-written types are removed or reduced to wrappers only where necessary +- The frozen ADR-0008 runtime schema contract is explicit and locally validated +- Shared semantic primitives are centralized or wrapped explicitly where that + buys real cross-crate value +- Runtime-local behavior types remain hand-written where generation does not + buy anything yet +- Wesley/codegen plumbing is explicitly deferred until the upstream contract is + stable enough to own it honestly ## Phase 9A: Mechanical Footprint Extension diff --git a/docs/plans/phase-8-runtime-schema-conformance.md b/docs/plans/phase-8-runtime-schema-conformance.md new file mode 100644 index 00000000..3c578b8a --- /dev/null +++ b/docs/plans/phase-8-runtime-schema-conformance.md @@ -0,0 +1,167 @@ + + + +# Phase 8 Runtime Schema Conformance Audit + +- **Status:** Echo-local conformance audit locked for `feat/adr-0008-0009-phase-8` +- **Date:** 2026-03-22 +- **Primary Plan:** [Implementation Plan: ADR-0008 and ADR-0009](adr-0008-and-0009.md) +- **Prep Inventory:** [Phase 8 Prep Inventory: ADR-0008 Runtime Schema Freeze](phase-8-schema-freeze-inventory.md) +- **Mapping Contract:** [Phase 8 Runtime Schema Mapping Contract](phase-8-runtime-schema-mapping-contract.md) + +## Purpose + +This audit answers a narrower question than the prep inventory: + +- given the frozen Artifacts A-D under `schemas/runtime/`, +- and given the current hand-written runtime and ABI types, +- where does Echo already conform, +- where is the schema using an intentional GraphQL wrapper shape, +- and where does naming or ownership drift still block honest generation? + +Phase 8 should not wire Wesley until these answers are explicit. + +## Status Legend + +- **Aligned:** the schema meaning matches the current Rust surface directly. +- **Intentional wrapper:** the schema uses a GraphQL-specific carrier shape + such as `*Input`, `*Kind`, or unions; this is expected and not drift. +- **Blocking drift:** current Rust naming or ownership would make generation + dishonest or noisy unless reconciled first. +- **Adapter-owned:** the frozen type is real, but its canonical owner is the + ABI/control-plane layer rather than `warp-core`. + +## Executive Summary + +1. The schema fragments are semantically sound against the current ADR-0008 + runtime model. There is no evidence that Artifacts A-D froze the wrong + concepts. +2. The biggest ownership blocker from the earlier Phase 8 slices has now been + resolved: shared logical counters and the core + `HeadId`/`WorldlineId`/`WriterHeadKey` types live in + `crates/echo-runtime-schema`. +3. The biggest blockers before generation are: + - remaining ABI-edge raw-byte DTOs outside the newly typed + `WorldlineId`/`HeadId`/`WriterHeadKey` path, where the frozen schema still + wants semantic wrappers + - Wesley/codegen instability upstream, which is why this branch is freezing + and validating the runtime schema locally instead of wiring generation + plumbing prematurely +4. GraphQL-specific input wrappers are expected. They should be treated as + schema transport encodings, not evidence that the core runtime surface is + wrong. + +## Artifact A: Identifiers and Logical Counters + +| Schema type | Canonical Rust owner today | Status | Notes | +| -------------------- | --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `HeadId` | `crates/echo-runtime-schema/src/lib.rs` | Aligned | Opaque hash-backed newtype matches the scalar intent. `warp-core` now consumes the shared owner and the ABI keeps a byte DTO. | +| `WorldlineId` | `crates/echo-runtime-schema/src/lib.rs` | Aligned | Opaque worldline identifier matches the scalar intent and now has a shared owner. | +| `IntentKind` | `crates/warp-core/src/head_inbox.rs` | Aligned | Domain-separated hash-backed newtype matches the frozen scalar semantics and remains intentionally runtime-owned for Phase 8. | +| `WorldlineTick` | `crates/echo-runtime-schema/src/lib.rs` | Aligned | Shared logical-counter owner now exists and both `warp-core` and `echo-wasm-abi` consume it. | +| `GlobalTick` | `crates/echo-runtime-schema/src/lib.rs` | Aligned | Same as `WorldlineTick`: shared owner exists, semantics stay intact. | +| `RunId` | `crates/echo-runtime-schema/src/lib.rs` | Aligned | Shared owner exists and the ABI re-exports the same control-plane token type. | +| `WriterHeadKey` | `crates/echo-runtime-schema/src/lib.rs` | Aligned | Runtime and ABI now agree on both name and typed field shape; runtime owner is shared. | +| `WriterHeadKeyInput` | none; schema-only wrapper | Intentional wrapper | GraphQL needs an explicit input mirror even though the runtime only needs `WriterHeadKey`. | + +### Artifact A blockers + +- None in the current Echo-local freeze. `IntentKind` remains intentionally + runtime-owned until a real generated consumer requires a shared home. + +## Artifact B: Routing and Admission + +| Schema type | Canonical Rust owner today | Status | Notes | +| ------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `InboxAddress` | `crates/warp-core/src/head_inbox.rs` | Aligned | Runtime newtype over `String` matches the scalar alias intent and remains intentionally runtime-owned for Phase 8. | +| `HeadEligibility` | `crates/warp-core/src/head.rs` with ABI mirror in `crates/echo-wasm-abi/src/kernel_port.rs` | Aligned | The two-state model matches exactly across runtime and ABI. | +| `IngressTarget` | `crates/warp-core/src/head_inbox.rs` | Intentional wrapper | Core enum is correct. Schema unions plus `IngressTargetInput` are GraphQL carriers for the same three-way split. | +| `IngressTargetInput` / `IngressTargetKind` | none; schema-only wrappers | Intentional wrapper | Required because GraphQL does not have native input unions. | +| `InboxPolicy` | `crates/warp-core/src/head_inbox.rs` | Intentional wrapper | Core enum is correct. Schema unions plus `InboxPolicyInput` are a transport encoding for `AcceptAll`, `KindFilter`, and `Budgeted`. | +| `InboxPolicyInput` / `InboxPolicyKind` | none; schema-only wrappers | Intentional wrapper | Required by GraphQL input limitations. | + +### Artifact B notes + +- `InboxPolicy::Budgeted { max_per_tick: u32 }` now maps to + `maxPerTick: PositiveInt!`, where the schema pins the full positive `u32` + range rather than a signed-`Int` subset. +- `IngressTarget::ExactHead { key: WriterHeadKey }` now inherits the aligned + typed-id ABI surface from Artifact A rather than a raw-byte fallback. + +## Artifact C: Playback Control + +| Schema type | Canonical Rust owner today | Status | Notes | +| ---------------------------------------- | ---------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------- | +| `SeekThen` | `crates/warp-core/src/playback.rs` | Aligned | Two-state follow-up enum matches exactly. | +| `PlaybackMode` | `crates/warp-core/src/playback.rs` | Intentional wrapper | Core enum is correct. Schema unions plus `PlaybackModeInput` encode the same modes for GraphQL. | +| `PlaybackModeInput` / `PlaybackModeKind` | none; schema-only wrappers | Intentional wrapper | Required because GraphQL input unions do not exist. | + +### Artifact C notes + +- No semantic mismatch was found here. +- The main rule for generation is to keep `PlaybackModeInput` as a carrier DTO + rather than pretending it is the canonical runtime enum. + +## Artifact D: Scheduler Results + +| Schema type | Canonical Rust owner today | Status | Notes | +| ------------------------------------------ | ----------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | +| `SchedulerMode` | `crates/echo-wasm-abi/src/kernel_port.rs` | Adapter-owned | The ABI is the real owner today. The schema fragment matches the single `UntilIdle` mode honestly. | +| `SchedulerModeInput` / `SchedulerModeKind` | none; schema-only wrappers | Intentional wrapper | Required for GraphQL input encoding. | +| `SchedulerState` | `crates/echo-wasm-abi/src/kernel_port.rs` | Adapter-owned | Enum matches exactly. | +| `WorkState` | `crates/echo-wasm-abi/src/kernel_port.rs` | Adapter-owned | Enum matches exactly. | +| `RunCompletion` | `crates/echo-wasm-abi/src/kernel_port.rs` | Adapter-owned | Enum matches exactly. | +| `HeadDisposition` | `crates/echo-wasm-abi/src/kernel_port.rs` | Adapter-owned | This is an ABI truth surface derived from runtime state; no core-owned counterpart is required. | +| `SchedulerStatus` | `crates/echo-wasm-abi/src/kernel_port.rs` | Adapter-owned | Struct shape matches the fragment. The schema is freezing the control-plane surface, not a `warp-core` struct. | + +### Artifact D notes + +- `HeadDisposition` is not missing just because `warp-core` does not own it. + It is intentionally an ABI/control-plane truth type. +- The scheduler-result freeze set is therefore a mix of core-owned concepts + (`HeadEligibility`) and ABI-owned DTOs (`SchedulerStatus`, `HeadDisposition`, + `SchedulerMode`). + +## Cross-Cutting Generation Blockers + +### 1. Opaque ids should not dissolve into raw byte vectors at the ABI edge + +`HeadId`, `WorldlineId`, and `WriterHeadKey` are frozen as typed opaque runtime +concepts, and the current ABI now carries typed wrappers for those ids/keys. +The remaining raw-byte DTO fields are payload/hash/blob carriers rather than +semantic runtime identifiers. + +That boundary is workable for hand-written adapters, but it still means the +generated runtime-schema types must stay separate from byte-oriented transport +payloads. + +### 2. Shared-owner expansion is intentionally bounded + +Phase 8 has already moved the frozen logical counters and core opaque ids/key +types into `crates/echo-runtime-schema`, and `warp-core`/`echo-wasm-abi` now +consume that shared owner where their semantics match. + +Not every frozen schema type should join that crate. `IntentKind` and +`InboxAddress` remain hand-written in `warp-core` because they are runtime +behavior types today, not clearly shared generated primitives. + +### 3. GraphQL wrapper DTOs must stay wrapper DTOs + +The schema's `*Input` and `*Kind` types exist because GraphQL cannot express +input unions directly. They should not be mistaken for evidence that the core +runtime enums are wrong or incomplete. + +This matters for generation layout: + +- core runtime generation should target the semantic enums and keys, +- adapter generation may also emit GraphQL carrier wrappers, +- but those wrappers should not leak back into `warp-core` as the new source of + truth. + +## Follow-On + +The scalar and ownership rules are now pinned in the +[Phase 8 Runtime Schema Mapping Contract](phase-8-runtime-schema-mapping-contract.md). +The next honest implementation slice is the explicit Phase 8 closeout pass: +lock the local freeze boundary, defer Wesley generation plumbing, and prepare +the branch for merge without pretending upstream generator churn is resolved. diff --git a/docs/plans/phase-8-runtime-schema-mapping-contract.md b/docs/plans/phase-8-runtime-schema-mapping-contract.md new file mode 100644 index 00000000..91f74910 --- /dev/null +++ b/docs/plans/phase-8-runtime-schema-mapping-contract.md @@ -0,0 +1,140 @@ + + + +# Phase 8 Runtime Schema Mapping Contract + +- **Status:** Echo-local mapping contract locked for `feat/adr-0008-0009-phase-8` +- **Date:** 2026-03-22 +- **Primary Plan:** [Implementation Plan: ADR-0008 and ADR-0009](adr-0008-and-0009.md) +- **Prep Inventory:** [Phase 8 Prep Inventory: ADR-0008 Runtime Schema Freeze](phase-8-schema-freeze-inventory.md) +- **Conformance Audit:** [Phase 8 Runtime Schema Conformance Audit](phase-8-runtime-schema-conformance.md) + +## Purpose + +This document pins the Rust-side ownership and scalar-mapping rules for the +frozen ADR-0008 runtime schema. + +It answers three concrete questions before any generation plumbing exists: + +1. Where will generated runtime-schema types live? +2. Which frozen schema types become shared generated Rust wrappers? +3. Which ABI fields are allowed to remain raw bytes instead of typed wrappers? + +## Ownership Decision + +The generated-or-generation-ready Rust home for shared ADR-0008 runtime-schema +types is: + +- `crates/echo-runtime-schema` + +That crate now exists as the single Echo-local owner for the subset that is +already clearly shared across runtime and adapter layers: + +- opaque runtime identifiers, +- logical counters, +- structural runtime key types. + +Not every frozen schema type automatically belongs there. Runtime-local +behavioral types may stay hand-written in their owning crate when generation +does not buy anything yet. + +The ownership split after generation lands should be: + +- `echo-runtime-schema` + generated source of truth for shared runtime-schema types +- `warp-core` + consumes or re-exports shared semantic types +- `echo-wasm-abi` + stays adapter-owned for host DTOs and converts to and from the shared types + where the ABI needs a different wire shape + +## Core Rule + +Semantic runtime identifiers and logical coordinates must not default to raw +`Vec` or plain integers in generated Rust. + +The default generated form is: + +- opaque-id newtype for identifiers, +- logical-counter newtype for counters, +- structured Rust `struct` for composite keys, +- string newtype for named aliases such as `InboxAddress` only when a real + shared/generated consumer exists + +Raw bytes remain acceptable only for fields that are semantically binary +payloads or content hashes rather than typed runtime identifiers. + +## Mapping Table + +| Schema type | Generated Rust home | Generated Rust shape | ABI DTO policy | Notes | +| --------------- | --------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `HeadId` | `echo-runtime-schema` | `#[repr(transparent)] struct HeadId([u8; 32]);` | Use typed wrapper DTOs for semantic head identifiers | This is an opaque runtime identifier, not a generic byte vector. | +| `WorldlineId` | `echo-runtime-schema` | `#[repr(transparent)] struct WorldlineId([u8; 32]);` | Use typed wrapper DTOs for semantic worldline identifiers | Same rule as `HeadId`. | +| `IntentKind` | `warp-core` (Phase 8) | hand-written opaque-id newtype | Use typed wrapper DTOs where the ABI exposes intent kinds semantically | Keep runtime-owned until a real generated consumer requires a shared home. | +| `WorldlineTick` | `echo-runtime-schema` | `#[repr(transparent)] struct WorldlineTick(u64);` | Use typed wrapper DTOs | Logical coordinate, not wall-clock time and not a bare `u64` in generated code. | +| `GlobalTick` | `echo-runtime-schema` | `#[repr(transparent)] struct GlobalTick(u64);` | Use typed wrapper DTOs | Logical cycle stamp, not wall-clock time. | +| `RunId` | `echo-runtime-schema` | `#[repr(transparent)] struct RunId(u64);` | Use typed wrapper DTOs | Control-plane generation token. | +| `InboxAddress` | `warp-core` (Phase 8) | hand-written string newtype | Use typed wrapper DTOs when the field is semantically an inbox alias | Keep runtime-owned; centralizing a plain routing alias buys nothing today. | +| `WriterHeadKey` | `echo-runtime-schema` | `struct WriterHeadKey { worldline_id: WorldlineId, head_id: HeadId }` | Use typed wrapper DTOs | Structural runtime key; its fields should stay typed. | + +## ABI Raw-Byte Exception Rule + +The ABI may keep raw `Vec` fields only for values that are inherently +binary artifacts or open payloads. + +Allowed raw-byte categories: + +- content hashes such as `state_root`, `commit_id`, and `artifact_hash` +- channel identifiers and channel payload bytes +- opaque payload envelopes such as `vars_bytes`, `data`, and intent bodies +- compatibility-sensitive byte-oriented blobs that are not part of the + runtime-schema freeze set + +Disallowed raw-byte default: + +- semantic runtime identifiers such as `HeadId`, `WorldlineId`, and + `WriterHeadKey` +- logical coordinates such as `WorldlineTick`, `GlobalTick`, and `RunId` + +## Immediate Consequences For Existing Code + +### `warp-core` + +- `warp-core` already consumes or re-exports the shared logical counters and + core opaque ids/key types introduced in `echo-runtime-schema`. +- Its role is semantic consumer plus runtime behavior owner. +- `IntentKind` and `InboxAddress` remain intentionally hand-written here for + Phase 8 because they do not yet justify a shared generated home. + +### `echo-wasm-abi` + +- `echo-wasm-abi` remains the owner of host DTO layout and CBOR envelope rules. +- It now consumes the shared logical counters from `echo-runtime-schema`. +- It should keep explicit adapter wrappers where the wire contract differs from + the shared semantic type, such as byte-serialized `HeadId` and `WorldlineId`. +- Existing raw-byte identifier fields outside those typed wrappers are now + technical debt to retire, not neutral defaults. + +### `echo-wesley-gen` + +- The current generic GraphQL mappings such as `Int -> i32` and `ID -> String` + are not sufficient for the ADR-0008 runtime schema. +- The runtime schema depends on custom scalar mappings and typed wrapper output, + not the generic DTO defaults used by the current IR-to-Rust path. + +## Out of Scope + +- wiring `cargo xtask wesley sync` +- changing current ABI v3 wire fields in this document alone +- ADR-0009 transport/conflict schema mapping + +## Recommended Next Slice + +With ownership and scalar-mapping rules pinned and the shared crate now +scaffolded, the next honest implementation slice is: + +1. treat the shared-owner split as intentionally complete for Phase 8 unless a + real generated consumer appears for `IntentKind`, +2. leave hashes and payload blobs as raw bytes, +3. keep Wesley/codegen plumbing deferred until the upstream schema/compiler + contract stabilizes. diff --git a/docs/plans/phase-8-schema-freeze-inventory.md b/docs/plans/phase-8-schema-freeze-inventory.md new file mode 100644 index 00000000..917fdefd --- /dev/null +++ b/docs/plans/phase-8-schema-freeze-inventory.md @@ -0,0 +1,270 @@ + + + +# Phase 8 Prep Inventory: ADR-0008 Runtime Schema Freeze + +- **Status:** Freeze inventory locked for `feat/adr-0008-0009-phase-8` +- **Date:** 2026-03-22 +- **Primary Plan:** [Implementation Plan: ADR-0008 and ADR-0009](adr-0008-and-0009.md) +- **Conformance Audit:** [Phase 8 Runtime Schema Conformance Audit](phase-8-runtime-schema-conformance.md) +- **Mapping Contract:** [Phase 8 Runtime Schema Mapping Contract](phase-8-runtime-schema-mapping-contract.md) + +## Purpose + +Phase 8 is not "generate whatever exists today." It is the point where Echo +pins the stable ADR-0008 runtime surface and only then teaches Wesley/codegen +to reproduce it. + +This inventory records: + +- the runtime types Phase 8 should freeze, +- where their canonical definitions live today, +- where host/runtime mirrors still exist, +- and the gaps that must be closed before generated types can replace + hand-written copies honestly. + +## Executive Findings + +1. The current Wesley-generated crates are **TTD-specific**, not ADR-0008 + runtime-schema crates. + - `crates/ttd-manifest/src/lib.rs` + - `crates/ttd-protocol-rs/lib.rs` +2. The repo documentation says `cargo xtask wesley sync` manages those vendored + artifacts, but `xtask/src/main.rs` does **not** implement a `wesley` + subcommand yet. +3. The shared Phase 8 owner crate now exists as + `crates/echo-runtime-schema`, and it already owns the frozen logical + counters plus the core `HeadId`/`WorldlineId`/`WriterHeadKey` types. The + rest of the freeze set intentionally stays hand-written in `warp-core`, + with host-facing adapter DTOs in `echo-wasm-abi`. +4. The living plan's old `SuperTickResult` shorthand should be retired. + The actual stable scheduler result surface is: + `SchedulerStatus`, `SchedulerState`, `WorkState`, `RunCompletion`, + `HeadEligibility`, and `HeadDisposition`. +5. Freezing those runtime surfaces also requires a few supporting types that the + earlier plan text underspecified: `WorldlineId`, `InboxAddress`, `SeekThen`, + `SchedulerMode`, and `RunId`. + +## Freeze Set Inventory + +| Candidate | Canonical definition today | Mirror / adapter surface | Phase 8 note | +| ----------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `HeadId` | `crates/echo-runtime-schema/src/lib.rs` | ABI head-key wrappers in `crates/echo-wasm-abi/src/kernel_port.rs` | Opaque hash-backed id; runtime owner is now shared, ABI wrapper remains byte-oriented | +| `WorldlineId` | `crates/echo-runtime-schema/src/lib.rs` | ABI worldline-id wrappers in `crates/echo-wasm-abi/src/kernel_port.rs` | Supporting opaque id needed by `WriterHeadKey`, `IngressTarget`, and observation/control wrappers | +| `WriterHeadKey` | `crates/echo-runtime-schema/src/lib.rs` | ABI head-key wrappers in `crates/echo-wasm-abi/src/kernel_port.rs` | Stable composite runtime key; runtime owner is now shared | +| `PlaybackMode` | `crates/warp-core/src/playback.rs` | TTD-generated `PlaybackMode` in `crates/ttd-protocol-rs/lib.rs` is related but not the runtime source of truth | Freeze the runtime enum first; do not treat the TTD schema as authoritative for ADR-0008 | +| `SeekThen` | `crates/warp-core/src/playback.rs` | No Wesley/runtime-generated equivalent today | Supporting playback-control enum required to express `PlaybackMode::Seek` honestly | +| `WorldlineTick` | `crates/echo-runtime-schema/src/lib.rs` | re-exported by `warp-core` and `echo-wasm-abi` | Shared logical-counter owner now exists; schema must preserve logical-counter semantics | +| `GlobalTick` | `crates/echo-runtime-schema/src/lib.rs` | re-exported by `warp-core` and `echo-wasm-abi` | Shared logical-counter owner now exists; schema/docs must keep correlation-not-time semantics explicit | +| `IntentKind` | `crates/warp-core/src/head_inbox.rs` | No Wesley/runtime-generated equivalent today | Stable opaque hash-backed id; intentionally runtime-owned until a real generated consumer exists | +| `InboxAddress` | `crates/warp-core/src/head_inbox.rs` | ABI/control routing byte/string mirrors in `crates/echo-wasm-abi/src/kernel_port.rs` | Supporting routing alias type needed to freeze `IngressTarget` honestly; intentionally runtime-owned for Phase 8 | +| `InboxPolicy` | `crates/warp-core/src/head_inbox.rs` | No Wesley/runtime-generated equivalent today | Good freeze candidate once variants are confirmed complete for ADR-0008 | +| `IngressTarget` | `crates/warp-core/src/head_inbox.rs` | ABI/control-intent routing mirrors in `crates/echo-wasm-abi/src/kernel_port.rs` | Good freeze candidate; schema must preserve `DefaultWriter` / `InboxAddress` / `ExactHead` split | +| `SchedulerMode` | `crates/echo-wasm-abi/src/kernel_port.rs` | `ControlIntentV1::Start` mapping in `crates/warp-wasm/src/warp_kernel.rs` | Supporting scheduler-control type; `SchedulerStatus.active_mode` cannot be frozen honestly without it | +| `SchedulerStatus` | `crates/echo-wasm-abi/src/kernel_port.rs` | Engine/runtime mapping in `crates/warp-wasm/src/warp_kernel.rs` | This is the real public scheduler result object; Phase 8 should freeze it explicitly instead of reviving `SuperTickResult` | +| `SchedulerState` | `crates/echo-wasm-abi/src/kernel_port.rs` | n/a | Stable scheduler lifecycle enum | +| `WorkState` | `crates/echo-wasm-abi/src/kernel_port.rs` | n/a | Stable scheduler boundary/work-availability enum | +| `RunCompletion` | `crates/echo-wasm-abi/src/kernel_port.rs` | n/a | Stable bounded-run completion enum | +| `RunId` | `crates/echo-runtime-schema/src/lib.rs` | re-exported by `echo-wasm-abi`; runtime/ABI mapping in `crates/warp-wasm/src/warp_kernel.rs` | Supporting control-plane token needed by `SchedulerStatus`; shared owner now exists | +| `HeadEligibility` | `crates/warp-core/src/head.rs` | ABI wrapper in `crates/echo-wasm-abi/src/kernel_port.rs` | Runtime/ABI pair must stay structurally aligned | +| `HeadDisposition` | `crates/echo-wasm-abi/src/kernel_port.rs` | runtime truth derived in `crates/warp-wasm/src/warp_kernel.rs` | ABI-facing scheduler truth surface; freeze alongside `SchedulerStatus` | + +## Current Boundary Shape + +### What Wesley/codegen actually covers today + +- `crates/ttd-manifest/src/lib.rs` vendors TTD IR/schema artifacts. +- `crates/ttd-protocol-rs/lib.rs` is generated from the TTD schema and serves + browser/controller protocol needs. + +That is useful, but it is **not** the Phase 8 runtime freeze target. + +### What Phase 8 has now seeded in-repo + +- `schemas/runtime/artifact-a-identifiers.graphql` +- `schemas/runtime/artifact-b-routing-and-admission.graphql` +- `schemas/runtime/artifact-c-playback-control.graphql` +- `schemas/runtime/artifact-d-scheduler-results.graphql` + +These are the first local, human-authored ADR-0008 runtime schema fragments. +They are source files, not generated output. + +## Proposed Runtime Schema Artifact Set + +Phase 8 should generate from a **runtime-focused** schema set, not from the TTD +browser/controller schema. The first honest artifact sketch is: + +### Artifact A: Runtime identifiers and logical counters + +- `HeadId` +- `WorldlineId` +- `WriterHeadKey` +- `IntentKind` +- `WorldlineTick` +- `GlobalTick` + +Source file: `schemas/runtime/artifact-a-identifiers.graphql` + +These are the low-level, semantically strict building blocks. They need schema +rules for opaque ids and logical counters before any larger DTOs are generated. + +### Artifact B: Runtime routing and admission + +- `InboxAddress` +- `InboxPolicy` +- `IngressTarget` +- `HeadEligibility` + +Source file: `schemas/runtime/artifact-b-routing-and-admission.graphql` + +This artifact covers deterministic ingress routing and declarative admission, +without dragging in transport/conflict surface area. + +### Artifact C: Runtime playback control + +- `PlaybackMode` +- `SeekThen` + +Source file: `schemas/runtime/artifact-c-playback-control.graphql` + +This keeps playback semantics explicit and separate from scheduler lifecycle. + +### Artifact D: Runtime scheduler result surface + +- `SchedulerStatus` +- `SchedulerMode` +- `SchedulerState` +- `WorkState` +- `RunCompletion` +- `RunId` +- `HeadDisposition` + +Source file: `schemas/runtime/artifact-d-scheduler-results.graphql` + +This replaces the stale `SuperTickResult` shorthand with the actual stable +control-plane surface exposed by ABI v3. + +### Deliberately out of this schema set + +- observation DTOs such as `HeadInfo`, `HeadObservation`, and snapshot response + envelopes +- transport/conflict types from ADR-0009 +- TTD/browser/controller protocol events and models + +Those remain adapter- or product-level concerns until the runtime freeze set is +pinned. + +### What remains hand-written today + +- Core runtime types in: + - `crates/warp-core/src/head.rs` + - `crates/warp-core/src/head_inbox.rs` + - `crates/warp-core/src/playback.rs` + - `crates/warp-core/src/clock.rs` +- Host-facing DTO/adapters in: + - `crates/echo-wasm-abi/src/kernel_port.rs` + +The `echo-wasm-abi` types should be treated as **adapter DTOs**, not proof that +the underlying ADR-0008 runtime schema is already frozen. + +## Phase 8 Gaps To Close + +### 1. Missing runtime Wesley schema + +There is not yet a **complete** ADR-0008 runtime schema artifact set in-repo. +This branch now seeds Artifacts A-D under `schemas/runtime/`, but generated IR +and generated Rust output are still missing. + +### 2. Missing generation/sync entrypoint + +The repo advertises `cargo xtask wesley sync`, but `xtask/src/main.rs` does not +implement it yet. Phase 8 needs a real, deterministic regeneration path before +generated artifacts can be trusted. + +For now, this branch keeps the runtime freeze loop Echo-local: + +- the source of truth lives under `schemas/runtime/*.graphql`, +- local validation happens via `pnpm schema:runtime:check`, +- and Wesley sync stays deliberately deferred until the upstream Echo-facing + schema/compiler contract stops moving. + +### 3. Chosen generated owner for shared runtime-schema types + +Phase 8 now has a concrete ownership decision for the generated Rust side: + +- shared opaque ids, logical counters, and structural runtime key types now + live in `crates/echo-runtime-schema`, +- `warp-core` already consumes or re-exports those shared types for the subset + landed in this slice, +- `IntentKind` and `InboxAddress` intentionally remain in `warp-core` because + they are frozen runtime types but not yet worth centralizing, +- and `echo-wasm-abi` should remain adapter-owned for host DTOs and convert to + and from the shared types rather than own a duplicate generated copy. + +### 4. Stale plan naming around scheduler results + +The inventory resolves this now: Phase 8 should freeze `SchedulerStatus`, +`SchedulerState`, `WorkState`, `RunCompletion`, `HeadEligibility`, and +`HeadDisposition` rather than back-porting a stale `SuperTickResult` name for +cosmetic consistency. + +### 5. Opaque id / logical counter mapping rules + +`HeadId`, `IntentKind`, `WorldlineTick`, and `GlobalTick` all have stricter +semantics than "some bytes" or "some integer." Phase 8 needs schema-side rules +for: + +- opaque fixed-size hash ids, +- logical counters that are coordinates rather than time, +- and wrapper-vs-generated ownership boundaries. + +This is now captured in the +[Phase 8 Runtime Schema Mapping Contract](phase-8-runtime-schema-mapping-contract.md). +The remaining Phase 8 work is closeout and explicit deferral, not more +ownership discovery. + +## Recommended Phase 8 Slice Order + +### Slice A: Freeze inventory and naming reconciliation + +- ratify the freeze set +- replace stale `SuperTickResult` wording in the living plan with the explicit + scheduler result surface +- record which surfaces remain adapter-only +- pin the runtime schema artifact split before adding generation + +### Slice B: Runtime schema source of truth + +- add the ADR-0008 runtime schema artifact(s) for the freeze set only +- keep ADR-0009 transport/conflict types out of scope + +### Slice C: Deterministic generation plumbing + +- implement `cargo xtask wesley sync` for the runtime schema path +- ensure generated output is deterministic and reviewable + +### Slice D: Hand-written type reduction + +- replace hand-written mirrors with generated types or thin wrappers where needed +- keep `echo-wasm-abi` as an adapter layer when ABI semantics genuinely differ + +## Out of Scope For This Phase + +- ADR-0009 transport/conflict schema freeze +- TTD browser/controller protocol redesign +- new ABI v4 work unrelated to the runtime freeze set +- footprint/conflict-policy work from Phase 9 + +## Exit Signal For The Inventory Slice + +This prep slice is complete when: + +- the freeze candidates and their owners are written down, +- the stale `SuperTickResult` naming drift is resolved explicitly, +- the first runtime schema artifact set is sketched concretely, +- and the next implementation slice can start from a concrete schema authoring + target instead of inference from Rust code. + +That bar is now met. The next honest Phase 8 slice is Echo-local schema +hardening and deferred-generation contract work, not premature Wesley +integration. diff --git a/docs/spec/SPEC-0009-wasm-abi-v3.md b/docs/spec/SPEC-0009-wasm-abi-v3.md index 88a03f8b..bcf3eeb4 100644 --- a/docs/spec/SPEC-0009-wasm-abi-v3.md +++ b/docs/spec/SPEC-0009-wasm-abi-v3.md @@ -131,8 +131,8 @@ Canonical payload shapes: { "kind": "set_head_eligibility", "head": { - "worldline_id": bytes(32), - "head_id": bytes(32) + "worldline_id": WorldlineId, + "head_id": HeadId }, "eligibility": "dormant" | "admitted" } @@ -181,7 +181,8 @@ The scheduler-facing enums use serde's declared shapes directly: `HeadDisposition` serialize as snake_case text strings. - `SchedulerMode::UntilIdle { cycle_limit }` serializes as `{ "kind": "until_idle", "cycle_limit": }`. -- `HeadKey.worldline_id` and `HeadKey.head_id` are raw `bytes(32)` values. +- `WorldlineId` and `HeadId` are typed opaque wrappers that serialize as + `bytes(32)`. Concrete `scheduler_status()` example: @@ -211,7 +212,7 @@ a865737461746568696e6163746976656672756e5f6964076a776f726b5f73746174656971756965 The request payload for `observe(request)` is canonical-CBOR bytes that decode to: -- `coordinate.worldline_id: bytes(32)` +- `coordinate.worldline_id: WorldlineId` encoded as `bytes(32)` - `coordinate.at: frontier | tick { worldline_tick }` - `frame: commit_boundary | recorded_truth | query_view` - `projection: head | snapshot | truth_channels | query` @@ -234,7 +235,7 @@ to: | Field | Type | Description | | ---------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `observation_version` | u32 | Observation contract version | -| `worldline_id` | bytes(32) | Worldline actually observed | +| `worldline_id` | `WorldlineId` | Worldline actually observed; serialized as `bytes(32)` | | `requested_at` | enum | Original coordinate selector | | `resolved_worldline_tick` | `WorldlineTick` | Resolved coordinate; historical reads use zero-based committed append indices, while `0` plus `commit_global_tick = null` represents empty `U0` frontier metadata | | `commit_global_tick` | `GlobalTick?` | Commit cycle stamp for the resolved commit; `null` means the resolved coordinate is empty `U0` rather than a committed append | diff --git a/package.json b/package.json index a057e6b6..afcb6379 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,14 @@ "scripts": { "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "schema:runtime:check": "node scripts/validate-runtime-schema-fragments.mjs" }, "devDependencies": { "@playwright/test": "^1.48.0", "asciichart": "^1.5.25", + "graphql": "16.11.0", + "prettier": "3.8.1", "vitepress": "1.6.4" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28d53c70..58baff97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,12 @@ importers: asciichart: specifier: ^1.5.25 version: 1.5.25 + graphql: + specifier: 16.11.0 + version: 16.11.0 + prettier: + specifier: 3.8.1 + version: 3.8.1 vitepress: specifier: 1.6.4 version: 1.6.4(@algolia/client-search@5.46.2)(@types/node@24.10.1)(@types/react@18.3.28)(postcss@8.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.6.3) @@ -1131,6 +1137,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -1255,6 +1265,11 @@ packages: preact@10.28.1: resolution: {integrity: sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -2538,6 +2553,8 @@ snapshots: gensync@1.0.0-beta.2: {} + graphql@16.11.0: {} + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -2662,6 +2679,8 @@ snapshots: preact@10.28.1: {} + prettier@3.8.1: {} + property-information@7.1.0: {} react-dom@18.3.1(react@18.3.1): diff --git a/schemas/runtime/README.md b/schemas/runtime/README.md new file mode 100644 index 00000000..447fbef6 --- /dev/null +++ b/schemas/runtime/README.md @@ -0,0 +1,93 @@ + + + +# ADR-0008 Runtime Schema Fragments + +These GraphQL SDL fragments are the **human-authored source of truth** for the +Phase 8 ADR-0008 runtime schema freeze. + +They are intentionally narrower than the browser/TTD protocol schema: + +- they cover stable runtime boundary types only, +- they do **not** include ADR-0009 transport/conflict types, +- and they do **not** replace the current `echo-wasm-abi` adapter DTOs yet. + +## Current Fragments + +- [artifact-a-identifiers.graphql](artifact-a-identifiers.graphql) + Runtime identifiers and logical counters. +- [artifact-b-routing-and-admission.graphql](artifact-b-routing-and-admission.graphql) + Deterministic ingress routing and head-admission policy types. +- [artifact-c-playback-control.graphql](artifact-c-playback-control.graphql) + Playback control modes and seek-follow-up semantics. +- [artifact-d-scheduler-results.graphql](artifact-d-scheduler-results.graphql) + Scheduler lifecycle/result metadata and supporting control-plane types. + +## Intent + +Phase 8 freezes the runtime shape first and wires generation second. + +That means these files are allowed to exist before: + +- `cargo xtask wesley sync` grows a runtime-schema path, +- Wesley IR is vendored for the runtime freeze set, +- or generated Rust replaces hand-written runtime mirrors. + +## Validation + +Run the local fragment validator before touching any generation plumbing: + +```sh +pnpm schema:runtime:check +``` + +The validator does two narrow jobs: + +- parse-check the SDL fragments via the repo's existing `prettier --check` + toolchain path, +- and verify that every referenced runtime type is defined somewhere inside the + local `schemas/runtime/` fragment set. + +This keeps Phase 8 moving without pretending Wesley is already stable enough to +own the runtime freeze loop. + +The current Echo-side mismatch inventory lives in: + +- [Phase 8 Runtime Schema Conformance Audit](../../docs/plans/phase-8-runtime-schema-conformance.md) +- [Phase 8 Runtime Schema Mapping Contract](../../docs/plans/phase-8-runtime-schema-mapping-contract.md) + +## Planned Output Contract + +Generation is explicitly deferred, but the intended artifact boundary is: + +- `schemas/runtime/*.graphql` + Human-authored source fragments for Artifacts A-D. +- `schemas/runtime/generated/runtime-schema.graphql` + Planned normalized single-file runtime schema bundle. +- `schemas/runtime/generated/runtime-ir.json` + Planned vendored Wesley IR snapshot for the frozen runtime schema. +- `schemas/runtime/generated/runtime-manifest.json` + Planned vendored schema manifest/metadata for deterministic regeneration. +- `crates/echo-runtime-schema/src/lib.rs` + Current Echo-local shared Rust home for runtime logical counters and core + opaque ids/key types; planned generated home once runtime Wesley sync exists. + +`echo-wasm-abi` remains adapter-owned even after that crate exists. It should +convert to and from the shared runtime-schema types rather than own a second +generated copy of `WorldlineTick`, `GlobalTick`, or `RunId`, and it should keep +opaque-id wrappers explicit where CBOR byte encoding differs from the runtime +semantic type. + +Not every frozen schema type must move into `echo-runtime-schema`. Phase 8 +intentionally leaves runtime-local behavior types such as `IntentKind` and +`InboxAddress` hand-written in `warp-core` until a real shared/generated +consumer exists. + +## Notes + +- These files are SDL **fragments**, not a standalone executable GraphQL API. +- Comments here carry semantic constraints that current GraphQL type syntax + cannot express directly, such as opaque-hash ids and logical-counter rules. +- Generation plumbing (`cargo xtask wesley sync`) still does not exist for this + runtime schema tree; Phase 8 is pinning and validating source files before + generation. diff --git a/schemas/runtime/artifact-a-identifiers.graphql b/schemas/runtime/artifact-a-identifiers.graphql new file mode 100644 index 00000000..0d087f70 --- /dev/null +++ b/schemas/runtime/artifact-a-identifiers.graphql @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 +# © James Ross Ω FLYING•ROBOTS + +# ADR-0008 Phase 8 runtime schema fragment: identifiers and logical counters. +# These scalar and key definitions pin the low-level runtime coordinate system +# before any generated Rust or adapter DTO work begins. + +""" +Opaque stable identifier for a writer head. + +Semantic rule: this is a fixed-width hash-backed identifier. Schema consumers +must preserve opacity and must not reinterpret it as a human-readable string +label or derive ordering semantics beyond byte-stable identity. +""" +scalar HeadId + +""" +Opaque stable identifier for a worldline. + +Semantic rule: this remains an opaque identifier even though implementations +often derive it from the initial worldline state. +""" +scalar WorldlineId + +""" +Opaque stable identifier for an intent kind. + +Semantic rule: this is a domain-separated, hash-backed identifier. Schema +consumers must not collapse it to a display label. +""" +scalar IntentKind + +""" +Per-worldline logical append coordinate. + +Semantic rule: this is a Lamport-style logical counter, not wall-clock time and +not a runtime-global ordering cursor. +""" +scalar WorldlineTick + +""" +Runtime-global logical cycle coordinate. + +Semantic rule: this is correlation metadata for runtime scheduler cycles, not +wall-clock time and not per-worldline append identity. +""" +scalar GlobalTick + +""" +Positive integer used where runtime semantics require Rust `u32` values but the +schema still needs a scalar-level positivity contract. + +Semantic rule: values span the full positive `u32` range `1..=4_294_967_295`. +Generators must not silently narrow this to GraphQL signed-`Int` semantics. +""" +scalar PositiveInt + +""" +Stable composite key identifying a writer head inside a worldline. +""" +type WriterHeadKey { + worldlineId: WorldlineId! + headId: HeadId! +} + +""" +Input mirror for exact-head routing and control surfaces. +""" +input WriterHeadKeyInput { + worldlineId: WorldlineId! + headId: HeadId! +} diff --git a/schemas/runtime/artifact-b-routing-and-admission.graphql b/schemas/runtime/artifact-b-routing-and-admission.graphql new file mode 100644 index 00000000..93a23626 --- /dev/null +++ b/schemas/runtime/artifact-b-routing-and-admission.graphql @@ -0,0 +1,136 @@ +# SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 +# © James Ross Ω FLYING•ROBOTS + +# ADR-0008 Phase 8 runtime schema fragment: routing and admission. +# This fragment freezes deterministic ingress routing and declarative scheduler +# admission for writer heads without dragging in ADR-0009 transport/conflict +# surface area. + +""" +Named inbox alias within a worldline. + +Semantic rule: this is a stable application-facing address, not an internal +head identity. +""" +scalar InboxAddress + +""" +Declarative scheduler admission for a writer head. +""" +enum HeadEligibility { + DORMANT + ADMITTED +} + +""" +Discriminant for the input-form ingress target envelope. + +GraphQL does not have native input unions, so the input surface uses a +discriminated wrapper until Wesley grows an equivalent first-class construct. +""" +enum IngressTargetKind { + DEFAULT_WRITER + INBOX_ADDRESS + EXACT_HEAD +} + +""" +Discriminant for the input-form inbox policy envelope. + +GraphQL does not have native input unions, so the input surface uses a +discriminated wrapper until Wesley grows an equivalent first-class construct. +""" +enum InboxPolicyKind { + ACCEPT_ALL + KIND_FILTER + BUDGETED +} + +""" +Route to the default writer for a worldline. +""" +type DefaultWriterTarget { + worldlineId: WorldlineId! +} + +""" +Route to a named inbox within a worldline. +""" +type InboxAddressTarget { + worldlineId: WorldlineId! + inbox: InboxAddress! +} + +""" +Route directly to an exact writer head. +""" +type ExactHeadTarget { + headKey: WriterHeadKey! +} + +""" +Deterministic ingress target for runtime routing. +""" +union IngressTarget = DefaultWriterTarget | InboxAddressTarget | ExactHeadTarget + +""" +Input-form routing target used for configuration and control operations. + +Validation rules: +- `DEFAULT_WRITER` requires `worldlineId` +- `INBOX_ADDRESS` requires `worldlineId` and `inbox` +- `EXACT_HEAD` requires `headKey` +""" +input IngressTargetInput { + kind: IngressTargetKind! + worldlineId: WorldlineId + inbox: InboxAddress + headKey: WriterHeadKeyInput +} + +""" +Accept all ingressed envelopes. +""" +type InboxPolicyAcceptAll { + kind: InboxPolicyKind! +} + +""" +Accept only a bounded set of intent kinds. +""" +type InboxPolicyKindFilter { + kind: InboxPolicyKind! + intentKinds: [IntentKind!]! +} + +""" +Accept only up to `maxPerTick` envelopes per scheduler cycle. + +Semantic rule: `maxPerTick` must be positive. +""" +type InboxPolicyBudgeted { + kind: InboxPolicyKind! + maxPerTick: PositiveInt! +} + +""" +Deterministic inbox admission policy. +""" +union InboxPolicy = + | InboxPolicyAcceptAll + | InboxPolicyKindFilter + | InboxPolicyBudgeted + +""" +Input-form inbox policy used for control/configuration surfaces. + +Validation rules: +- `ACCEPT_ALL` must not provide `intentKinds` or `maxPerTick` +- `KIND_FILTER` requires non-empty `intentKinds` +- `BUDGETED` requires positive `maxPerTick` +""" +input InboxPolicyInput { + kind: InboxPolicyKind! + intentKinds: [IntentKind!] + maxPerTick: PositiveInt +} diff --git a/schemas/runtime/artifact-c-playback-control.graphql b/schemas/runtime/artifact-c-playback-control.graphql new file mode 100644 index 00000000..10bab124 --- /dev/null +++ b/schemas/runtime/artifact-c-playback-control.graphql @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 +# © James Ross Ω FLYING•ROBOTS + +# ADR-0008 Phase 8 runtime schema fragment: playback control. +# This fragment freezes the control-plane playback mode surface without mixing +# it into scheduler lifecycle or observation DTOs. + +""" +Follow-up behavior after a seek completes. +""" +enum SeekThen { + PAUSE + PLAY +} + +""" +Discriminant for the input-form playback mode envelope. + +GraphQL does not have native input unions, so playback control uses a +discriminated input wrapper until Wesley grows an equivalent first-class +construct. +""" +enum PlaybackModeKind { + PAUSED + PLAY + STEP_FORWARD + STEP_BACK + SEEK +} + +""" +Paused playback state. +""" +type PlaybackModePaused { + kind: PlaybackModeKind! +} + +""" +Continuous forward playback state. +""" +type PlaybackModePlay { + kind: PlaybackModeKind! +} + +""" +Advance one tick then pause. +""" +type PlaybackModeStepForward { + kind: PlaybackModeKind! +} + +""" +Seek one tick backward then pause. +""" +type PlaybackModeStepBack { + kind: PlaybackModeKind! +} + +""" +Seek to a specific worldline tick, then apply a follow-up behavior. +""" +type PlaybackModeSeek { + kind: PlaybackModeKind! + target: WorldlineTick! + then: SeekThen! +} + +""" +Playback control state. +""" +union PlaybackMode = + | PlaybackModePaused + | PlaybackModePlay + | PlaybackModeStepForward + | PlaybackModeStepBack + | PlaybackModeSeek + +""" +Input-form playback control state. + +Validation rules: +- `PAUSED`, `PLAY`, `STEP_FORWARD`, and `STEP_BACK` must not provide `target` + or `then` +- `SEEK` requires both `target` and `then` +""" +input PlaybackModeInput { + kind: PlaybackModeKind! + target: WorldlineTick + then: SeekThen +} diff --git a/schemas/runtime/artifact-d-scheduler-results.graphql b/schemas/runtime/artifact-d-scheduler-results.graphql new file mode 100644 index 00000000..30b2648d --- /dev/null +++ b/schemas/runtime/artifact-d-scheduler-results.graphql @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 +# © James Ross Ω FLYING•ROBOTS + +# ADR-0008 Phase 8 runtime schema fragment: scheduler result surface. +# This fragment freezes the public runtime scheduler metadata surface that +# replaced the old `SuperTickResult` shorthand. + +""" +Opaque control-plane run generation token. + +Semantic rule: this is not provenance, not replay state, and not a worldline +coordinate. It is valid only within one initialized kernel lifetime. +""" +scalar RunId + +""" +Discriminant for the scheduler mode envelope. +""" +enum SchedulerModeKind { + UNTIL_IDLE +} + +""" +Declared scheduler mode visible to hosts. + +Semantic rule: when `cycleLimit` is present, it must be positive. +""" +type SchedulerModeUntilIdle { + kind: SchedulerModeKind! + cycleLimit: PositiveInt +} + +""" +Current scheduler mode. +""" +union SchedulerMode = SchedulerModeUntilIdle + +""" +Input-form scheduler mode used by control intents. + +Validation rules: +- `UNTIL_IDLE` may omit `cycleLimit` +- when provided, `cycleLimit` must be positive +""" +input SchedulerModeInput { + kind: SchedulerModeKind! + cycleLimit: PositiveInt +} + +""" +Scheduler lifecycle state. +""" +enum SchedulerState { + INACTIVE + RUNNING + STOPPING +} + +""" +Work availability at the current scheduler boundary. +""" +enum WorkState { + QUIESCENT + RUNNABLE_PENDING + BLOCKED_ONLY +} + +""" +Completion reason for the most recent run. +""" +enum RunCompletion { + QUIESCED + BLOCKED_ONLY + CYCLE_LIMIT_REACHED + STOPPED +} + +""" +Runtime truth about a head's scheduler disposition. +""" +enum HeadDisposition { + DORMANT + RUNNABLE + BLOCKED + RETIRED +} + +""" +Current scheduler metadata. +""" +type SchedulerStatus { + state: SchedulerState! + activeMode: SchedulerMode + workState: WorkState! + runId: RunId + latestCycleGlobalTick: GlobalTick + latestCommitGlobalTick: GlobalTick + lastQuiescentGlobalTick: GlobalTick + lastRunCompletion: RunCompletion +} diff --git a/scripts/check-append-only.js b/scripts/check-append-only.js index e736856e..14b08f60 100644 --- a/scripts/check-append-only.js +++ b/scripts/check-append-only.js @@ -3,10 +3,7 @@ import { execFileSync } from "node:child_process"; -const files = [ - "AGENTS.md", - "TASKS-DAG.md", -]; +const files = ["AGENTS.md", "docs/archive/tasks/TASKS-DAG.md"]; const args = process.argv.slice(2); const baseArgIndex = args.indexOf("--base"); @@ -31,9 +28,13 @@ const errors = []; for (const file of files) { let diffOutput = ""; try { - diffOutput = execFileSync("git", ["diff", "--numstat", baseRef, "--", file], { - encoding: "utf8", - }); + diffOutput = execFileSync( + "git", + ["diff", "--numstat", baseRef, "--", file], + { + encoding: "utf8", + }, + ); } catch (err) { const stderr = err?.stderr?.toString?.() ?? ""; if (err?.code === "ENOENT") { @@ -75,7 +76,9 @@ for (const file of files) { } if (errors.length > 0) { - throw new Error(["Append-only invariant violations detected:", ...errors].join("\n")); + throw new Error( + ["Append-only invariant violations detected:", ...errors].join("\n"), + ); } console.log(`Append-only check passed (base: ${baseRef}).`); diff --git a/scripts/generate-dependency-dags.js b/scripts/generate-dependency-dags.js index 2246db2a..a025bf0e 100644 --- a/scripts/generate-dependency-dags.js +++ b/scripts/generate-dependency-dags.js @@ -71,7 +71,10 @@ function maybeWrapMilestonesJson(json) { return { generated_at: null, milestones: json }; } if (json && typeof json === "object" && Array.isArray(json.milestones)) { - return { generated_at: json.generated_at ?? null, milestones: json.milestones }; + return { + generated_at: json.generated_at ?? null, + milestones: json.milestones, + }; } fail( `Unsupported milestones JSON format: expected an array or { milestones: [...] }.`, @@ -79,9 +82,9 @@ function maybeWrapMilestonesJson(json) { } function formatDateYYYYMMDD(date) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, "0"); + const day = String(date.getUTCDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } @@ -100,7 +103,7 @@ function parseArgs(argv) { milestonesJson: ".cache/echo/deps/milestones-all.json", configJson: "docs/assets/dags/deps-config.json", outDir: "docs/assets/dags", - tasksDagPath: "TASKS-DAG.md", + tasksDagPath: path.join("docs", "archive", "tasks", "TASKS-DAG.md"), snapshot: null, snapshotLabelMode: "auto", }; @@ -135,7 +138,7 @@ function parseArgs(argv) { " --milestones-json Read/write milestones snapshot JSON", " --config Dependency config (edges) JSON", " --out-dir Output directory for DOT/SVG", - " --tasks-dag Path to TASKS-DAG.md (reality edges)", + " --tasks-dag Path to docs/archive/tasks/TASKS-DAG.md (reality edges)", " --snapshot Override label date in output graphs (legacy; prefer --snapshot-label)", " --snapshot-label Snapshot label: auto|none|rolling|YYYY-MM-DD", "", @@ -167,7 +170,8 @@ function resolveSnapshotLabel({ snapshot, snapshotLabelMode, generatedAt }) { if (modeRaw === "none") return { mode: "none", label: null }; if (modeRaw === "rolling") return { mode: "rolling", label: "rolling" }; if (modeRaw === "auto") { - const fallback = generatedAt?.slice(0, 10) ?? formatDateYYYYMMDD(new Date()); + const fallback = + generatedAt?.slice(0, 10) ?? formatDateYYYYMMDD(new Date()); return { mode: "date", label: fallback }; } @@ -237,9 +241,12 @@ function fetchAllMilestonesSnapshot(nameWithOwner) { } function confidenceEdgeAttrs(confidence) { - if (confidence === "strong") return 'color="black", penwidth=1.4, style="solid"'; - if (confidence === "medium") return 'color="gray40", penwidth=1.2, style="dashed"'; - if (confidence === "weak") return 'color="gray70", penwidth=1.2, style="dotted"'; + if (confidence === "strong") + return 'color="black", penwidth=1.4, style="solid"'; + if (confidence === "medium") + return 'color="gray40", penwidth=1.2, style="dashed"'; + if (confidence === "weak") + return 'color="gray70", penwidth=1.2, style="dotted"'; fail(`Unknown confidence: ${confidence}`); } @@ -293,7 +300,9 @@ function emitIssueDot({ issues, issueEdges, snapshotLabel, realityEdges }) { const missing = [...nodes].filter((n) => !byNum.has(n)).sort((a, b) => a - b); if (missing.length) { - console.warn(`Issue DAG: dropping missing issue ids (not in snapshot): ${missing.join(", ")}`); + console.warn( + `Issue DAG: dropping missing issue ids (not in snapshot): ${missing.join(", ")}`, + ); } // Filter nodes absent from the snapshot (config or reality edges referencing unknown issues); they are dropped before rendering. const validNodes = [...nodes].filter((n) => byNum.has(n)); @@ -309,7 +318,9 @@ function emitIssueDot({ issues, issueEdges, snapshotLabel, realityEdges }) { const lines = []; lines.push("// SPDX-License-Identifier: Apache-2.0"); - lines.push("// © James Ross Ω FLYING•ROBOTS "); + lines.push( + "// © James Ross Ω FLYING•ROBOTS ", + ); lines.push("digraph echo_issue_dependencies {"); lines.push( ' graph [rankdir=LR, labelloc="t", fontsize=18, fontname="Helvetica", newrank=true, splines=true];', @@ -337,8 +348,12 @@ function emitIssueDot({ issues, issueEdges, snapshotLabel, realityEdges }) { lines.push(' L1 [label="strong", fillcolor="#ffffff"];'); lines.push(' L2 [label="medium", fillcolor="#ffffff"];'); lines.push(' L3 [label="weak", fillcolor="#ffffff"];'); - lines.push(' LG [label="confirmed (reality)", color="green", fontcolor="green"];'); - lines.push(' LR [label="missing from plan", color="red", fontcolor="red"];'); + lines.push( + ' LG [label="confirmed (reality)", color="green", fontcolor="green"];', + ); + lines.push( + ' LR [label="missing from plan", color="red", fontcolor="red"];', + ); lines.push( ` L1 -> L2 [arrowhead=none, ${confidenceEdgeAttrs("strong")}];`, ); @@ -354,10 +369,7 @@ function emitIssueDot({ issues, issueEdges, snapshotLabel, realityEdges }) { ); for (const msTitle of sortedGroupTitles) { const clusterId = - "cluster_" + - msTitle - .replaceAll(/[^a-zA-Z0-9]/g, "_") - .slice(0, 48); + "cluster_" + msTitle.replaceAll(/[^a-zA-Z0-9]/g, "_").slice(0, 48); const fill = milestoneFillFor(msTitle); lines.push(` subgraph ${clusterId} {`); lines.push(` label="${escapeDotString(msTitle)}";`); @@ -404,9 +416,9 @@ function emitIssueDot({ issues, issueEdges, snapshotLabel, realityEdges }) { if (!realityEdge) continue; const { from: u, to: v } = realityEdge; if (byNum.has(u) && byNum.has(v)) { - lines.push( - ` i${u} -> i${v} [color="red", penwidth=2.0, style="dashed", tooltip="Inferred from TASKS-DAG.md (missing from Plan)"];` - ); + lines.push( + ` i${u} -> i${v} [color="red", penwidth=2.0, style="dashed", tooltip="Inferred from TASKS-DAG.md (missing from Plan)"];`, + ); } } } @@ -442,7 +454,9 @@ function emitMilestoneDot({ milestones, milestoneEdges, snapshotLabel }) { const lines = []; lines.push("// SPDX-License-Identifier: Apache-2.0"); - lines.push("// © James Ross Ω FLYING•ROBOTS "); + lines.push( + "// © James Ross Ω FLYING•ROBOTS ", + ); lines.push("digraph echo_milestone_dependencies {"); lines.push( ' graph [rankdir=LR, labelloc="t", fontsize=18, fontname="Helvetica", splines=true];', @@ -511,8 +525,13 @@ function main() { const args = parseArgs(process.argv); const config = readJsonFile(args.configJson); - if (!Array.isArray(config.issue_edges) || !Array.isArray(config.milestone_edges)) { - fail(`Invalid config: expected issue_edges and milestone_edges arrays in ${args.configJson}`); + if ( + !Array.isArray(config.issue_edges) || + !Array.isArray(config.milestone_edges) + ) { + fail( + `Invalid config: expected issue_edges and milestone_edges arrays in ${args.configJson}`, + ); } if (args.fetch) { @@ -535,15 +554,19 @@ function main() { } const issuesWrapped = maybeWrapIssuesJson(readJsonFile(args.issuesJson)); - const milestonesWrapped = maybeWrapMilestonesJson(readJsonFile(args.milestonesJson)); + const milestonesWrapped = maybeWrapMilestonesJson( + readJsonFile(args.milestonesJson), + ); let realityEdges = null; - const tasksDagPath = path.resolve(process.cwd(), args.tasksDagPath ?? "TASKS-DAG.md"); + const tasksDagPath = path.resolve(process.cwd(), args.tasksDagPath); if (fs.existsSync(tasksDagPath)) { try { const tasksDagContent = fs.readFileSync(tasksDagPath, "utf8"); const { edges: tasksDagEdges } = parseTasksDag(tasksDagContent); - realityEdges = new Set(tasksDagEdges.map((edge) => `${edge.from}->${edge.to}`)); + realityEdges = new Set( + tasksDagEdges.map((edge) => `${edge.from}->${edge.to}`), + ); } catch (err) { console.warn( `Warning: failed to parse ${tasksDagPath} for reality edges: ${err?.message ?? err}`, @@ -578,8 +601,18 @@ function main() { fs.writeFileSync(milestoneDotPath, milestoneDot, "utf8"); if (args.render) { - runChecked("dot", ["-Tsvg", issueDotPath, "-o", path.join(args.outDir, "issue-deps.svg")]); - runChecked("dot", ["-Tsvg", milestoneDotPath, "-o", path.join(args.outDir, "milestone-deps.svg")]); + runChecked("dot", [ + "-Tsvg", + issueDotPath, + "-o", + path.join(args.outDir, "issue-deps.svg"), + ]); + runChecked("dot", [ + "-Tsvg", + milestoneDotPath, + "-o", + path.join(args.outDir, "milestone-deps.svg"), + ]); } process.stdout.write( @@ -587,8 +620,12 @@ function main() { "Generated dependency DAGs:", `- ${issueDotPath}`, `- ${milestoneDotPath}`, - args.render ? `- ${path.join(args.outDir, "issue-deps.svg")}` : "- (SVGs not rendered; pass --render)", - args.render ? `- ${path.join(args.outDir, "milestone-deps.svg")}` : "- (SVGs not rendered; pass --render)", + args.render + ? `- ${path.join(args.outDir, "issue-deps.svg")}` + : "- (SVGs not rendered; pass --render)", + args.render + ? `- ${path.join(args.outDir, "milestone-deps.svg")}` + : "- (SVGs not rendered; pass --render)", "", "Tip: fetch fresh GitHub data with --fetch (requires gh auth + network).", ].join("\n") + "\n", diff --git a/scripts/generate-tasks-dag.js b/scripts/generate-tasks-dag.js index 7d7639b3..4527c515 100644 --- a/scripts/generate-tasks-dag.js +++ b/scripts/generate-tasks-dag.js @@ -7,19 +7,35 @@ import { spawnSync } from "node:child_process"; import { parseTasksDag } from "./parse-tasks-dag.js"; import { escapeDotString } from "./dag-utils.js"; -const INPUT_FILE = "TASKS-DAG.md"; +const INPUT_FILE = path.join("docs", "archive", "tasks", "TASKS-DAG.md"); const OUT_DIR = "docs/assets/dags"; const DOT_FILE = path.join(OUT_DIR, "tasks-dag.dot"); const SVG_FILE = path.join(OUT_DIR, "tasks-dag.svg"); // Cluster heuristic: match known prefixes at the start of issue titles to group related work. // Prefix list is configurable via docs/assets/dags/clusters-config.json (array of strings); we fall back to this default. const DEFAULT_CLUSTER_PREFIXES = [ - "TT0", "TT1", "TT2", "TT3", - "S1", "M1", "M2", "M4", "W1", - "Demo 2", "Demo 3", - "Spec:", "Draft", "Tooling:", "Backlog:", + "TT0", + "TT1", + "TT2", + "TT3", + "S1", + "M1", + "M2", + "M4", + "W1", + "Demo 2", + "Demo 3", + "Spec:", + "Draft", + "Tooling:", + "Backlog:", ]; -const CLUSTER_CONFIG_PATH = path.join("docs", "assets", "dags", "clusters-config.json"); +const CLUSTER_CONFIG_PATH = path.join( + "docs", + "assets", + "dags", + "clusters-config.json", +); function loadClusterPrefixes() { try { @@ -28,10 +44,14 @@ function loadClusterPrefixes() { if (Array.isArray(parsed) && parsed.every((p) => typeof p === "string")) { return parsed; } - console.warn(`clusters-config.json is invalid (expected array of strings); using defaults.`); + console.warn( + `clusters-config.json is invalid (expected array of strings); using defaults.`, + ); } catch (err) { if (err?.code !== "ENOENT") { - console.warn(`Failed to read ${CLUSTER_CONFIG_PATH}: ${err.message}; using defaults.`); + console.warn( + `Failed to read ${CLUSTER_CONFIG_PATH}: ${err.message}; using defaults.`, + ); } } return DEFAULT_CLUSTER_PREFIXES; @@ -44,15 +64,23 @@ function fail(message) { } function runChecked(cmd, args) { - const result = spawnSync(cmd, args, { encoding: "utf8", timeout: 30000, killSignal: "SIGTERM" }); + const result = spawnSync(cmd, args, { + encoding: "utf8", + timeout: 30000, + killSignal: "SIGTERM", + }); if (result.error && result.error.code === "ETIMEDOUT") { fail(`Command timed out: ${cmd} ${args.join(" ")}`); } - if (result.timedOut || (result.status === null && result.signal === "SIGTERM")) { + if ( + result.timedOut || + (result.status === null && result.signal === "SIGTERM") + ) { fail(`Command timed out: ${cmd} ${args.join(" ")}`); } if (result.error) fail(`Failed to run ${cmd}: ${result.error.message}`); - if (result.status !== 0) fail(`Command failed: ${cmd} ${args.join(" ")}\n${result.stderr}`); + if (result.status !== 0) + fail(`Command failed: ${cmd} ${args.join(" ")}\n${result.stderr}`); return result.stdout; } @@ -71,7 +99,10 @@ function wrapLabel(text, maxLineLength = 30) { } continue; } - if ((current + (current ? " " : "") + word).length > maxLineLength && current.length) { + if ( + (current + (current ? " " : "") + word).length > maxLineLength && + current.length + ) { lines.push(current); current = word; } else { @@ -118,7 +149,7 @@ function generateDot(nodes, edges) { connectedNodeIds.add(e.from); connectedNodeIds.add(e.to); } - + // Create a filtered map of nodes const filteredNodes = new Map(); for (const [id, node] of nodes) { @@ -132,19 +163,27 @@ function generateDot(nodes, edges) { ); const lines = []; - lines.push('digraph tasks_dag {'); - lines.push(' graph [rankdir=LR, labelloc="t", fontsize=18, fontname="Helvetica", newrank=true, splines=true];'); - lines.push(' node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10, margin="0.10,0.06"];'); + lines.push("digraph tasks_dag {"); + lines.push( + ' graph [rankdir=LR, labelloc="t", fontsize=18, fontname="Helvetica", newrank=true, splines=true];', + ); + lines.push( + ' node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10, margin="0.10,0.06"];', + ); lines.push(' edge [fontname="Helvetica", fontsize=9, arrowsize=0.8];'); - lines.push(' label="Echo — Tasks DAG (from TASKS-DAG.md)\nGenerated by scripts/generate-tasks-dag.js";'); - lines.push(''); + lines.push( + ` label="Echo — Tasks DAG (from ${escapeDotString(INPUT_FILE)})\\nGenerated by scripts/generate-tasks-dag.js";`, + ); + lines.push(""); lines.push(" subgraph cluster_legend {"); lines.push(' label="Legend";'); lines.push(' color="gray70";'); lines.push(' fontcolor="gray30";'); lines.push(' style="rounded";'); - lines.push(' LG [label="confirmed in TASKS-DAG.md", color="green", fontcolor="green"];'); + lines.push( + ` LG [label="confirmed in ${escapeDotString(INPUT_FILE)}", color="green", fontcolor="green"];`, + ); lines.push(" }"); lines.push(""); @@ -163,21 +202,32 @@ function generateDot(nodes, edges) { lines.push(` label="${escapeDotString(name)}";`); lines.push(' style="rounded"; color="gray70";'); // Simple color cycle for clusters - const colors = ["#dbeafe", "#dcfce7", "#ffedd5", "#f3f4f6", "#fef9c3", "#ede9fe", "#ccfbf1", "#fee2e2"]; + const colors = [ + "#dbeafe", + "#dcfce7", + "#ffedd5", + "#f3f4f6", + "#fef9c3", + "#ede9fe", + "#ccfbf1", + "#fee2e2", + ]; const color = colors[hashString(name) % colors.length]; lines.push(` node [fillcolor="${color}"];`); - + for (const node of groupNodes) { const label = `#${node.number}\n${node.title}`; let safeLabel = wrapLabel(label, 30); safeLabel = escapeDotString(safeLabel); - lines.push(` i${node.number} [label="${safeLabel}", URL="${escapeDotString(node.url)}", tooltip="${escapeDotString(node.title)}"];`); + lines.push( + ` i${node.number} [label="${safeLabel}", URL="${escapeDotString(node.url)}", tooltip="${escapeDotString(node.title)}"];`, + ); } - lines.push(' }'); + lines.push(" }"); } - lines.push(''); + lines.push(""); for (const edge of edges) { if (filteredNodes.has(edge.from) && filteredNodes.has(edge.to)) { const attrs = confidenceAttrs(edge.confidence); @@ -187,18 +237,18 @@ function generateDot(nodes, edges) { } } - lines.push('}'); + lines.push("}"); return lines.join("\n"); } function main() { if (!fs.existsSync(INPUT_FILE)) fail(`Input file not found: ${INPUT_FILE}`); - + const content = fs.readFileSync(INPUT_FILE, "utf8"); const { nodes, edges } = parseTasksDag(content); - + const dotContent = generateDot(nodes, edges); - + if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true }); fs.writeFileSync(DOT_FILE, dotContent); console.log(`Wrote DOT file to ${DOT_FILE}`); @@ -209,7 +259,7 @@ function main() { } catch (e) { console.warn( "Warning: Failed to render SVG (is graphviz installed?). Only DOT file generated.", - e?.message ?? e + e?.message ?? e, ); } } diff --git a/scripts/validate-runtime-schema-fragments.mjs b/scripts/validate-runtime-schema-fragments.mjs new file mode 100644 index 00000000..08ce5838 --- /dev/null +++ b/scripts/validate-runtime-schema-fragments.mjs @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) James Ross FLYING-ROBOTS + +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { basename, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parse, visit } from "graphql"; + +const BUILTIN_TYPES = new Set(["String", "Int", "Float", "Boolean", "ID"]); +const DEFINITION_KIND_NAMES = new Map([ + ["ScalarTypeDefinition", "scalar"], + ["ObjectTypeDefinition", "type"], + ["InputObjectTypeDefinition", "input"], + ["EnumTypeDefinition", "enum"], + ["UnionTypeDefinition", "union"], +]); +const SCRIPT_DIR = fileURLToPath(new URL(".", import.meta.url)); +const REPO_ROOT = resolve(SCRIPT_DIR, ".."); + +function usage() { + console.error( + "usage: node scripts/validate-runtime-schema-fragments.mjs [--dir ]", + ); +} + +function parseArgs(argv) { + let schemaDir = "schemas/runtime"; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--dir") { + const next = argv[index + 1]; + if (!next) { + usage(); + process.exit(2); + } + schemaDir = next; + index += 1; + continue; + } + if (arg === "--help" || arg === "-h") { + usage(); + process.exit(0); + } + + usage(); + console.error(`unexpected argument: ${arg}`); + process.exit(2); + } + + return resolve(schemaDir); +} + +function listSchemaFiles(schemaDir) { + const files = readdirSync(schemaDir) + .filter((entry) => entry.endsWith(".graphql")) + .sort() + .map((entry) => join(schemaDir, entry)); + + if (files.length === 0) { + throw new Error(`no runtime schema fragments found under ${schemaDir}`); + } + + return files; +} + +function resolvePrettierInvocation() { + const localPrettier = resolve( + REPO_ROOT, + "node_modules", + ".bin", + process.platform === "win32" ? "prettier.cmd" : "prettier", + ); + const candidates = [ + { command: "npx", prefix: ["prettier"] }, + { command: "pnpm", prefix: ["exec", "prettier"] }, + { command: localPrettier, prefix: [], requireExists: true }, + ]; + + for (const candidate of candidates) { + if (candidate.requireExists && !existsSync(candidate.command)) { + continue; + } + const probe = spawnSync( + candidate.command, + [...candidate.prefix, "--version"], + { encoding: "utf8" }, + ); + if (!probe.error && probe.status === 0) { + return candidate; + } + } + + throw new Error( + "failed to locate prettier via npx, pnpm exec, or node_modules/.bin", + ); +} + +function runPrettierCheck(files) { + const formattingErrors = []; + const prettier = resolvePrettierInvocation(); + + for (const file of files) { + const source = readFileSync(file, "utf8"); + const syntheticFilePath = join( + REPO_ROOT, + "schemas/runtime", + basename(file), + ); + const result = spawnSync( + prettier.command, + [ + ...prettier.prefix, + "--parser", + "graphql", + "--stdin-filepath", + syntheticFilePath, + ], + { + encoding: "utf8", + input: source, + }, + ); + + if (result.error) { + throw new Error( + `failed to run prettier for schema validation: ${result.error.message}`, + ); + } + + if (result.status !== 0) { + if (result.stderr) { + process.stderr.write(result.stderr); + } + process.exit(result.status ?? 1); + } + + if (result.stdout !== source) { + formattingErrors.push(file); + } + } + + if (formattingErrors.length > 0) { + console.error("runtime schema formatting check failed:"); + for (const file of formattingErrors) { + console.error(` - ${file}`); + } + process.exit(1); + } +} + +function lineForNode(node) { + return node?.loc?.startToken?.line ?? 1; +} + +function parseDocuments(files) { + return files.map((file) => { + const source = readFileSync(file, "utf8"); + try { + return { + file, + document: parse(source, { noLocation: false }), + }; + } catch (error) { + if (error instanceof Error && "locations" in error) { + const line = error.locations?.[0]?.line ?? 1; + throw new Error(`${file}:${line}: ${error.message}`); + } + throw error; + } + }); +} + +function collectDefinitions(documents, definitions, errors) { + for (const { file, document } of documents) { + for (const definition of document.definitions) { + const kind = DEFINITION_KIND_NAMES.get(definition.kind); + if (!kind || !("name" in definition) || !definition.name) { + continue; + } + + const name = definition.name.value; + const line = lineForNode(definition.name); + const previous = definitions.get(name); + if (previous) { + errors.push( + `${file}:${line}: duplicate ${kind} ${name}; already defined at ${previous.file}:${previous.line}`, + ); + continue; + } + + definitions.set(name, { kind, file, line }); + } + } +} + +function validateReference(file, lineNumber, refName, definitions, errors) { + if (BUILTIN_TYPES.has(refName)) { + return; + } + + if (!definitions.has(refName)) { + errors.push( + `${file}:${lineNumber}: missing referenced type ${refName} in runtime schema fragments`, + ); + } +} + +function validateReferences(documents, definitions, errors) { + for (const { file, document } of documents) { + visit(document, { + NamedType(node) { + validateReference( + file, + lineForNode(node.name), + node.name.value, + definitions, + errors, + ); + }, + }); + } +} + +function main() { + const schemaDir = parseArgs(process.argv.slice(2)); + const files = listSchemaFiles(schemaDir); + + runPrettierCheck(files); + + const documents = parseDocuments(files); + const definitions = new Map(); + const errors = []; + + collectDefinitions(documents, definitions, errors); + validateReferences(documents, definitions, errors); + + if (errors.length > 0) { + console.error("runtime schema validation failed:"); + for (const error of errors) { + console.error(` - ${error}`); + } + process.exit(1); + } + + console.log( + `runtime schema validation passed for ${files.length} fragment(s) under ${schemaDir}`, + ); +} + +try { + main(); +} catch (error) { + console.error( + error instanceof Error + ? error.message + : "runtime schema validation failed", + ); + process.exit(1); +} diff --git a/scripts/verify-local.sh b/scripts/verify-local.sh index 2a58cc6f..fe627582 100755 --- a/scripts/verify-local.sh +++ b/scripts/verify-local.sh @@ -698,6 +698,7 @@ run_docs_lint() { local discovered_md_files=() local md_files=() local md_file + local should_validate_runtime_schema=0 mapfile -t discovered_md_files < <(printf '%s\n' "$CHANGED_FILES" | awk '/\.md$/ {print}') for md_file in "${discovered_md_files[@]}"; do @@ -705,15 +706,37 @@ run_docs_lint() { md_files+=("$md_file") fi done - if [[ ${#md_files[@]} -eq 0 ]]; then + + while IFS= read -r changed_file; do + [[ -z "$changed_file" ]] && continue + case "$changed_file" in + schemas/runtime/*.graphql|scripts/validate-runtime-schema-fragments.mjs|tests/hooks/test_runtime_schema_validation.sh) + should_validate_runtime_schema=1 + ;; + esac + done <<< "${CHANGED_FILES}" + + if [[ ${#md_files[@]} -eq 0 && "$should_validate_runtime_schema" -eq 0 ]]; then return fi - if ! command -v npx >/dev/null 2>&1; then - echo "[verify-local] npx not found; skipping markdown format check for ${#md_files[@]} changed markdown files" >&2 - return + + if [[ ${#md_files[@]} -ne 0 ]]; then + if ! command -v npx >/dev/null 2>&1; then + echo "[verify-local] npx not found; skipping markdown format check for ${#md_files[@]} changed markdown files" >&2 + else + echo "[verify-local] prettier --check (${#md_files[@]} markdown files)" + npx prettier --check "${md_files[@]}" + fi + fi + + if [[ "$should_validate_runtime_schema" -eq 1 ]]; then + if ! command -v node >/dev/null 2>&1; then + echo "[verify-local] node not found; cannot run runtime schema validation" >&2 + return 1 + fi + echo "[verify-local] runtime schema validation" + node scripts/validate-runtime-schema-fragments.mjs fi - echo "[verify-local] prettier --check (${#md_files[@]} markdown files)" - npx prettier --check "${md_files[@]}" } run_targeted_checks() { diff --git a/tests/hooks/test_dependency_dags.sh b/tests/hooks/test_dependency_dags.sh new file mode 100644 index 00000000..232ab432 --- /dev/null +++ b/tests/hooks/test_dependency_dags.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/../.." || exit 1 + +PASS=0 +FAIL=0 + +pass() { + echo " PASS: $1" + PASS=$((PASS + 1)) +} + +fail() { + echo " FAIL: $1" + FAIL=$((FAIL + 1)) +} + +tmpdir="$(mktemp -d)" +output_file="$(mktemp)" + +cleanup() { + rm -rf "$tmpdir" + rm -f "$output_file" +} +trap cleanup EXIT + +mkdir -p \ + "$tmpdir/scripts" \ + "$tmpdir/docs/archive/tasks" \ + "$tmpdir/docs/assets/dags" \ + "$tmpdir/.cache/echo/deps" + +cat >"$tmpdir/package.json" <<'EOF' +{ + "type": "module" +} +EOF + +cp scripts/generate-dependency-dags.js "$tmpdir/scripts/generate-dependency-dags.js" +cp scripts/parse-tasks-dag.js "$tmpdir/scripts/parse-tasks-dag.js" +cp scripts/dag-utils.js "$tmpdir/scripts/dag-utils.js" + +cat >"$tmpdir/.cache/echo/deps/open-issues.json" <<'EOF' +{ + "generated_at": "2026-03-23T00:00:00Z", + "issues": [ + { + "number": 1, + "title": "Seed issue", + "body": "", + "labels": [], + "milestone": null, + "url": "https://example.com/issues/1" + }, + { + "number": 2, + "title": "Dependent issue", + "body": "", + "labels": [], + "milestone": null, + "url": "https://example.com/issues/2" + } + ] +} +EOF + +cat >"$tmpdir/.cache/echo/deps/milestones-all.json" <<'EOF' +{ + "generated_at": "2026-03-23T00:00:00Z", + "milestones": [] +} +EOF + +cat >"$tmpdir/docs/assets/dags/deps-config.json" <<'EOF' +{ + "issue_edges": [], + "milestone_edges": [] +} +EOF + +cat >"$tmpdir/docs/archive/tasks/TASKS-DAG.md" <<'EOF' +## [#2: Dependent issue](https://example.com/issues/2) + +- Blocked by: + - [#1: Seed issue](https://example.com/issues/1) +EOF + +echo "=== dependency DAG default tasks source ===" +echo + +if ( + cd "$tmpdir" && + node scripts/generate-dependency-dags.js >"$output_file" 2>&1 +); then + if grep -Eq 'i1 -> i2 \[[^]]*color="red"' "$tmpdir/docs/assets/dags/issue-deps.dot"; then + pass "generator reads archived TASKS-DAG source by default" + else + fail "generator should render a reality-only edge from the archived TASKS-DAG source" + if [[ -f "$tmpdir/docs/assets/dags/issue-deps.dot" ]]; then + cat "$tmpdir/docs/assets/dags/issue-deps.dot" + else + cat "$output_file" + fi + fi +else + fail "generator should succeed with only the archived TASKS-DAG source present" + cat "$output_file" +fi + +echo +echo "PASS: $PASS" +echo "FAIL: $FAIL" + +if [[ "$FAIL" -ne 0 ]]; then + exit 1 +fi diff --git a/tests/hooks/test_runtime_schema_validation.sh b/tests/hooks/test_runtime_schema_validation.sh new file mode 100755 index 00000000..08fa835a --- /dev/null +++ b/tests/hooks/test_runtime_schema_validation.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/../.." || exit 1 + +PASS=0 +FAIL=0 + +tmpdir_directives="$(mktemp -d)" + +pass() { + echo " PASS: $1" + PASS=$((PASS + 1)) +} + +fail() { + echo " FAIL: $1" + FAIL=$((FAIL + 1)) +} + +tmpdir="$(mktemp -d)" +output_file="$(mktemp)" + +cleanup() { + rm -rf "$tmpdir_directives" + rm -rf "$tmpdir" + rm -f "$output_file" +} +trap cleanup EXIT + +echo "=== runtime schema validation ===" +echo + +if node scripts/validate-runtime-schema-fragments.mjs >"$output_file" 2>&1; then + pass "validator accepts the checked-in runtime schema fragments" +else + fail "validator should accept the checked-in runtime schema fragments" + cat "$output_file" +fi + +cp schemas/runtime/*.graphql "$tmpdir_directives"/ +cat <<'EOF' >"$tmpdir_directives/directive-safe.graphql" +# SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 +# © James Ross Ω FLYING•ROBOTS + +type DirectiveSafeProbe { + legacyField: String @deprecated(reason: "old") +} +EOF + +if node scripts/validate-runtime-schema-fragments.mjs --dir "$tmpdir_directives" >"$output_file" 2>&1; then + pass "validator accepts directive-bearing GraphQL fields" +else + fail "validator should accept directive-bearing GraphQL fields" + cat "$output_file" +fi + +cp schemas/runtime/*.graphql "$tmpdir"/ +sed 's/^[[:space:]]*scalar RunId[[:space:]]*$/scalar RemovedRunId/' \ + "$tmpdir/artifact-d-scheduler-results.graphql" \ + >"$tmpdir/artifact-d-scheduler-results.graphql.tmp" +mv \ + "$tmpdir/artifact-d-scheduler-results.graphql.tmp" \ + "$tmpdir/artifact-d-scheduler-results.graphql" + +if node scripts/validate-runtime-schema-fragments.mjs --dir "$tmpdir" >"$output_file" 2>&1; then + fail "validator should reject fragments with missing referenced types" + cat "$output_file" +else + if grep -q "missing referenced type RunId" "$output_file"; then + pass "validator reports missing referenced types across fragment files" + else + fail "validator should explain which referenced type is missing" + cat "$output_file" + fi +fi + +echo +echo "PASS: $PASS" +echo "FAIL: $FAIL" + +if [[ "$FAIL" -ne 0 ]]; then + exit 1 +fi