From f246f0711623617e7dd4f61d95642576bde44d0b Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 7 Oct 2026 04:12:36 -0700 Subject: [PATCH] docs: state reachable root coverage and bind detached-write evidence --- CHANGELOG.md | 2 + crates/warp-core/src/worldline_state.rs | 59 ++++++++++++++++++- .../application-contract-hosting.md | 2 + docs/topics/WAL.md | 2 + 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d99c3899b..b88b818f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### Fixed +- `WorldlineState::state_root` documentation now states its reachable-state boundary. Detached create-if-absent writes remain bound by patch and commit identities. Hash bytes are unchanged. + - `ReadView::at` validates the aperture and derives the selected current frontier basis with one state hash. `ReadView::new` still checks an independently selected expected basis. Both retain the same aperture and trust boundaries. diff --git a/crates/warp-core/src/worldline_state.rs b/crates/warp-core/src/worldline_state.rs index 096703d6f..c49b63346 100644 --- a/crates/warp-core/src/worldline_state.rs +++ b/crates/warp-core/src/worldline_state.rs @@ -247,7 +247,11 @@ impl WorldlineState { &self.warp_state } - /// Returns the canonical full-state root hash for this worldline. + /// Returns the canonical hash of WARP state reachable from this worldline's root. + /// + /// Detached nodes and their attachments are outside this hash boundary. + /// Equal state roots therefore do not prove equal stores. A committed + /// detached write remains bound by the tick patch digest and commit identity. #[must_use] pub fn state_root(&self) -> Hash { crate::snapshot::compute_state_root_for_warp_state(&self.warp_state, &self.root) @@ -483,6 +487,59 @@ mod tests { assert_eq!(ws.root().local_id, make_node_id("root")); } + #[test] + fn detached_creates_preserve_state_root_but_change_patch_and_commit_identity() { + use crate::attachment::{AtomPayload, AttachmentValue}; + use crate::snapshot::compute_commit_hash_v2; + use crate::tick_patch::{TickCommitStatus, WarpOp}; + + let mut state = WorldlineState::empty(); + let root = state.state_root(); + let mut patch_digests = Vec::new(); + let mut commit_ids = Vec::new(); + for label in ["detached-a", "detached-b"] { + let node = NodeKey { + warp_id: state.root().warp_id, + local_id: make_node_id(label), + }; + let slot = AttachmentKey::node_alpha(node); + let patch = WarpTickPatchV1::new( + 0, + [1; 32], + TickCommitStatus::Committed, + vec![crate::SlotId::Node(node), crate::SlotId::Attachment(slot)], + vec![crate::SlotId::Node(node), crate::SlotId::Attachment(slot)], + vec![ + WarpOp::UpsertNode { + node, + record: NodeRecord { + ty: make_type_id("cell"), + }, + }, + WarpOp::SetAttachment { + key: slot, + value: Some(AttachmentValue::Atom(AtomPayload::new( + make_type_id("value"), + bytes::Bytes::from_static(b"retained"), + ))), + }, + ], + ); + let result = patch.apply_to_state(&mut state.warp_state); + assert!(result.is_ok(), "detached create must apply: {result:?}"); + assert_eq!(state.state_root(), root); + assert!(state.store(&node.warp_id).is_some_and(|store| { + store.node(&node.local_id).is_some() + && store.node_attachment(&node.local_id).is_some() + })); + patch_digests.push(patch.digest()); + // Hold root, parents, and policy fixed to isolate the patch binding. + commit_ids.push(compute_commit_hash_v2(&root, &[], &patch.digest(), 0)); + } + assert_ne!(patch_digests[0], patch_digests[1]); + assert_ne!(commit_ids[0], commit_ids[1]); + } + #[test] fn worldline_frontier_starts_at_tick_zero() { let frontier = WorldlineFrontier::new(wl(1), WorldlineState::empty()); diff --git a/docs/architecture/application-contract-hosting.md b/docs/architecture/application-contract-hosting.md index 659899bd7..f9058ffba 100644 --- a/docs/architecture/application-contract-hosting.md +++ b/docs/architecture/application-contract-hosting.md @@ -163,6 +163,8 @@ state, frontier, or Receipt publication. The transitional direct prepare/commit seam can still return noncommitted evidence to trusted host tests, but it is not the application lifecycle. +The create-if-absent profile creates a node and its alpha attachment. It does not create a skeleton edge from the lane root. A detached cell therefore remains outside the reachable-state root hash. Equal roots do not prove equal stores or the absence of a detached write. The retained tick patch and commit identity bind that write; duplicate checks also compare the typed target-value digest. This preserves the [Merkle commit law](../spec/merkle-commit.md). + The external-provider schema additionally admits one exact zero-choice `compiler-produced-bounded-pure/v1` target configuration. It contains no application operation, target-specific budget override, or mutation authority. diff --git a/docs/topics/WAL.md b/docs/topics/WAL.md index 882623367..ad07dc020 100644 --- a/docs/topics/WAL.md +++ b/docs/topics/WAL.md @@ -245,6 +245,8 @@ buffer says it is dirty, because a file was written to disk, or because a UI event happened. It is durable because Echo recorded the accepted submission and later recorded any decided receipt under host-owned WAL authority. +State roots cover reachable WARP state. A detached create-if-absent cell can leave the root unchanged. The retained patch and commit bind that write. Root equality alone is not a complete recovery or no-mutation witness. + ## Recovery Postures The useful postures are: