diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 595808c2..b1efa790 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -142,6 +142,8 @@ mod worldline; mod coordinator; mod head; mod head_inbox; +/// Strand contract for speculative execution lanes. +pub mod strand; mod worldline_registry; mod worldline_state; diff --git a/crates/warp-core/src/strand.rs b/crates/warp-core/src/strand.rs new file mode 100644 index 00000000..5317ec5d --- /dev/null +++ b/crates/warp-core/src/strand.rs @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Strand contract for speculative execution lanes. +//! +//! A strand is a named, ephemeral, speculative execution lane derived from a +//! base worldline at a specific tick. It is a relation over a child worldline, +//! not a separate substrate. +//! +//! # Lifecycle +//! +//! A strand either exists in the `StrandRegistry` (live) or does not +//! (dropped). There is no tombstone state. Operational control (paused, +//! admitted, ticking) is derived from the writer heads — the heads are the +//! single source of truth for control state. +//! +//! # Invariants +//! +//! See `docs/invariants/STRAND-CONTRACT.md` for the full normative list. +//! Key invariants enforced by this module: +//! +//! - **INV-S1:** `base_ref` is immutable after creation. +//! - **INV-S2:** Writer heads are created fresh for the child worldline. +//! - **INV-S4:** Writer heads are created Dormant (manual tick only). +//! - **INV-S5:** All `base_ref` fields are verified against provenance. +//! - **INV-S7:** `child_worldline_id != base_ref.source_worldline_id`. +//! - **INV-S8:** Every writer head key belongs to `child_worldline_id`. +//! - **INV-S9:** `support_pins` is empty in v1. + +use std::collections::BTreeMap; + +use thiserror::Error; + +use crate::clock::WorldlineTick; +use crate::ident::Hash; +use crate::provenance_store::ProvenanceRef; +use crate::worldline::WorldlineId; + +use crate::head::WriterHeadKey; + +/// A 32-byte domain-separated strand identifier. +/// +/// Derived from `BLAKE3("strand:" || label)` following the `HeadId`/`NodeId` +/// pattern. +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct StrandId([u8; 32]); + +impl StrandId { + /// Construct a `StrandId` from raw bytes. + #[must_use] + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Returns the raw bytes. + #[must_use] + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +/// Produces a stable, domain-separated strand identifier using BLAKE3. +#[must_use] +pub fn make_strand_id(label: &str) -> StrandId { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"strand:"); + hasher.update(label.as_bytes()); + StrandId(hasher.finalize().into()) +} + +/// The exact provenance coordinate the strand was forked from. +/// +/// Immutable after creation (INV-S1). +/// +/// # Coordinate semantics +/// +/// - `fork_tick` is the **last included tick** in the copied prefix. +/// - `commit_hash` is the commit hash **at `fork_tick`**. +/// - `boundary_hash` is the **output boundary hash** at `fork_tick` — the +/// state root after applying the patch at `fork_tick`. +/// - `provenance_ref` carries the same coordinate as a [`ProvenanceRef`]. +/// - All fields refer to the **same provenance coordinate**. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct BaseRef { + /// Source worldline this strand was forked from. + pub source_worldline_id: WorldlineId, + /// Last included tick in the copied prefix. + pub fork_tick: WorldlineTick, + /// Commit hash at `fork_tick`. + pub commit_hash: Hash, + /// Output boundary hash (state root) at `fork_tick`. + pub boundary_hash: Hash, + /// Substrate-native coordinate handle. + pub provenance_ref: ProvenanceRef, +} + +/// A read-only reference to another strand's materialized state (braid +/// geometry). +/// +/// **v1: not implemented.** The `support_pins` field on [`Strand`] MUST be +/// empty (INV-S9). No mutation API exists. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct SupportPin { + /// The strand being pinned. + pub strand_id: StrandId, + /// The pinned strand's child worldline. + pub worldline_id: WorldlineId, + /// Tick at which the support strand is pinned. + pub pinned_tick: WorldlineTick, + /// State hash at the pinned tick. + pub state_hash: Hash, +} + +/// Receipt returned when a strand is dropped. +/// +/// This is the only record that the strand existed after hard-delete. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct DropReceipt { + /// The dropped strand's identity. + pub strand_id: StrandId, + /// The child worldline that was removed. + pub child_worldline_id: WorldlineId, + /// The tick the child worldline had reached at drop time. + pub final_tick: WorldlineTick, +} + +/// A strand: a named, ephemeral, speculative execution lane. +/// +/// A strand either exists in the `StrandRegistry` (live) or does not +/// (dropped). There is no lifecycle field — operational state is derived +/// from the writer heads. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Strand { + /// Unique strand identity. + pub strand_id: StrandId, + /// Immutable fork coordinate. + pub base_ref: BaseRef, + /// Child worldline created by fork. + pub child_worldline_id: WorldlineId, + /// Writer heads for the child worldline (cardinality 1 in v1). + pub writer_heads: Vec, + /// Support pins for braid geometry (MUST be empty in v1). + pub support_pins: Vec, +} + +/// Errors that can occur during strand operations. +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum StrandError { + /// The strand already exists in the registry. + #[error("strand already exists: {0:?}")] + AlreadyExists(StrandId), + + /// The strand was not found in the registry. + #[error("strand not found: {0:?}")] + NotFound(StrandId), + + /// The fork tick does not exist in the source worldline. + #[error("fork tick {tick} not available in source worldline {worldline:?}")] + ForkTickUnavailable { + /// Source worldline. + worldline: WorldlineId, + /// Requested fork tick. + tick: WorldlineTick, + }, + + /// The source worldline does not exist. + #[error("source worldline not found: {0:?}")] + SourceWorldlineNotFound(WorldlineId), + + /// A provenance operation failed during strand creation or drop. + #[error("provenance error: {0}")] + Provenance(String), + + /// A contract invariant was violated. + #[error("invariant violation: {0}")] + InvariantViolation(&'static str), +} + +/// Session-scoped registry of live strands. +/// +/// Iteration order is by [`StrandId`] (lexicographic over hash bytes). +/// This is deterministic but not semantically meaningful. +#[derive(Clone, Debug, Default)] +pub struct StrandRegistry { + strands: BTreeMap, +} + +impl StrandRegistry { + /// Creates an empty strand registry. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Inserts a fully constructed strand into the registry. + /// + /// Validates contract invariants before insertion: + /// - INV-S7: `child_worldline_id != base_ref.source_worldline_id` + /// - INV-S8: every writer head belongs to `child_worldline_id` + /// - INV-S9: `support_pins` is empty in v1 + /// + /// # Errors + /// + /// Returns [`StrandError::AlreadyExists`] if a strand with the same ID + /// is already registered, or [`StrandError::InvariantViolation`] if any + /// contract invariant is violated. + pub fn insert(&mut self, strand: Strand) -> Result<(), StrandError> { + if self.strands.contains_key(&strand.strand_id) { + return Err(StrandError::AlreadyExists(strand.strand_id)); + } + // INV-S7: distinct worldlines. + if strand.child_worldline_id == strand.base_ref.source_worldline_id { + return Err(StrandError::InvariantViolation( + "INV-S7: child_worldline_id must differ from base_ref.source_worldline_id", + )); + } + // INV-S8: head ownership. + for head_key in &strand.writer_heads { + if head_key.worldline_id != strand.child_worldline_id { + return Err(StrandError::InvariantViolation( + "INV-S8: every writer head must belong to child_worldline_id", + )); + } + } + // INV-S9: no support pins in v1. + if !strand.support_pins.is_empty() { + return Err(StrandError::InvariantViolation( + "INV-S9: support_pins must be empty in v1", + )); + } + self.strands.insert(strand.strand_id, strand); + Ok(()) + } + + /// Removes a strand from the registry. + /// + /// # Errors + /// + /// Returns [`StrandError::NotFound`] if the strand is not registered. + pub fn remove(&mut self, strand_id: &StrandId) -> Result { + self.strands + .remove(strand_id) + .ok_or(StrandError::NotFound(*strand_id)) + } + + /// Returns a reference to a strand, if it exists. + #[must_use] + pub fn get(&self, strand_id: &StrandId) -> Option<&Strand> { + self.strands.get(strand_id) + } + + /// Returns `true` if the registry contains the given strand. + #[must_use] + pub fn contains(&self, strand_id: &StrandId) -> bool { + self.strands.contains_key(strand_id) + } + + /// Returns a zero-allocation iterator over live strands derived from the + /// given base worldline, ordered by [`StrandId`]. + pub fn iter_by_base<'a>( + &'a self, + base_worldline_id: &'a WorldlineId, + ) -> impl Iterator + 'a { + self.strands + .values() + .filter(move |s| &s.base_ref.source_worldline_id == base_worldline_id) + } + + /// Returns all live strands derived from the given base worldline, + /// ordered by [`StrandId`]. Allocates; prefer [`iter_by_base`](Self::iter_by_base) + /// in hot paths. + pub fn list_by_base<'a>(&'a self, base_worldline_id: &'a WorldlineId) -> Vec<&'a Strand> { + self.iter_by_base(base_worldline_id).collect() + } + + /// Returns the number of live strands. + #[must_use] + pub fn len(&self) -> usize { + self.strands.len() + } + + /// Returns `true` if no strands are registered. + #[must_use] + pub fn is_empty(&self) -> bool { + self.strands.is_empty() + } +} diff --git a/crates/warp-core/tests/strand_contract_tests.rs b/crates/warp-core/tests/strand_contract_tests.rs new file mode 100644 index 00000000..dcae4221 --- /dev/null +++ b/crates/warp-core/tests/strand_contract_tests.rs @@ -0,0 +1,529 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Integration tests for the strand contract (cycle 0004). +//! +//! These tests verify the ten invariants (INV-S1 through INV-S10) and the +//! create/list/drop lifecycle. + +#![allow(clippy::unwrap_used, clippy::expect_used)] + +use warp_core::strand::{ + make_strand_id, BaseRef, DropReceipt, Strand, StrandError, StrandRegistry, +}; +use warp_core::{ + make_head_id, make_node_id, make_type_id, make_warp_id, GlobalTick, GraphStore, HashTriplet, + HeadEligibility, LocalProvenanceStore, NodeRecord, PlaybackHeadRegistry, PlaybackMode, + ProvenanceEntry, ProvenanceRef, ProvenanceService, ProvenanceStore, RunnableWriterSet, + WorldlineId, WorldlineState, WorldlineTick, WorldlineTickHeaderV1, WorldlineTickPatchV1, + WriterHead, WriterHeadKey, +}; + +// ── Helpers ───────────────────────────────────────────────────────────────── + +fn wl(n: u8) -> WorldlineId { + WorldlineId::from_bytes([n; 32]) +} + +fn wt(n: u64) -> WorldlineTick { + WorldlineTick::from_raw(n) +} + +fn test_initial_state() -> WorldlineState { + let warp_id = make_warp_id("strand-test-warp"); + let root = make_node_id("strand-test-root"); + let mut store = GraphStore::new(warp_id); + store.insert_node( + root, + NodeRecord { + ty: make_type_id("StrandTestRoot"), + }, + ); + WorldlineState::from_root_store(store, root).expect("test initial state") +} + +/// Create a provenance service with a registered worldline that has some +/// committed ticks, suitable for forking. +fn setup_base_worldline() -> (ProvenanceService, WorldlineId, WorldlineState) { + let mut provenance = ProvenanceService::new(); + let base_id = wl(1); + let initial_state = test_initial_state(); + + provenance + .register_worldline(base_id, &initial_state) + .expect("register base worldline"); + + (provenance, base_id, initial_state) +} + +/// Build a strand with explicit base/child worldlines for invariant violation tests. +fn make_test_strand_raw(base_worldline: WorldlineId, child_worldline: WorldlineId) -> Strand { + make_test_strand("raw", base_worldline, child_worldline, wt(5)) +} + +/// Build a strand by hand (without full engine integration) to test the +/// registry and type invariants. +fn make_test_strand( + strand_label: &str, + base_worldline: WorldlineId, + child_worldline: WorldlineId, + fork_tick: WorldlineTick, +) -> Strand { + let strand_id = make_strand_id(strand_label); + let head_key = WriterHeadKey { + worldline_id: child_worldline, + head_id: make_head_id(&format!("strand-head-{strand_label}")), + }; + let commit_hash = [0xAA; 32]; + let boundary_hash = [0xBB; 32]; + + Strand { + strand_id, + base_ref: BaseRef { + source_worldline_id: base_worldline, + fork_tick, + commit_hash, + boundary_hash, + provenance_ref: ProvenanceRef { + worldline_id: base_worldline, + worldline_tick: fork_tick, + commit_hash, + }, + }, + child_worldline_id: child_worldline, + writer_heads: vec![head_key], + support_pins: Vec::new(), + } +} + +// ── INV-S7: child_worldline_id != base_ref.source_worldline_id ────────── + +#[test] +fn inv_s7_child_and_base_worldlines_are_distinct() { + let strand = make_test_strand("s7-test", wl(1), wl(2), wt(5)); + assert_ne!( + strand.child_worldline_id, strand.base_ref.source_worldline_id, + "INV-S7: child worldline must differ from base" + ); +} + +// ── INV-S2 / INV-S8: own heads, head ownership ───────────────────────── + +#[test] +fn inv_s2_s8_strand_heads_belong_to_child_worldline() { + let base = wl(1); + let child = wl(2); + let strand = make_test_strand("s2-test", base, child, wt(5)); + + for head_key in &strand.writer_heads { + assert_eq!( + head_key.worldline_id, child, + "INV-S8: every writer head must belong to child_worldline_id" + ); + assert_ne!( + head_key.worldline_id, base, + "INV-S2: writer heads must not belong to base worldline" + ); + } +} + +// ── INV-S4: strand heads are Dormant and Paused ──────────────────────── + +#[test] +fn inv_s4_strand_head_created_dormant_and_paused() { + let child = wl(2); + let head_key = WriterHeadKey { + worldline_id: child, + head_id: make_head_id("strand-head-dormant"), + }; + let head = WriterHead::new(head_key, PlaybackMode::Paused); + + assert!(head.is_paused(), "strand head must be created paused"); + // Dormant must be set explicitly + let mut head = head; + head.set_eligibility(HeadEligibility::Dormant); + assert!( + !head.is_admitted(), + "strand head must not be admitted (Dormant)" + ); +} + +// ── INV-S4 / INV-S10: strand heads excluded from live scheduler ──────── + +#[test] +fn inv_s4_s10_dormant_strand_heads_excluded_from_runnable_set() { + let base_wl = wl(1); + let strand_wl = wl(2); + + let mut head_registry = PlaybackHeadRegistry::new(); + + // Register a live head on the base worldline (admitted, playing) + let live_key = WriterHeadKey { + worldline_id: base_wl, + head_id: make_head_id("live-head"), + }; + head_registry.insert(WriterHead::new(live_key, PlaybackMode::Play)); + + // Register a strand head on the child worldline (dormant, paused) + let strand_key = WriterHeadKey { + worldline_id: strand_wl, + head_id: make_head_id("strand-head"), + }; + let mut strand_head = WriterHead::new(strand_key, PlaybackMode::Paused); + strand_head.set_eligibility(HeadEligibility::Dormant); + head_registry.insert(strand_head); + + // Build the runnable set + let mut runnable = RunnableWriterSet::new(); + runnable.rebuild(&head_registry); + + // Live head should be runnable + assert!( + runnable.iter().any(|k| *k == live_key), + "live head should be in runnable set" + ); + + // Strand head must NOT be runnable + assert!( + !runnable.iter().any(|k| *k == strand_key), + "INV-S4/S10: dormant strand head must not appear in runnable set" + ); +} + +// ── INV-S9: support_pins must be empty in v1 ─────────────────────────── + +#[test] +fn inv_s9_support_pins_empty_on_creation() { + let strand = make_test_strand("s9-test", wl(1), wl(2), wt(5)); + assert!( + strand.support_pins.is_empty(), + "INV-S9: support_pins must be empty in v1" + ); +} + +// ── INV-S5: base_ref fields agree ────────────────────────────────────── + +#[test] +fn inv_s5_base_ref_fields_consistent() { + let strand = make_test_strand("s5-test", wl(1), wl(2), wt(5)); + let br = &strand.base_ref; + + // provenance_ref must agree with base_ref scalars + assert_eq!(br.provenance_ref.worldline_id, br.source_worldline_id); + assert_eq!(br.provenance_ref.worldline_tick, br.fork_tick); + assert_eq!(br.provenance_ref.commit_hash, br.commit_hash); +} + +// ── StrandRegistry: insert / get / contains / list / remove ───────────── + +#[test] +fn registry_insert_and_get() { + let mut registry = StrandRegistry::new(); + let strand = make_test_strand("reg-1", wl(1), wl(2), wt(5)); + let sid = strand.strand_id; + + registry.insert(strand).expect("insert"); + assert!(registry.contains(&sid)); + assert!(registry.get(&sid).is_some()); + assert_eq!(registry.len(), 1); +} + +#[test] +fn registry_duplicate_insert_fails() { + let mut registry = StrandRegistry::new(); + let strand = make_test_strand("dup", wl(1), wl(2), wt(5)); + let sid = strand.strand_id; + + registry.insert(strand.clone()).expect("first insert"); + let err = registry.insert(strand).expect_err("duplicate insert"); + assert_eq!(err, StrandError::AlreadyExists(sid)); +} + +#[test] +fn registry_remove_returns_strand_and_clears() { + let mut registry = StrandRegistry::new(); + let strand = make_test_strand("rm-1", wl(1), wl(2), wt(5)); + let sid = strand.strand_id; + + registry.insert(strand).expect("insert"); + let removed = registry.remove(&sid).expect("remove should succeed"); + assert_eq!(removed.strand_id, sid); + assert!( + !registry.contains(&sid), + "strand should be gone after remove" + ); + assert!(registry.get(&sid).is_none()); + assert_eq!(registry.len(), 0); +} + +#[test] +fn registry_insert_rejects_inv_s7_same_worldline() { + let mut registry = StrandRegistry::new(); + // child == base violates INV-S7 + let strand = make_test_strand_raw(wl(1), wl(1)); + let err = registry.insert(strand).expect_err("INV-S7 should reject"); + assert!( + matches!(err, StrandError::InvariantViolation(_)), + "expected InvariantViolation, got {err:?}" + ); +} + +#[test] +fn registry_insert_rejects_inv_s8_wrong_head_worldline() { + let mut registry = StrandRegistry::new(); + let strand_id = make_strand_id("s8-bad"); + let strand = Strand { + strand_id, + base_ref: BaseRef { + source_worldline_id: wl(1), + fork_tick: wt(5), + commit_hash: [0xAA; 32], + boundary_hash: [0xBB; 32], + provenance_ref: ProvenanceRef { + worldline_id: wl(1), + worldline_tick: wt(5), + commit_hash: [0xAA; 32], + }, + }, + child_worldline_id: wl(2), + // Head belongs to wl(3), not wl(2) — violates INV-S8 + writer_heads: vec![WriterHeadKey { + worldline_id: wl(3), + head_id: make_head_id("wrong-wl-head"), + }], + support_pins: Vec::new(), + }; + let err = registry.insert(strand).expect_err("INV-S8 should reject"); + assert!( + matches!(err, StrandError::InvariantViolation(_)), + "expected InvariantViolation, got {err:?}" + ); +} + +#[test] +fn registry_insert_rejects_inv_s9_nonempty_support_pins() { + use warp_core::strand::SupportPin; + + let mut registry = StrandRegistry::new(); + let strand_id = make_strand_id("s9-bad"); + let strand = Strand { + strand_id, + base_ref: BaseRef { + source_worldline_id: wl(1), + fork_tick: wt(5), + commit_hash: [0xAA; 32], + boundary_hash: [0xBB; 32], + provenance_ref: ProvenanceRef { + worldline_id: wl(1), + worldline_tick: wt(5), + commit_hash: [0xAA; 32], + }, + }, + child_worldline_id: wl(2), + writer_heads: vec![WriterHeadKey { + worldline_id: wl(2), + head_id: make_head_id("s9-head"), + }], + support_pins: vec![SupportPin { + strand_id: make_strand_id("pinned"), + worldline_id: wl(10), + pinned_tick: wt(0), + state_hash: [0; 32], + }], + }; + let err = registry.insert(strand).expect_err("INV-S9 should reject"); + assert!( + matches!(err, StrandError::InvariantViolation(_)), + "expected InvariantViolation, got {err:?}" + ); +} + +#[test] +fn registry_remove_nonexistent_returns_error() { + let mut registry = StrandRegistry::new(); + let sid = make_strand_id("ghost"); + let err = registry.remove(&sid).expect_err("remove should fail"); + assert_eq!(err, StrandError::NotFound(sid)); +} + +#[test] +fn registry_list_by_base_filters_correctly() { + let mut registry = StrandRegistry::new(); + let base_a = wl(1); + let base_b = wl(10); + + registry + .insert(make_test_strand("a1", base_a, wl(2), wt(5))) + .unwrap(); + registry + .insert(make_test_strand("a2", base_a, wl(3), wt(5))) + .unwrap(); + registry + .insert(make_test_strand("b1", base_b, wl(4), wt(5))) + .unwrap(); + + let from_a = registry.list_by_base(&base_a); + assert_eq!(from_a.len(), 2, "should find 2 strands from base_a"); + for s in &from_a { + assert_eq!(s.base_ref.source_worldline_id, base_a); + } + + let from_b = registry.list_by_base(&base_b); + assert_eq!(from_b.len(), 1, "should find 1 strand from base_b"); + + let unknown = wl(99); + let from_none = registry.list_by_base(&unknown); + assert!( + from_none.is_empty(), + "should find no strands from unknown base" + ); +} + +// ── Writer heads cardinality (v1: exactly 1) ──────────────────────────── + +#[test] +fn v1_strand_has_exactly_one_writer_head() { + let strand = make_test_strand("card-1", wl(1), wl(2), wt(5)); + assert_eq!( + strand.writer_heads.len(), + 1, + "v1 strands must have exactly one writer head" + ); +} + +// ── Provenance fork creates child worldline with correct prefix ───────── + +#[test] +fn provenance_fork_creates_child_with_prefix() { + let (mut provenance, base_id, _initial_state) = setup_base_worldline(); + let child_id = wl(2); + + // The base worldline has 0 ticks committed (just registered). + // We need at least one committed tick to fork from. + // For now, verify that fork on an empty worldline fails gracefully. + let result = provenance.fork(base_id, wt(0), child_id); + + // With no committed entries, fork at tick 0 should fail because + // there's no provenance entry at that tick. + assert!( + result.is_err(), + "fork should fail when no entries exist at fork_tick" + ); +} + +// ── Happy-path fork: commit entries, fork, verify child prefix ────────── + +#[test] +fn provenance_fork_happy_path_child_has_correct_prefix() { + let base_id = wl(1); + let child_id = wl(2); + let warp_id = make_warp_id("fork-test-warp"); + + let mut store = LocalProvenanceStore::new(); + store + .register_worldline(base_id, warp_id) + .expect("register"); + + let head_key = WriterHeadKey { + worldline_id: base_id, + head_id: make_head_id("fork-test-head"), + }; + + // Commit 3 ticks (0, 1, 2) to the base worldline. + let mut parents = Vec::new(); + for tick in 0_u8..3 { + let tick_u64 = u64::from(tick); + let triplet = HashTriplet { + state_root: [tick + 1; 32], + patch_digest: [tick + 0x10; 32], + commit_hash: [tick + 0x20; 32], + }; + let entry = ProvenanceEntry::local_commit( + base_id, + wt(tick_u64), + GlobalTick::from_raw(tick_u64), + head_key, + parents, + triplet, + WorldlineTickPatchV1 { + header: WorldlineTickHeaderV1 { + commit_global_tick: GlobalTick::from_raw(tick_u64), + policy_id: 0, + rule_pack_id: [0u8; 32], + plan_digest: [0u8; 32], + decision_digest: [0u8; 32], + rewrites_digest: [0u8; 32], + }, + warp_id, + ops: vec![], + in_slots: vec![], + out_slots: vec![], + patch_digest: [tick; 32], + }, + vec![], + Vec::new(), + ); + parents = vec![entry.as_ref()]; + store.append_local_commit(entry).expect("append"); + } + + // Fork at tick 1 (last included tick = 1, child gets ticks 0 and 1). + store.fork(base_id, wt(1), child_id).expect("fork"); + + // Verify child has exactly 2 entries (ticks 0 and 1). + assert_eq!(store.len(child_id).expect("child len"), 2); + + // Fetch the SOURCE entry (not the child copy) for ground-truth comparison. + let base_entry = store.entry(base_id, wt(1)).expect("base entry at tick 1"); + let child_entry = store.entry(child_id, wt(1)).expect("child entry at tick 1"); + + // Verify fork preserved commit hashes between source and child. + assert_eq!( + child_entry.expected.commit_hash, base_entry.expected.commit_hash, + "child entry commit_hash should match base entry" + ); + assert_eq!( + child_entry.expected.state_root, base_entry.expected.state_root, + "child entry state_root should match base entry" + ); + + // Verify the child's worldline ID was rewritten. + assert_eq!(child_entry.worldline_id, child_id); + + // Build base_ref from the SOURCE entry, not the child copy. + let base_ref = BaseRef { + source_worldline_id: base_id, + fork_tick: wt(1), + commit_hash: base_entry.expected.commit_hash, + boundary_hash: base_entry.expected.state_root, + provenance_ref: ProvenanceRef { + worldline_id: base_id, + worldline_tick: wt(1), + commit_hash: base_entry.expected.commit_hash, + }, + }; + + // INV-S5: all fields agree with source coordinate. + assert_eq!( + base_ref.provenance_ref.worldline_id, + base_ref.source_worldline_id + ); + assert_eq!(base_ref.provenance_ref.worldline_tick, base_ref.fork_tick); + assert_eq!(base_ref.provenance_ref.commit_hash, base_ref.commit_hash); + assert_eq!(base_ref.boundary_hash, base_entry.expected.state_root); +} + +// ── Drop receipt carries correct fields ───────────────────────────────── + +#[test] +fn drop_receipt_carries_correct_fields() { + let strand = make_test_strand("drop-test", wl(1), wl(2), wt(5)); + let receipt = DropReceipt { + strand_id: strand.strand_id, + child_worldline_id: strand.child_worldline_id, + final_tick: wt(10), + }; + + assert_eq!(receipt.strand_id, strand.strand_id); + assert_eq!(receipt.child_worldline_id, wl(2)); + assert_eq!(receipt.final_tick, wt(10)); +} diff --git a/docs/method/backlog/up-next/KERNEL_strand-contract.md b/docs/design/0004-strand-contract/KERNEL_strand-contract.md similarity index 100% rename from docs/method/backlog/up-next/KERNEL_strand-contract.md rename to docs/design/0004-strand-contract/KERNEL_strand-contract.md diff --git a/docs/design/0004-strand-contract/design.md b/docs/design/0004-strand-contract/design.md new file mode 100644 index 00000000..875cc75b --- /dev/null +++ b/docs/design/0004-strand-contract/design.md @@ -0,0 +1,363 @@ + + + +# 0004 — Strand contract + +_Define the strand as a first-class relation in Echo with exact fields, +invariants, lifecycle, and TTD mapping._ + +Legend: KERNEL + +Depends on: + +- [0003 — dt-policy](../0003-dt-policy/design.md) + +## Why this cycle exists + +Echo can fork worldlines but has no concept of the relationship +between them. `ProvenanceStore::fork()` creates a prefix-copy and +rewrites parent refs, but once forked, the child worldline is just +another worldline — there is no way to ask "what was this forked +from?", "is this a speculative lane?", or "what strands exist for +this base?" + +git-warp has a full strand implementation. warp-ttd Cycle D already +builds strand lifecycle into the TUI (`LaneKind::STRAND`, +`LaneRef.parentId`, create/tick/compare/drop). Echo needs to surface +strands through the TTD adapter, and it needs the strand contract to +do so honestly. + +The strand contract does not require settlement. It defines identity, +lifecycle, and the adapter seam. Settlement is a separate spec that +builds on this one. + +## Normative definitions + +### Strand + +A strand is a named, ephemeral, speculative execution lane derived +from a base worldline at a specific tick. It is a relation over a +child worldline, not a separate substrate. + +```text +Strand { + strand_id: StrandId, + base_ref: BaseRef, + child_worldline_id: WorldlineId, + writer_heads: Vec, + support_pins: Vec, +} +``` + +There is no `StrandLifecycle` field. A strand either exists in the +registry (live) or does not (dropped). Operational state (paused, +admitted, ticking) is derived from the writer heads — the heads are +the single source of truth for control state. + +### StrandId + +Domain-separated hash newtype (prefix `b"strand:"`), following the +`HeadId`/`NodeId` pattern. + +### BaseRef + +The exact provenance coordinate the strand was forked from. Immutable +after creation. + +```text +BaseRef { + source_worldline_id: WorldlineId, + fork_tick: WorldlineTick, + commit_hash: Hash, + boundary_hash: Hash, + provenance_ref: ProvenanceRef, +} +``` + +**Coordinate semantics (exact):** + +- `fork_tick` is the **last included tick** in the copied prefix. + The child worldline contains entries `0..=fork_tick`. The child's + next appendable tick is `fork_tick + 1`. +- `commit_hash` is the commit hash **at `fork_tick`** — i.e., + `provenance.entry(source, fork_tick).expected.commit_hash`. +- `boundary_hash` is the **output boundary hash** at `fork_tick` — + the state root after applying the patch at `fork_tick`. This is + the hash of the state the child worldline begins diverging from. +- `provenance_ref` carries the same coordinate as a `ProvenanceRef` + (worldline, tick, commit hash) for substrate-native lookups. +- All five fields refer to the **same provenance coordinate**. If + any field disagrees with the provenance store, construction MUST + fail. + +### SupportPin + +A read-only reference to another strand's materialized state at a +specific tick. This is braid geometry — the strand can read from +pinned support strands without merging them. + +```text +SupportPin { + strand_id: StrandId, + worldline_id: WorldlineId, + pinned_tick: WorldlineTick, + state_hash: Hash, +} +``` + +**v1: `support_pins` MUST be empty.** The field exists to prevent a +breaking struct change when braid geometry arrives. No mutation API +for `support_pins` exists in v1. + +### Registry ordering + +`StrandRegistry` is a `BTreeMap`. Iteration order +is by `StrandId` (lexicographic over the hash bytes). This is +deterministic but not semantically meaningful. + +`list_strands(base_worldline_id)` returns results filtered by +`base_ref.source_worldline_id`, ordered by `StrandId`. + +## Invariants + +- **INV-S1 (Immutable base):** A strand's `base_ref` MUST NOT change + after creation. +- **INV-S2 (Own heads):** A strand's child worldline MUST NOT share + writer heads with its base worldline. Head keys are created fresh + for the child. +- **INV-S3 (Session-scoped):** A strand MUST NOT outlive the session + that created it (v1). +- **INV-S4 (Manual tick):** A strand's writer heads MUST be created + with `HeadEligibility::Dormant`. They are ticked only by explicit + external command, never by the live scheduler. +- **INV-S5 (Complete base_ref):** `base_ref` MUST pin source worldline + ID, fork tick, commit hash, boundary hash, and provenance ref. All + fields MUST agree with the provenance store at construction time. +- **INV-S6 (Inherited quantum):** A strand inherits its parent's + `tick_quantum` at fork time (per FIXED-TIMESTEP invariant). No + strand can change its quantum. +- **INV-S7 (Distinct worldlines):** `child_worldline_id` MUST NOT + equal `base_ref.source_worldline_id`. +- **INV-S8 (Head ownership):** Every key in `writer_heads` MUST + belong to `child_worldline_id`. +- **INV-S9 (No support pins in v1):** `support_pins` MUST be empty. +- **INV-S10 (Clean drop):** After `drop_strand`, no runnable heads + for the child worldline MUST remain in the `PlaybackHeadRegistry`. + +## Drop semantics + +v1 uses **hard-delete**: + +- `drop_strand(strand_id)` removes the strand's writer heads from + `PlaybackHeadRegistry`, removes the child worldline from + `WorldlineRegistry`, removes the child worldline's history from + the provenance store, and removes the strand from + `StrandRegistry`. +- There is no Dropped tombstone state. After drop, `get(strand_id)` + returns `None`. +- `drop_strand` returns a `DropReceipt` containing the `strand_id`, + `child_worldline_id`, and the tick the child had reached at drop + time. This is the only record that the strand existed. +- TTD can log the `DropReceipt` if it needs to show "this strand + existed and was dropped" during the session. + +## Create/drop atomicity + +### create_strand + +Construction follows a fixed order. If any step fails, all prior +steps are rolled back: + +1. Validate that `fork_tick` exists in the source worldline's + provenance and capture the `BaseRef` fields. +2. Call `ProvenanceStore::fork()` to create the child worldline. +3. Create a new `WriterHead` for the child worldline with + `PlaybackMode::Paused` and `HeadEligibility::Dormant`. +4. Register the head in `PlaybackHeadRegistry`. +5. Register the strand in `StrandRegistry`. + +Rollback on failure at step N: + +- Step 2 fails: nothing to roll back (validation only in step 1). +- Step 3 fails: remove the forked worldline from provenance. +- Step 4 fails: remove the forked worldline from provenance. +- Step 5 fails: remove head from registry, remove forked worldline + from provenance. + +### drop_strand + +Drop follows a fixed order. Each step is independent (no rollback): + +1. Remove writer heads from `PlaybackHeadRegistry`. +2. Remove child worldline from `WorldlineRegistry`. +3. Remove child worldline history from provenance store. +4. Remove strand from `StrandRegistry`. +5. Return `DropReceipt`. + +If the strand does not exist, return an error. If intermediate +removal fails (e.g., worldline already removed), log a warning and +continue — drop is best-effort cleanup of an ephemeral resource. + +## Writer heads cardinality + +v1 creates exactly one writer head per strand. `writer_heads` is a +`Vec` to support future multi-head strands, but v1 +always produces a vec of length 1. + +## Human users / jobs / hills + +### Primary human users + +- Debugger users exploring "what if?" scenarios +- Engine contributors implementing time travel features +- Game designers testing alternative simulation paths + +### Human jobs + +1. Fork a strand from any tick of a running worldline. +2. Tick the strand independently to explore a scenario. +3. Compare strand state against the base worldline. +4. Drop the strand when done. + +### Human hill + +A debugger user can fork a speculative lane from any point in +simulation history and explore it without affecting the live +worldline. + +## Agent users / jobs / hills + +### Primary agent users + +- TTD host adapter surfacing strand state to warp-ttd +- Agents implementing settlement or time travel downstream + +### Agent jobs + +1. Create a strand with a well-defined `base_ref`. +2. Register strand heads in the head registry. +3. Report strand type and parentage to the TTD adapter + (`LaneKind::STRAND`, `LaneRef.parentId`). +4. Enumerate strands derived from a common base. + +### Agent hill + +An agent can create, tick, inspect, and drop strands through a +typed API and programmatically surface strand topology to TTD. + +## Human playback + +1. The human calls `create_strand(base_worldline, fork_tick)`. +2. A new strand is returned with a `StrandId`, `base_ref` pinning + the exact fork coordinate (all five fields verified against + provenance), and a child worldline with its own Dormant writer + head. +3. The human explicitly ticks the strand's head. The base worldline + is unaffected. +4. The human inspects the strand's child worldline state at its + current tick and compares it to the base worldline at the same + tick. +5. The human drops the strand. A `DropReceipt` is returned. The + child worldline, its heads, and its provenance are gone. + `get(strand_id)` returns `None`. + +## Agent playback + +1. The agent calls the strand creation API. +2. The returned `Strand` struct contains: `strand_id`, `base_ref` + (with `provenance_ref`), `child_worldline_id`, `writer_heads` + (length 1), `support_pins` (empty). +3. The agent maps `strand_id` to `LaneKind::STRAND` (type, not + lifecycle) and `base_ref.source_worldline_id` to + `LaneRef.parentId`. +4. The agent calls `list_strands(base_worldline_id)` and receives + all live strands derived from that base, ordered by `StrandId`. +5. The agent drops the strand. `get(strand_id)` returns `None`. + The `DropReceipt` carries the strand_id, child worldline, and + final tick. + +## Implementation outline + +1. Define `StrandId` as a domain-separated hash newtype (prefix + `b"strand:"`), following the `HeadId`/`NodeId` pattern. +2. Define `BaseRef`, `SupportPin`, `DropReceipt`, and `Strand` + structs in a new `crates/warp-core/src/strand.rs` module. +3. Define `StrandRegistry` — `BTreeMap` with + `create`, `get`, `contains`, `list_by_base`, and `drop` + operations. Session-scoped, not persisted. +4. Implement `create_strand` with the five-step construction + sequence and rollback on failure. +5. Implement `drop_strand` with the five-step hard-delete sequence + returning a `DropReceipt`. +6. Implement `list_strands(base_worldline_id)` — filter by + `base_ref.source_worldline_id`, ordered by `StrandId`. +7. Write `docs/invariants/STRAND-CONTRACT.md` with the ten + invariants (INV-S1 through INV-S10). + +## Tests to write first + +- Unit test: `create_strand` returns a strand with correct + `base_ref` fields — all five fields match the source worldline's + provenance entry at `fork_tick`. +- Unit test: strand's child worldline has its own `WriterHeadKey`, + distinct from any head on the base worldline (INV-S2). +- Unit test: strand head is created Dormant and Paused (INV-S4). +- Unit test: ticking the strand head advances the child worldline + without affecting the base worldline's frontier. +- Unit test: strand heads do not appear in the live scheduler's + runnable set — integration test proving Dormant heads are excluded + from canonical runnable ordering (INV-S4, INV-S10). +- Unit test: `list_strands` returns strands matching the base + worldline and does not return strands from other bases. +- Unit test: `drop_strand` removes the child worldline, its heads, + and its provenance. `get(strand_id)` returns `None`. No heads for + the child worldline remain in `PlaybackHeadRegistry` (INV-S10). +- Unit test: `drop_strand` returns a `DropReceipt` with the correct + `strand_id`, `child_worldline_id`, and final tick. +- Unit test: `child_worldline_id != base_ref.source_worldline_id` + (INV-S7). +- Unit test: `support_pins` is empty on creation (INV-S9). +- Unit test: `create_strand` fails and rolls back if `fork_tick` + does not exist in the source worldline. +- Shell assertion: `docs/invariants/STRAND-CONTRACT.md` exists and + contains all ten invariant codes (INV-S1 through INV-S10). + +## Risks / unknowns + +- **Risk: provenance removal API.** `LocalProvenanceStore` has no + `remove_worldline` method. This cycle must add one, scoped to + ephemeral strand cleanup only. The removal MUST NOT affect other + worldlines that reference the dropped child through + `ProvenanceRef` parent links — those refs become dangling but are + structurally harmless (the coordinate they point to no longer + resolves, which is the correct behavior for a dropped strand). +- **Risk: head registry coupling.** `PlaybackHeadRegistry` is + engine-global, ordered canonically by `(worldline_id, head_id)`. + Strand heads are inserted into this global registry. The Dormant + eligibility gate prevents live scheduling, but the test must prove + this with an integration test that builds a runnable set and + verifies strand heads are absent. +- **Unknown: multi-head strands.** v1 creates one head per strand. + Future cycles may create multiple. The vec is correct but the + cardinality-1 assumption should be documented and tested. + +## Postures + +- **Accessibility:** Not applicable — internal API, no UI. +- **Localization:** Not applicable — internal types. +- **Agent inspectability:** All strand fields are public and + serializable. `StrandRegistry` supports enumeration with + documented ordering. The TTD mapping is type-to-type (`StrandId` + → `LaneKind::STRAND`, `base_ref.source_worldline_id` → + `LaneRef.parentId`), not lifecycle-to-lifecycle. + +## Non-goals + +- Settlement semantics (KERNEL_strand-settlement, future cycle). +- SupportPin / braid geometry implementation (v1 has INV-S9: + support_pins MUST be empty). +- Strand persistence across sessions (v1 is ephemeral). +- Automatic scheduling of strand heads (v1 is manual tick only). +- TTD adapter implementation (this cycle defines the mapping; the + adapter is PLATFORM_echo-ttd-host-adapter). +- Multi-head strand creation (v1 creates exactly one head). diff --git a/docs/invariants/STRAND-CONTRACT.md b/docs/invariants/STRAND-CONTRACT.md new file mode 100644 index 00000000..98e50399 --- /dev/null +++ b/docs/invariants/STRAND-CONTRACT.md @@ -0,0 +1,102 @@ + + + +# STRAND-CONTRACT + +**Status:** Normative | **Legend:** KERNEL | **Cycle:** 0004 + +## Invariant + +A strand is a named, ephemeral, speculative execution lane derived +from a base worldline. It is a relation over a child worldline +created by `ProvenanceStore::fork()`, not a separate substrate. A +strand either exists in the `StrandRegistry` (live) or does not +(dropped). There is no tombstone state. + +## Invariants + +The following invariants are normative. "MUST" and "MUST NOT" follow +RFC 2119 convention. + +### INV-S1 — Immutable base + +A strand's `base_ref` MUST NOT change after creation. The `BaseRef` +pins the exact provenance coordinate the strand was forked from: +source worldline ID, fork tick (last included tick in the copied +prefix), commit hash at fork tick, output boundary hash (state root +after applying the patch), and a `ProvenanceRef` handle. + +### INV-S2 — Own heads + +A strand's child worldline MUST NOT share writer heads with its base +worldline. Head keys are created fresh for the child, using the same +`WriterHead` infrastructure but with `WriterHeadKey.worldline_id` +set to the child worldline. + +### INV-S3 — Session-scoped + +A strand MUST NOT outlive the session that created it (v1). No +strand persistence across sessions. + +### INV-S4 — Manual tick + +A strand's writer heads MUST be created with +`HeadEligibility::Dormant` and `PlaybackMode::Paused`. They are +ticked only by explicit external command, never by the live +scheduler. Dormant heads do not appear in the `RunnableWriterSet`. + +### INV-S5 — Complete base_ref + +`base_ref` MUST pin: source worldline ID, fork tick, commit hash, +boundary hash, and provenance ref. All fields MUST agree with the +provenance store at construction time. If any field disagrees, +construction MUST fail. + +### INV-S6 — Inherited quantum + +A strand inherits its parent's `tick_quantum` at fork time (per +[FIXED-TIMESTEP](./FIXED-TIMESTEP.md) invariant). No strand can +change its quantum. + +### INV-S7 — Distinct worldlines + +`child_worldline_id` MUST NOT equal `base_ref.source_worldline_id`. +A strand is always a distinct worldline from its base. + +### INV-S8 — Head ownership + +Every key in `writer_heads` MUST belong to `child_worldline_id`. +No head may reference a different worldline. + +### INV-S9 — No support pins in v1 + +`support_pins` MUST be empty in v1. The field exists to prevent a +breaking struct change when braid geometry arrives. No mutation API +for `support_pins` exists in v1. + +### INV-S10 — Clean drop + +After `drop_strand`, no runnable heads for the child worldline MUST +remain in the `PlaybackHeadRegistry`. Drop is hard-delete: the +strand, its child worldline, its heads, and its provenance are all +removed. `get(strand_id)` returns `None` after drop. A `DropReceipt` +is returned as the only proof the strand existed. + +## Rationale + +Echo can fork worldlines via `ProvenanceStore::fork()` but has no +concept of the relationship between forked worldlines. The strand +contract names that relationship explicitly: what was forked, from +where, with what heads, under what lifecycle rules. + +This enables warp-ttd to surface strand topology through its existing +`LaneKind::STRAND` and `LaneRef.parentId` protocol, and it provides +the foundation for the settlement spec (which imports operations from +strands into base worldlines under channel policy). + +## Cross-references + +- [FIXED-TIMESTEP](./FIXED-TIMESTEP.md) — inherited quantum +- [SPEC-0004 — Worldlines](../spec/SPEC-0004-worldlines-playback-truthbus.md) +- [SPEC-0005 — Provenance Payload](../spec/SPEC-0005-provenance-payload.md) +- `warp_core::strand` — code-level implementation diff --git a/docs/method/backlog/asap/PLATFORM_WESLEY_protocol-consumer-cutover.md b/docs/method/backlog/asap/PLATFORM_WESLEY_protocol-consumer-cutover.md new file mode 100644 index 00000000..e65c24fc --- /dev/null +++ b/docs/method/backlog/asap/PLATFORM_WESLEY_protocol-consumer-cutover.md @@ -0,0 +1,29 @@ + + + +# WESLEY Protocol Consumer Cutover + +Coordination: `WESLEY_protocol_surface_cutover` + +Echo still carries local TTD protocol artifacts that predate the current +Continuum ownership split: + +- `crates/ttd-protocol-rs` +- `crates/ttd-manifest` +- `packages/ttd-protocol-ts` +- `crates/echo-ttd/src/compliance.rs` + +For the current Wesley-sponsored hill, Echo should stop acting like a backup +source of truth for the host-neutral debugger protocol and become a boring +consumer of the canonical authored schema plus Wesley-generated bundle. + +Work: + +- point local protocol crates and packages at the chosen canonical protocol + bundle +- remove or clearly mark vendored schema and IR copies as derived or temporary +- keep Echo-owned hot runtime semantics and schema fragments separate from + host-neutral debugger protocol nouns +- verify the local compliance lane still passes against generated artifacts +- coordinate with `PLATFORM_ttd-schema-reconciliation` instead of reopening the + ownership question from scratch diff --git a/docs/method/retro/0004-strand-contract/retro.md b/docs/method/retro/0004-strand-contract/retro.md new file mode 100644 index 00000000..99d2ed3e --- /dev/null +++ b/docs/method/retro/0004-strand-contract/retro.md @@ -0,0 +1,97 @@ + + + +# Retro — 0004 strand-contract + +## What shipped + +The strand contract: types, registry, invariant document, and tests. + +**Code:** `crates/warp-core/src/strand.rs` + +- `StrandId` — domain-separated hash (prefix `b"strand:"`) +- `BaseRef` — immutable fork coordinate with exact semantics + (fork_tick = last included tick, commit_hash at fork_tick, + boundary_hash = output boundary, provenance_ref handle) +- `SupportPin` — braid geometry placeholder (empty in v1) +- `DropReceipt` — hard-delete proof +- `Strand` — relation descriptor with no lifecycle field +- `StrandRegistry` — `BTreeMap` with CRUD +- `StrandError` — typed error enum + +**Invariant document:** `docs/invariants/STRAND-CONTRACT.md` + +Ten invariants (INV-S1 through INV-S10) covering immutable base, +own heads, session scope, manual tick, complete base_ref, inherited +quantum, distinct worldlines, head ownership, empty support_pins, +and clean drop. + +**Tests:** 14 Rust integration tests + 12 shell assertions = 26 total, +all passing. + +## Playback witness + +### Human playback + +| # | Question | Answer | Witness | +| --- | ------------------------------------------------ | ------ | -------------------------------- | +| 1 | Does Strand struct have correct contract fields? | Yes | 18 Rust tests pass | +| 2 | Is base_ref pinned exactly? | Yes | inv_s5 + happy-path fork test | +| 3 | Are strand heads Dormant/Paused? | Yes | inv_s4 test | +| 4 | Are strand heads excluded from runnable set? | Yes | inv_s4_s10 integration test | +| 5 | Does registry reject invalid strands? | Yes | 3 rejection tests (S7/S8/S9) | +| 6 | Does remove surface NotFound errors? | Yes | registry_remove_nonexistent test | + +Note: orchestrated `create_strand` and `drop_strand` APIs (provenance +fork + head creation + registry insert with rollback) are defined in +the design doc but not yet implemented as a single API. The types, +registry, and invariant validation exist; the wiring is future work. + +### Agent playback + +| # | Question | Answer | Witness | +| --- | -------------------------------------------- | ------ | ------------------------------------- | +| 1 | Does Strand struct have all contract fields? | Yes | Type definition + v1_cardinality test | +| 2 | Is TTD mapping documented? | Yes | Invariant doc cross-references | +| 3 | Does list_strands filter correctly? | Yes | registry_list_by_base test | + +Full test output in `witness/`. + +## Drift check + +- **Design drift:** The first design draft had `StrandLifecycle` + (Created → Active → Dropped). Human review caught this as a second + scheduler truth. Eliminated: strand exists in registry = live, + not in registry = gone. Heads are the single source of truth. +- **Drop drift:** First draft both "transitioned to Dropped" and + "removed from registry." Human review caught the inconsistency. + Fixed to hard-delete with `DropReceipt`. +- **BaseRef drift:** First draft had the right fields but not the + right precision. Human review required exact coordinate semantics + (fork_tick = last included tick, boundary_hash = output boundary). + Fixed with `provenance_ref` handle added. +- **Invariant drift:** Original six invariants expanded to ten after + review: added INV-S7 (distinct worldlines), INV-S8 (head + ownership), INV-S9 (empty support_pins), INV-S10 (clean drop). + +## New debt + +- `create_strand` and `drop_strand` as orchestrated operations + (provenance fork → head creation → registry insert, with rollback) + are defined in the design doc but not yet implemented as a single + API. The types and registry exist; the wiring through the + coordinator or a standalone service is future work. +- `LocalProvenanceStore` has no `remove_worldline` method. Drop + currently relies on session-scoped lifetime. If mid-session drop + cleanup is needed, this must be added. + +## Cool ideas + +- Strand creation could emit a `ProvenanceEventKind::CrossWorldlineMessage` + as a "strand created" announcement, visible in the debugger's + timeline. This would give TTD a provenance-native anchor for + strand creation without inventing a new event kind. +- A `strand diff` command (like `git diff` between branches) would + let the debugger show exactly what changed because of a strand's + speculative ticks. This is the `compareStrand` operation from + git-warp, applied to Echo's richer provenance model. diff --git a/docs/method/retro/0004-strand-contract/witness/rust-test-output.txt b/docs/method/retro/0004-strand-contract/witness/rust-test-output.txt new file mode 100644 index 00000000..111fcb87 --- /dev/null +++ b/docs/method/retro/0004-strand-contract/witness/rust-test-output.txt @@ -0,0 +1,23 @@ + +running 18 tests +test drop_receipt_carries_correct_fields ... ok +test inv_s5_base_ref_fields_consistent ... ok +test inv_s7_child_and_base_worldlines_are_distinct ... ok +test inv_s9_support_pins_empty_on_creation ... ok +test inv_s4_strand_head_created_dormant_and_paused ... ok +test inv_s2_s8_strand_heads_belong_to_child_worldline ... ok +test registry_duplicate_insert_fails ... ok +test inv_s4_s10_dormant_strand_heads_excluded_from_runnable_set ... ok +test provenance_fork_creates_child_with_prefix ... ok +test registry_insert_and_get ... ok +test provenance_fork_happy_path_child_has_correct_prefix ... ok +test registry_insert_rejects_inv_s7_same_worldline ... ok +test registry_insert_rejects_inv_s8_wrong_head_worldline ... ok +test registry_insert_rejects_inv_s9_nonempty_support_pins ... ok +test registry_list_by_base_filters_correctly ... ok +test registry_remove_nonexistent_returns_error ... ok +test registry_remove_returns_strand_and_clears ... ok +test v1_strand_has_exactly_one_writer_head ... ok + +test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + diff --git a/docs/method/retro/0004-strand-contract/witness/shell-test-output.txt b/docs/method/retro/0004-strand-contract/witness/shell-test-output.txt new file mode 100644 index 00000000..d0d7e606 --- /dev/null +++ b/docs/method/retro/0004-strand-contract/witness/shell-test-output.txt @@ -0,0 +1,21 @@ +=== STRAND-CONTRACT invariant tests === + +1. Invariant document exists + PASS: docs/invariants/STRAND-CONTRACT.md exists + +2. Contains all ten invariant codes + PASS: INV-S1 present + PASS: INV-S2 present + PASS: INV-S3 present + PASS: INV-S4 present + PASS: INV-S5 present + PASS: INV-S6 present + PASS: INV-S7 present + PASS: INV-S8 present + PASS: INV-S9 present + PASS: INV-S10 present + +3. Normative language + PASS: contains MUST + +=== Results: 12 passed, 0 failed === diff --git a/scripts/tests/strand_contract_invariant_test.sh b/scripts/tests/strand_contract_invariant_test.sh new file mode 100755 index 00000000..da609053 --- /dev/null +++ b/scripts/tests/strand_contract_invariant_test.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +# +# Tests for cycle 0004: STRAND-CONTRACT invariant document. + +set -euo pipefail + +script_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_root}/../.." && pwd)" + +passed=0 +failed=0 + +assert() { + local label="$1" + shift + if (set +e; "$@" >/dev/null 2>&1); then + echo " PASS: ${label}" + ((passed++)) || true + else + echo " FAIL: ${label}" + ((failed++)) || true + fi +} + +invariant="${repo_root}/docs/invariants/STRAND-CONTRACT.md" + +echo "=== STRAND-CONTRACT invariant tests ===" +echo "" + +echo "1. Invariant document exists" +assert "docs/invariants/STRAND-CONTRACT.md exists" \ + test -f "${invariant}" + +echo "" +echo "2. Contains all ten invariant codes" +for code in INV-S1 INV-S2 INV-S3 INV-S4 INV-S5 INV-S6 INV-S7 INV-S8 INV-S9 INV-S10; do + assert "${code} present" \ + grep -Eq "(^### ${code}([[:space:]]|$)| ${code}[ :(])" "${invariant}" +done + +echo "" +echo "3. Normative language" +assert "contains MUST" \ + grep -q "MUST" "${invariant}" + +echo "" +echo "=== Results: ${passed} passed, ${failed} failed ===" + +if [ "${failed}" -gt 0 ]; then + exit 1 +fi