From d8e38d46c3827b3a02ddf13bc4a3f82224a1f163 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Thu, 27 Aug 2026 18:50:53 +0700 Subject: [PATCH 1/2] Tighten runtime identity and query contracts Build pins from one resolved location, keep runtime-scoped text honest, and reject invalid revision transitions without breaking no-op watch polls. Preserve tree revision clocks across borrowed snapshots and extraction, reject revision exhaustion before mutation, and let hosts report real predicate work to traversal budgets. --- README.md | 6 +- crates/addressable/src/address.rs | 129 +++++++++++++---- crates/addressable/src/identity.rs | 36 ++++- crates/addressable/src/live.rs | 103 ++++++++++++- crates/addressable_reference/src/space.rs | 24 +-- crates/addressable_tree/src/lib.rs | 169 +++++++++++++++++----- docs/ARCHITECTURE.md | 16 +- docs/MIGRATION.md | 37 ++++- docs/adr/0002-tree-runtime-from-exedra.md | 22 ++- examples/addressable_tour/src/main.rs | 4 +- 10 files changed, 456 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 8be9f16..c7971e7 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,13 @@ on the workflow types use the same call paths and run as doctests. traversal budgets are explicit. - Pinned resolution reports stale, moved, or rebound outcomes instead of silently accepting a different referent. +- `Pinned::from_location` captures exact address, referent, and revision from + one observation. `SpaceId` and textual locator/pin forms are runtime-scoped, + not durable cross-process names. - Guarded transactions validate every operation before applying any operation. - Replaying a query delta produces the same snapshot as full recomputation; - another space or live-query stream is rejected atomically. + another space, live-query stream, regressing revision, or unclocked change is + rejected atomically. - Correspondence preserves one-to-many mappings and provenance, and composed mapping legs cannot disagree about their connecting source. - Dynamic tooling recovers a declared schema and uses the same typed guarded diff --git a/crates/addressable/src/address.rs b/crates/addressable/src/address.rs index 613d9d3..14552d5 100644 --- a/crates/addressable/src/address.rs +++ b/crates/addressable/src/address.rs @@ -12,7 +12,7 @@ use core::{ str::FromStr, }; -use crate::{Revision, SpaceId}; +use crate::{Location, Revision, SpaceId}; /// One validated address segment. /// @@ -71,7 +71,7 @@ pub enum NameError { /// /// Callers normally obtain one with [`Self::parse`], then place it in an exact /// [`Locator`] or use it as the base of a relative locator. Hosts retain the -/// canonical address in each resolved [`Location`](crate::Location). +/// canonical address in each resolved [`Location`]. pub struct AbsoluteAddress { segments: Box<[Name]>, marker: PhantomData S>, @@ -407,9 +407,11 @@ pub enum LocatorKind { /// A view-qualified resolution recipe in one runtime space instance. /// /// Callers construct locators and pass them to a domain host's resolution API. -/// Successful resolution normally produces a [`Location`](crate::Location). -/// Exact and relative locators both retain their structured form and can be -/// serialized when `V` implements [`core::fmt::Display`]. +/// Successful resolution normally produces a [`Location`]. +/// Exact and relative locators both retain their structured form. Their +/// [`Display`](core::fmt::Display) and [`FromStr`] representations are +/// runtime-scoped because [`SpaceId`] is runtime identity; do not persist or +/// exchange that text unless the host preserves the same space-id assignment. /// /// ``` /// use addressable::{AbsoluteAddress, Locator, RelativeAddress, SpaceId}; @@ -538,7 +540,7 @@ where } } -/// Failure to parse a canonical [`Locator`] document. +/// Failure to parse a runtime-scoped textual [`Locator`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum LocatorParseError { /// Length prefixes or structural separators were malformed. @@ -553,13 +555,37 @@ pub enum LocatorParseError { /// A locator pinned to expected semantic identity and revision. /// -/// Create a pin from a successfully resolved location when later resolution -/// must not silently accept staleness, movement, or rebinding. A domain host's -/// pinned-resolution API interprets the preconditions and returns a rich +/// Create a pin with [`Pinned::from_location`] after successful resolution when +/// later resolution must not silently accept staleness, movement, or rebinding. +/// The pin uses the location's canonical exact address and captures its +/// referent and revision as one observation. A domain host's pinned-resolution +/// API interprets those preconditions and returns a rich /// [`Resolution`](crate::Resolution) outcome. -/// Use the original locator, the returned [`Location::referent`](crate::Location::referent), -/// and its [`Location::revision`](crate::Location::revision) together; mixing -/// observations from different resolutions defeats the pin's meaning. +/// +/// Like [`Locator`] text, the [`Display`](core::fmt::Display) and [`FromStr`] +/// representation is runtime-scoped because it contains a [`SpaceId`]. +/// +/// ``` +/// use addressable::{AbsoluteAddress, Location, Pinned, Revision, SpaceId}; +/// +/// enum Space {} +/// #[derive(Clone, Debug, PartialEq, Eq)] +/// enum View { Assembly } +/// +/// let space = SpaceId::::new(7); +/// let location = Location::new( +/// View::Assembly, +/// Revision::new(space, 3), +/// 42_u64, +/// 9_u64, +/// AbsoluteAddress::parse("/basilica/nave")?, +/// ); +/// let pinned = Pinned::from_location(&location); +/// +/// assert_eq!(pinned.expected_referent(), &42); +/// assert_eq!(pinned.expected_revision(), location.revision()); +/// # Ok::<(), addressable::AddressError>(()) +/// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Pinned { locator: Locator, @@ -568,13 +594,29 @@ pub struct Pinned { } impl Pinned { - /// Pins a locator to identity observed at `expected_revision`. + /// Pins one successfully resolved location as a canonical exact locator. /// - /// The locator, identity, and revision must come from one successful - /// resolution. Addressable stores that evidence; the resolving host checks - /// it when the pin is used. + /// This constructor captures the address, referent identity, and revision + /// from the same observation, so callers cannot accidentally mix evidence + /// from different resolutions. #[must_use] - pub const fn new( + pub fn from_location(location: &Location) -> Self + where + V: Clone, + I: Clone, + { + Self { + locator: Locator::exact( + location.space(), + location.view().clone(), + location.address().clone(), + ), + expected_referent: location.referent().clone(), + expected_revision: location.revision(), + } + } + + const fn from_parts( locator: Locator, expected_referent: I, expected_revision: Revision, @@ -635,7 +677,7 @@ where fn from_str(text: &str) -> Result { let (locator, rest) = take_length_prefixed(text).ok_or(PinnedParseError::InvalidSyntax)?; - let locator = locator.parse().map_err(PinnedParseError::InvalidLocator)?; + let locator: Locator = locator.parse().map_err(PinnedParseError::InvalidLocator)?; let (identity, revision) = take_length_prefixed(rest).ok_or(PinnedParseError::InvalidSyntax)?; let revision = revision @@ -653,15 +695,15 @@ where let revision = revision .parse::() .map_err(|_| PinnedParseError::InvalidRevision)?; - Ok(Self::new( - locator, - identity, - Revision::new(SpaceId::new(revision_space), revision), - )) + let expected_revision = Revision::new(SpaceId::new(revision_space), revision); + if locator.space() != expected_revision.space() { + return Err(PinnedParseError::SpaceMismatch); + } + Ok(Self::from_parts(locator, identity, expected_revision)) } } -/// Failure to parse a canonical [`Pinned`] document. +/// Failure to parse a runtime-scoped textual [`Pinned`] value. #[derive(Clone, Debug, PartialEq, Eq)] pub enum PinnedParseError { /// Length prefixes or structural separators were malformed. @@ -672,6 +714,8 @@ pub enum PinnedParseError { InvalidIdentity(I), /// Expected revision was not a `u64`. InvalidRevision, + /// The locator and expected revision name different runtime spaces. + SpaceMismatch, } fn take_length_prefixed(text: &str) -> Option<(&str, &str)> { @@ -686,7 +730,7 @@ mod tests { use alloc::string::ToString; use super::{AbsoluteAddress, AddressError, Locator, Pinned, RelativeAddress}; - use crate::{Revision, SpaceId}; + use crate::{Location, Revision, SpaceId}; #[derive(Debug, PartialEq, Eq)] struct Space; @@ -760,7 +804,16 @@ mod tests { relative ); - let pinned = Pinned::new(relative, 42_u64, Revision::new(SpaceId::new(7), 9)); + let pinned_location = Location::new( + 2, + Revision::new(SpaceId::new(7), 9), + 42_u64, + 3_u64, + relative + .to_absolute() + .expect("relative locator materializes"), + ); + let pinned = Pinned::from_location(&pinned_location); let pinned_text = pinned.to_string(); assert_eq!( pinned_text @@ -769,4 +822,28 @@ mod tests { pinned ); } + + #[test] + fn pinned_document_rejects_disagreeing_space_identities() { + let locator = Locator::::exact( + SpaceId::new(7), + 2, + AbsoluteAddress::parse("/basilica/nave").expect("valid exact address"), + ); + let locator_text = locator.to_string(); + let text = alloc::format!( + "{}:{}{}:{}:{}:{}", + locator_text.len(), + locator_text, + 2, + 42, + 8, + 9 + ); + + assert!(matches!( + text.parse::>(), + Err(super::PinnedParseError::SpaceMismatch) + )); + } } diff --git a/crates/addressable/src/identity.rs b/crates/addressable/src/identity.rs index 7285453..5a466db 100644 --- a/crates/addressable/src/identity.rs +++ b/crates/addressable/src/identity.rs @@ -163,10 +163,27 @@ impl Revision { self.sequence } - /// Returns the next revision in the same space, wrapping only after `u64::MAX`. + /// Returns the next revision in the same space. + /// + /// # Panics + /// + /// Panics when the host-owned sequence is exhausted. Revision reuse would + /// allow stale pins, guards, and handles to appear current again. Hosts + /// that need to handle exhaustion explicitly can use + /// [`Self::checked_next`]. #[must_use] pub const fn next(self) -> Self { - Self::new(self.space, self.sequence.wrapping_add(1)) + self.checked_next() + .expect("address-space revisions exhausted") + } + + /// Returns the next revision, or `None` when the sequence is exhausted. + #[must_use] + pub const fn checked_next(self) -> Option { + match self.sequence.checked_add(1) { + Some(sequence) => Some(Self::new(self.space, sequence)), + None => None, + } } } @@ -414,4 +431,19 @@ mod tests { ); assert_ne!(a, b, "location equality includes occurrence context"); } + + #[test] + #[should_panic(expected = "address-space revisions exhausted")] + fn revision_exhaustion_does_not_wrap() { + let revision = Revision::new(SpaceId::::new(1), u64::MAX); + + let _ = revision.next(); + } + + #[test] + fn revision_exhaustion_can_be_reported_without_panicking() { + let revision = Revision::new(SpaceId::::new(1), u64::MAX); + + assert_eq!(revision.checked_next(), None); + } } diff --git a/crates/addressable/src/live.rs b/crates/addressable/src/live.rs index 3e9d336..3fd05f5 100644 --- a/crates/addressable/src/live.rs +++ b/crates/addressable/src/live.rs @@ -218,9 +218,10 @@ where K: Clone + Eq, T: Clone + Eq, { - /// Applies one delta atomically. + /// Applies one forward delta atomically. /// - /// On error, `self` is unchanged. + /// The destination revision must be later than the source unless this is an + /// empty no-op delta. On error, `self` is unchanged. pub fn apply(&mut self, delta: &QueryDelta) -> Result<(), DeltaError> { if self.live_query != delta.live_query { return Err(DeltaError::LiveQueryMismatch); @@ -234,6 +235,14 @@ where actual: delta.from_revision, }); } + if delta.to_revision.get() < delta.from_revision.get() + || (delta.to_revision == delta.from_revision && !delta.changes.is_empty()) + { + return Err(DeltaError::InvalidRevisionTransition { + from: delta.from_revision, + to: delta.to_revision, + }); + } if self.identity != delta.identity { return Err(DeltaError::IdentityMismatch); } @@ -345,6 +354,10 @@ pub struct QueryDelta { impl QueryDelta { /// Creates a delta from ordered structural changes on behalf of a host. + /// + /// Hosts must supply a destination revision later than the source, except + /// for an empty no-op delta. [`QuerySnapshot::apply`] validates that + /// invariant before replay. #[must_use] pub fn new( live_query: LiveQueryId, @@ -398,7 +411,10 @@ where K: Clone + Eq, T: Clone + Eq, { - /// Computes a deterministic delta between complete snapshots. + /// Computes a deterministic forward delta between complete snapshots. + /// + /// `after` must belong to the same space and live-query stream. Its revision + /// may equal `before` only when the ordered entries are unchanged. pub fn between( before: &QuerySnapshot, after: &QuerySnapshot, @@ -409,6 +425,14 @@ where if before.revision.space() != after.revision.space() { return Err(DeltaError::SpaceMismatch); } + if after.revision.get() < before.revision.get() + || (after.revision == before.revision && after.entries != before.entries) + { + return Err(DeltaError::InvalidRevisionTransition { + from: before.revision, + to: after.revision, + }); + } if before.identity != after.identity { return Err(DeltaError::IdentityMismatch); } @@ -515,6 +539,13 @@ pub enum DeltaError { /// Delta's declared previous revision. actual: Revision, }, + /// The delta moves backward or changes entries without advancing. + InvalidRevisionTransition { + /// Delta's declared previous revision. + from: Revision, + /// Delta's invalid destination revision. + to: Revision, + }, /// Snapshot and delta use different live-entry identities. IdentityMismatch, /// A snapshot contains duplicate stable keys. @@ -593,6 +624,72 @@ mod tests { assert_eq!(snapshot, original); } + #[test] + fn replay_rejects_invalid_revision_transitions_atomically() { + let space = SpaceId::::new(1); + let stream = LiveQueryId::::new(10); + let mut snapshot = QuerySnapshot::new( + stream, + Revision::new(space, 4), + ResultIdentity::Entry, + [ResultEntry::new(1_u8, "one")], + ); + let original = snapshot.clone(); + let rewind = QueryDelta::new( + stream, + Revision::new(space, 4), + Revision::new(space, 3), + ResultIdentity::Entry, + [], + ); + + assert!(matches!( + snapshot.apply(&rewind), + Err(DeltaError::InvalidRevisionTransition { .. }) + )); + assert_eq!(snapshot, original, "failed replay must remain atomic"); + + let earlier = QuerySnapshot::new( + stream, + Revision::new(space, 3), + ResultIdentity::Entry, + [ResultEntry::new(1_u8, "one")], + ); + assert!(matches!( + QueryDelta::between(&original, &earlier), + Err(DeltaError::InvalidRevisionTransition { .. }) + )); + + let no_op = QueryDelta::new( + stream, + Revision::new(space, 4), + Revision::new(space, 4), + ResultIdentity::Entry, + [], + ); + snapshot + .apply(&no_op) + .expect("an empty poll is a valid no-op"); + assert_eq!(snapshot, original); + + let unclocked_change = QueryDelta::new( + stream, + Revision::new(space, 4), + Revision::new(space, 4), + ResultIdentity::Entry, + [super::QueryChange::Updated { + index: 0, + old: ResultEntry::new(1_u8, "one"), + new: ResultEntry::new(1_u8, "changed"), + }], + ); + assert!(matches!( + snapshot.apply(&unclocked_change), + Err(DeltaError::InvalidRevisionTransition { .. }) + )); + assert_eq!(snapshot, original); + } + #[test] fn delta_replay_agrees_with_full_recomputation() { let space = SpaceId::::new(1); diff --git a/crates/addressable_reference/src/space.rs b/crates/addressable_reference/src/space.rs index 436920f..63a1a2b 100644 --- a/crates/addressable_reference/src/space.rs +++ b/crates/addressable_reference/src/space.rs @@ -13,7 +13,7 @@ use addressable::{ QueryResults, QuerySemantics, QueryStats, QueryStep, Resolution, ResolvedHandle, ResultOrdering, Revision, SpaceId, TraversalBudget, VisitIdentity, }; -use addressable_tree::{HostNode, TreeAxis, TreeHost, TreeNode, TreeRuntime}; +use addressable_tree::{HostNode, PredicateMatch, TreeAxis, TreeHost, TreeNode, TreeRuntime}; use crate::model::{ BasilicaAxis, BasilicaLocation, BasilicaLocator, BasilicaPredicate, BasilicaQuery, @@ -165,10 +165,9 @@ impl Basilica { /// Resolves a pinned locator without silently accepting rebinding. /// - /// Form the pin from one successful resolution: retain the locator and pair - /// it with the returned location's referent and revision. The result then - /// distinguishes a still-valid target from movement, rebinding, staleness, - /// absence, and ambiguity. + /// Form the pin from one successful resolution with + /// [`Pinned::from_location`]. The result then distinguishes a still-valid + /// target from movement, rebinding, staleness, absence, and ambiguity. /// /// ``` /// use addressable::{Pinned, Resolution, SpaceId}; @@ -179,7 +178,7 @@ impl Basilica { /// let Resolution::Resolved(root) = space.resolve(&locator) else { /// panic!("reference root must resolve"); /// }; - /// let pinned = Pinned::new(locator, *root.referent(), root.revision()); + /// let pinned = Pinned::from_location(&root); /// let Resolution::Resolved(root_again) = space.resolve_pinned(&pinned) else { /// panic!("unchanged pin must resolve"); /// }; @@ -418,7 +417,7 @@ impl Basilica { } fn assembly_runtime(&self) -> TreeRuntime<&Self> { - TreeRuntime::resume(self.revision, self) + TreeRuntime::from_revision(self.revision, self) } fn assembly_query( @@ -719,16 +718,17 @@ impl TreeHost for Basilica { (parent.view == *view).then(|| self.projected_node(parent)) } - fn matches(&self, node: &HostNode, predicate: &Self::Predicate) -> bool { + fn matches(&self, node: &HostNode, predicate: &Self::Predicate) -> PredicateMatch { let Some(feature) = self.feature(*node.referent()) else { - return false; + return PredicateMatch::new(false, 1); }; - match predicate { + let matched = match predicate { BasilicaPredicate::Any => true, BasilicaPredicate::Kind(kind) => feature.kind == *kind, BasilicaPredicate::LoadAtLeast(threshold) => feature.effective_load() >= *threshold, BasilicaPredicate::NameContains(fragment) => feature.name.contains(fragment), - } + }; + PredicateMatch::new(matched, 1) } } @@ -840,7 +840,7 @@ mod tests { assert_eq!(north.referent(), south.referent()); assert_ne!(north.occurrence(), south.occurrence()); - let pinned = Pinned::new(exact, *north.referent(), space.revision()); + let pinned = Pinned::from_location(&north); space .occurrences .iter_mut() diff --git a/crates/addressable_tree/src/lib.rs b/crates/addressable_tree/src/lib.rs index ff4ed79..178f37d 100644 --- a/crates/addressable_tree/src/lib.rs +++ b/crates/addressable_tree/src/lib.rs @@ -10,7 +10,7 @@ //! //! ``` //! use addressable::{AbsoluteAddress, Query, Resolution, SpaceId}; -//! use addressable_tree::{TreeAxis, TreeHost, TreeNode, TreeRuntime}; +//! use addressable_tree::{PredicateMatch, TreeAxis, TreeHost, TreeNode, TreeRuntime}; //! //! #[derive(Clone, Copy, Debug, PartialEq, Eq)] //! enum View { Instances } @@ -60,8 +60,12 @@ //! None //! } //! -//! fn matches(&self, _node: &TreeNode, predicate: &Predicate) -> bool { -//! *predicate == Predicate::Any +//! fn matches( +//! &self, +//! _node: &TreeNode, +//! predicate: &Predicate, +//! ) -> PredicateMatch { +//! PredicateMatch::new(*predicate == Predicate::Any, 1) //! } //! } //! @@ -146,6 +150,30 @@ impl TreeNode { } } +/// Result of evaluating one host-owned predicate against a projected node. +/// +/// [`TreeRuntime`] adds `work` to +/// [`QueryStats::work_units`](addressable::QueryStats::work_units). Hosts choose +/// a stable unit meaningful for their domain: a constant in-memory comparison +/// commonly costs one, while resolving an opinion stack or consulting an +/// external index may cost more. The work value may be zero for a cached result +/// that performs no observable host work. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PredicateMatch { + /// Whether the node satisfies the predicate. + pub matched: bool, + /// Host-defined work performed to decide the match. + pub work: u32, +} + +impl PredicateMatch { + /// Reports a predicate result and its host-defined work. + #[must_use] + pub const fn new(matched: bool, work: u32) -> Self { + Self { matched, work } + } +} + /// Host-owned projection required by [`TreeRuntime`]. /// /// The host retains storage and indexing choices. Every method observes one @@ -203,8 +231,13 @@ pub trait TreeHost: Sized { /// Projects the parent of one occurrence, if it has one. fn parent(&self, view: &Self::View, occurrence: &Self::Occurrence) -> Option>; - /// Evaluates one domain predicate against a projected node. - fn matches(&self, node: &HostNode, predicate: &Self::Predicate) -> bool; + /// Evaluates one domain predicate and reports the work it performed. + /// + /// The runtime charges the returned [`PredicateMatch::work`] against the + /// query's + /// [`TraversalBudget::max_work`](addressable::TraversalBudget::max_work). + /// A constant in-memory comparison normally reports one work unit. + fn matches(&self, node: &HostNode, predicate: &Self::Predicate) -> PredicateMatch; } impl TreeHost for &H { @@ -251,7 +284,7 @@ impl TreeHost for &H { H::parent(self, view, occurrence) } - fn matches(&self, node: &HostNode, predicate: &Self::Predicate) -> bool { + fn matches(&self, node: &HostNode, predicate: &Self::Predicate) -> PredicateMatch { H::matches(self, node, predicate) } } @@ -307,8 +340,10 @@ pub type TreeQuery = Query, TreeAxis, { id: SpaceId, @@ -318,11 +353,12 @@ pub struct TreeRuntime { struct AdvanceRevision<'a, S> { revision: &'a mut Revision, + next: Revision, } impl Drop for AdvanceRevision<'_, S> { fn drop(&mut self) { - *self.revision = self.revision.next(); + *self.revision = self.next; } } @@ -330,8 +366,8 @@ impl TreeRuntime { /// Binds a host value to one runtime address-space identity. /// /// `id` must identify a genuinely new space instance. Restore an extracted - /// instance with [`Self::resume`]; reusing its id with `new` would restart - /// the clock and could make stale observations appear current. + /// instance with [`Self::from_revision`]; reusing its id with `new` would + /// restart the clock and could make stale observations appear current. #[must_use] pub fn new(id: SpaceId, host: H) -> Self { Self { @@ -341,12 +377,13 @@ impl TreeRuntime { } } - /// Restores a host with its previously recorded runtime revision. + /// Binds a host value at an existing host-owned revision. /// - /// Use the values returned by [`Self::into_host`]. The revision already - /// carries the owning [`SpaceId`], so a mismatched pair cannot be supplied. + /// Use this for a host snapshot that already owns its revision, including + /// the values returned by [`Self::into_host`]. The revision already carries + /// the owning [`SpaceId`], so a mismatched pair cannot be supplied. #[must_use] - pub const fn resume(revision: Revision, host: H) -> Self { + pub const fn from_revision(revision: Revision, host: H) -> Self { Self { id: revision.space(), revision, @@ -374,8 +411,9 @@ impl TreeRuntime { /// Recovers the revision and host value, consuming the live runtime. /// - /// Pass both values to [`Self::resume`] to restore the same clock. Mutation - /// should normally remain inside the runtime through [`Self::commit`]. + /// Pass both values to [`Self::from_revision`] to restore the same clock. + /// Mutation should normally remain inside the runtime through + /// [`Self::commit`]. #[must_use] pub fn into_host(self) -> (Revision, H) { (self.revision, self.host) @@ -389,9 +427,11 @@ impl TreeRuntime { /// revision also advances if the closure unwinds after partially mutating /// the host, so a caught panic cannot leave changed data at the old clock. pub fn commit(&mut self, mutation: impl FnOnce(&mut H) -> T) -> (Revision, T) { + let next = self.revision.next(); let value = { let _advance = AdvanceRevision { revision: &mut self.revision, + next, }; mutation(&mut self.host) }; @@ -404,8 +444,9 @@ impl TreeRuntime { /// precondition and successfully applying every operation to a private /// replacement value. Dry runs and no-op batches retain the current host. pub fn replace_host(&mut self, host: H) -> Revision { + let next = self.revision.next(); self.host = host; - self.revision = self.revision.next(); + self.revision = next; self.revision } @@ -581,12 +622,14 @@ impl TreeRuntime { self.traverse(locator.view(), &frontier, *axis, semantics, &mut stats)? } QueryStep::Filter(predicate) => { - let inspected = u32::try_from(frontier.len()).unwrap_or(u32::MAX); - let filtered = frontier - .into_iter() - .filter(|node| self.host.matches(node, predicate)) - .collect(); - stats.charge_work(semantics.budget, inspected)?; + let mut filtered = Vec::with_capacity(frontier.len()); + for node in frontier { + let predicate_match = self.host.matches(&node, predicate); + stats.charge_work(semantics.budget, predicate_match.work)?; + if predicate_match.matched { + filtered.push(node); + } + } filtered } }; @@ -752,10 +795,10 @@ mod tests { use addressable::{ AbsoluteAddress, Deduplication, Locator, Pinned, Query, QueryError, RelativeAddress, - Resolution, SpaceId, TraversalBudget, + Resolution, Revision, SpaceId, TraversalBudget, }; - use super::{TreeAxis, TreeHost, TreeNode, TreeReadError, TreeRuntime}; + use super::{PredicateMatch, TreeAxis, TreeHost, TreeNode, TreeReadError, TreeRuntime}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Space {} @@ -769,6 +812,7 @@ mod tests { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Predicate { Referent(u64), + Expensive, } #[derive(Clone, Debug)] @@ -783,6 +827,7 @@ mod tests { struct Host { nodes: Vec, node_lookups: Cell, + predicate_work: Cell, } impl Host { @@ -796,6 +841,7 @@ mod tests { stored(4, 20, Some(2), "/root/a/leaf"), ], node_lookups: Cell::new(0), + predicate_work: Cell::new(0), } } @@ -861,9 +907,20 @@ mod tests { .map(Self::project) } - fn matches(&self, node: &TreeNode, predicate: &Predicate) -> bool { + fn matches( + &self, + node: &TreeNode, + predicate: &Predicate, + ) -> PredicateMatch { match predicate { - Predicate::Referent(referent) => node.referent() == referent, + Predicate::Referent(referent) => { + PredicateMatch::new(node.referent() == referent, 1) + } + Predicate::Expensive => { + self.predicate_work + .set(self.predicate_work.get().saturating_add(4)); + PredicateMatch::new(true, 4) + } } } } @@ -906,7 +963,7 @@ mod tests { "exact and relative recipes reach one location" ); - let pin = Pinned::new(exact, *exact_location.referent(), exact_location.revision()); + let pin = Pinned::from_location(&exact_location); assert!( matches!(runtime.resolve_pinned(&pin), Resolution::Resolved(_)), "fresh pin resolves normally" @@ -968,6 +1025,24 @@ mod tests { ); } + #[test] + fn predicate_evaluation_reports_host_defined_work() { + let runtime = runtime(); + let results = runtime + .query_many( + &Query::many(runtime.root_locator(View::Instances)) + .traverse(TreeAxis::Descendants) + .filter(Predicate::Expensive), + ) + .expect("query stays within the default budget"); + + assert_eq!( + results.stats().work_units, + 5 + runtime.host().predicate_work.get(), + "query statistics must report the work the host performed" + ); + } + #[test] fn budget_failure_and_host_replacement_are_revision_safe() { let mut runtime = runtime(); @@ -1015,7 +1090,7 @@ mod tests { } #[test] - fn commit_and_resume_preserve_the_revision_clock() { + fn commit_and_reconstruction_preserve_the_revision_clock() { let mut runtime = runtime(); let locator = Locator::exact( runtime.id(), @@ -1025,7 +1100,7 @@ mod tests { let Resolution::Resolved(location) = runtime.resolve(&locator) else { panic!("pin target resolves"); }; - let pin = Pinned::new(locator, *location.referent(), location.revision()); + let pin = Pinned::from_location(&location); runtime.commit(|host| host.nodes.reverse()); assert!(matches!( @@ -1034,10 +1109,10 @@ mod tests { )); let (revision, host) = runtime.into_host(); - let resumed = TreeRuntime::resume(revision, host); - assert_eq!(resumed.revision(), revision); + let reconstructed = TreeRuntime::from_revision(revision, host); + assert_eq!(reconstructed.revision(), revision); assert!(matches!( - resumed.resolve_pinned(&pin), + reconstructed.resolve_pinned(&pin), Resolution::StaleRevision { .. } )); } @@ -1057,4 +1132,30 @@ mod tests { assert!(outcome.is_err()); assert_eq!(runtime.revision(), revision.next()); } + + #[test] + fn exhausted_revision_rejects_host_changes_before_mutation() { + let space = SpaceId::new(7); + let revision = Revision::new(space, u64::MAX); + let mut runtime = TreeRuntime::from_revision(revision, Host::new()); + let node_count = runtime.host().nodes.len(); + + let outcome = catch_unwind(AssertUnwindSafe(|| { + runtime.commit(|host| host.nodes.clear()); + })); + + assert!(outcome.is_err()); + assert_eq!(runtime.revision(), revision); + assert_eq!(runtime.host().nodes.len(), node_count); + + let mut replacement = Host::new(); + replacement.nodes.clear(); + let outcome = catch_unwind(AssertUnwindSafe(|| { + runtime.replace_host(replacement); + })); + + assert!(outcome.is_err()); + assert_eq!(runtime.revision(), revision); + assert_eq!(runtime.host().nodes.len(), node_count); + } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b1330ad..65b3498 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -21,6 +21,13 @@ Type-level space markers should prevent accidental interchange between domains where possible. Runtime space identity is still needed when multiple instances of one typed space coexist. +`SpaceId` is runtime identity, not a durable global name. Consequently, +formatted `Locator` and `Pinned` values are safe only within a lifetime or +protocol that preserves the same space-id assignment. A future persisted or +cross-process envelope needs a durable, namespaced space identity and version; +the typed runtime core must not imply that contract before a transport consumer +can prove it. + ### Referent and occurrence A referent is the semantic thing. An occurrence is one contextual appearance of @@ -43,6 +50,11 @@ the same referent. - A `Pinned` reference combines a locator with an expected identity, revision, or fingerprint and must report rebinding rather than silently accepting it. +A pin is formed from one resolved `Location`, using that location's canonical +exact address, referent, and revision together. This prevents callers from +accidentally mixing evidence from different resolutions or retaining relative +navigation as a rebinding recipe. + ### Endpoint and edge An endpoint combines a located owner with a typed facet such as a property, @@ -214,7 +226,9 @@ The initial implementation should turn these into tests or conformance cases: 5. Occurrence equality does not imply or erase referent equality. 6. Query ordering and deduplication are deterministic when requested. 7. Traversal terminates under declared cycle and budget policy. -8. Live delta replay agrees with full recomputation. +8. Live delta replay agrees with full recomputation. Changes advance + monotonically within one space and live-query stream; an empty same-revision + poll is a no-op. 9. Failed guarded transactions have no partial observable effect. 10. Correspondence composition preserves ambiguity and provenance. 11. Typed and dynamic execution agree for representable queries and operations. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index b1c9452..a051d5b 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -24,13 +24,27 @@ space and sequence. Documents produced by the earlier unpublished bootstrap format should be reparsed and re-emitted by an adapter that supplies the space recorded in their embedded locator. +Create new pins with `Pinned::from_location(&location)`. The earlier +three-argument `Pinned::new` constructor was removed because it allowed a +locator, referent, and revision from different observations to be combined. +The new constructor records the resolved location's canonical exact address. +Parsing also rejects a locator whose `SpaceId` disagrees with the expected +revision's space. + +The textual forms of `Locator` and `Pinned` are now documented as +runtime-scoped. `SpaceId` is not a durable global name, so do not persist or +exchange those forms unless the host preserves the same id assignment. + ## Live-query scope `QuerySnapshot`, `QueryDelta`, and `DeltaError` gain a space marker parameter. Snapshot and delta constructors also take a host-assigned `LiveQueryId`. The reference `Basilica::watch` method now takes `&mut self` so it can allocate that local id. Replay rejects mismatched live-query ids, mismatched space -revisions, and deltas that transition between spaces. +revisions, deltas that transition between spaces, and deltas whose destination +revision moves backward or carries changes without advancing. Handle +`DeltaError::InvalidRevisionTransition` when matching replay or differencing +failures. An empty same-revision delta remains a valid no-op poll. ## Closed cardinality markers @@ -67,15 +81,22 @@ the adapter to typed host state. ## Tree runtime revisions and host iteration `TreeRuntime::into_host` now returns `(Revision, H)`. Restore that pair with -`TreeRuntime::resume(revision, host)`; do not call `new` with an id belonging to -an existing space instance. Apply already-validated mutations through -`TreeRuntime::commit` so the revision advances without cloning the whole host. +`TreeRuntime::from_revision(revision, host)`; do not call `new` with an id +belonging to an existing space instance. Hosts that already own a revision use +the same constructor for coherent snapshots. Apply already-validated mutations +through `TreeRuntime::commit` so the revision advances without cloning the +whole host. `TreeHost::nodes` and `TreeHost::children` now return iterators instead of appending to output vectors. Referent and occurrence identities must implement `Ord`, and hosts may override `occurrences_of` to use an index. A blanket implementation makes `&H` a read-only tree host whenever `H` is one. +`TreeHost::matches` now returns `PredicateMatch` instead of `bool`. Report one +work unit for a constant in-memory check, or a domain-defined cost for work such +as resolving a composed opinion stack. The runtime charges that value against +`TraversalBudget::max_work`. + ## Resolution and live change exhaustiveness `Resolution` is now exhaustive and accepts an optional fifth type parameter @@ -87,3 +108,11 @@ not appropriate. not produce it without typed referent evidence. Use `Updated` for observable value changes; a future rebound event must carry enough typed identity for the producer and replay logic to agree on its meaning. + +## Revision exhaustion + +`Revision::next` now panics instead of wrapping after `u64::MAX`, because +reusing an earlier revision could make stale evidence appear current. Hosts +that need to report exhaustion use `Revision::checked_next`. `TreeRuntime` +checks for an available revision before invoking a commit closure or replacing +its host, so exhaustion cannot leave a mutation at the old revision. diff --git a/docs/adr/0002-tree-runtime-from-exedra.md b/docs/adr/0002-tree-runtime-from-exedra.md index 49fafda..f980751 100644 --- a/docs/adr/0002-tree-runtime-from-exedra.md +++ b/docs/adr/0002-tree-runtime-from-exedra.md @@ -38,16 +38,23 @@ and occurrence identities are ordered so cycle detection and deduplication use traversal budgets; - validated revision-scoped runtime handles. +Predicate matching returns `PredicateMatch` rather than a bare boolean. The +result includes host-defined work, which the runtime charges against the query +budget. Layerstack supplied the concrete need: testing a composed field may +resolve an arbitrary opinion stack, so charging one unit merely because one +frontier node was tested would make `max_work` misleading. + Cardinality shaping through `Measured` and budget charging through `QueryStats` remain in `addressable`, because both tree and specialized graph evaluators use those host-independent query laws. The runtime exposes immutable host access and an infallible in-place commit that advances its revision once. Extraction returns the revision with the host, -and `resume` restores that clock. Domain transactions remain responsible for -validating fallible preconditions before commit. A drop guard advances the -clock during unwinding as well, so a caught panic cannot expose a partially -mutated host at its old revision. +and `from_revision` restores that clock or binds a host snapshot that already +owns one. Domain transactions remain responsible for validating fallible +preconditions before commit. A drop guard advances the clock during unwinding +as well, so a caught panic cannot expose a partially mutated host at its old +revision. ## Consequences @@ -58,6 +65,11 @@ mutated host at its old revision. their specialized evaluator. - Non-tree relationship views retain specialized evaluators; this runtime does not force the reference dependency graph into a tree abstraction. -- The dependency-free `addressable` semantic nucleus remains unchanged. +- The dependency-free `addressable` semantic nucleus does not absorb tree + traversal or host storage. +- Overstory's retained inspection snapshot and a composed Layerstack stage can + both be borrowed as tree hosts without surrendering storage, revision, path + interning, or composition ownership. Those additive experiments remain + consumer-side evidence rather than APIs to land for their own sake. - Watches can later recompute the same typed tree query through this runtime, but live-query scheduling is not pulled into this slice. diff --git a/examples/addressable_tour/src/main.rs b/examples/addressable_tour/src/main.rs index f147bfd..3e2c54b 100644 --- a/examples/addressable_tour/src/main.rs +++ b/examples/addressable_tour/src/main.rs @@ -77,11 +77,11 @@ fn addresses_and_identity(basilica: &Basilica) -> FeatureId { "north and south appearances must remain distinct", ); - let pinned = Pinned::new(north_locator, *north.referent(), basilica.revision()); + let pinned = Pinned::from_location(&north); let pinned_document = pinned.to_string(); let decoded_pin = pinned_document .parse::>() - .expect("canonical pin must parse"); + .expect("runtime-scoped pin must parse"); assert_eq!( decoded_pin, pinned, "pinned locator serialization must round-trip", From b1011c094d42de0efd0e54e67294b379aa159067 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Thu, 27 Aug 2026 18:51:21 +0700 Subject: [PATCH 2/2] Prepare core crates for 0.1.0 Mark addressable and addressable_tree as the release boundary while keeping the reference, tooling, and tour internal. Add standalone package documentation, changelogs, dual-license materials, docs.rs metadata, package CI, and the consumer evidence that shaped the release. --- .github/workflows/ci.yml | 16 +++ PLANS.md | 91 +++++++++---- STATUS.md | 57 ++++---- crates/addressable/CHANGELOG.md | 15 +++ crates/addressable/Cargo.toml | 7 +- crates/addressable/LICENSE-APACHE | 176 +++++++++++++++++++++++++ crates/addressable/LICENSE-MIT | 19 +++ crates/addressable/README.md | 74 +++++++++++ crates/addressable_tree/CHANGELOG.md | 13 ++ crates/addressable_tree/Cargo.toml | 6 +- crates/addressable_tree/LICENSE-APACHE | 176 +++++++++++++++++++++++++ crates/addressable_tree/LICENSE-MIT | 19 +++ crates/addressable_tree/README.md | 88 +++++++++++-- 13 files changed, 692 insertions(+), 65 deletions(-) create mode 100644 crates/addressable/CHANGELOG.md create mode 100644 crates/addressable/LICENSE-APACHE create mode 100644 crates/addressable/LICENSE-MIT create mode 100644 crates/addressable/README.md create mode 100644 crates/addressable_tree/CHANGELOG.md create mode 100644 crates/addressable_tree/LICENSE-APACHE create mode 100644 crates/addressable_tree/LICENSE-MIT diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 220a1d3..91bb1f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,6 +159,22 @@ jobs: env: RUSTDOCFLAGS: "-D warnings" + package: + name: cargo package --list + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: install stable toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_STABLE_VER }} + + - name: cargo package --list + run: | + cargo package -p addressable --locked --list >/dev/null + cargo package -p addressable_tree --locked --list >/dev/null + typos: name: typos runs-on: ubuntu-latest diff --git a/PLANS.md b/PLANS.md index 492b764..7699d09 100644 --- a/PLANS.md +++ b/PLANS.md @@ -1,40 +1,73 @@ -# Addressable tree runtime consumer slice +# Addressable 0.1 release evidence ## Goal -Turn the first real Exedra consumer into subtraction by moving reusable rooted -tree resolution and query execution into a small `no_std + alloc` -`addressable_tree` crate. Exedra should retain its storage and domain policies -while deleting its custom path type, recursive path lookup, and consumer-local -assembly-query executor. Preserve the revision clock across extraction, and -make in-place commits the normal mutation path. +Prepare `addressable` and `addressable_tree` for a first `0.1.0` release only +after their public contracts have survived independent consumer use. Bring +forward API corrections that concrete consumers expose, then package the two +reusable crates without publishing, tagging, or landing consumer branches. + +## Fence + +Addressable owns durable addressing and shared interaction semantics; it +explicitly does not own consumer storage, UI state, presence reduction, +collaboration algorithms, or composition policy. ## Non-goals -- A compulsory storage engine or index. -- Generic endpoint value or mutation traits before a second domain proves them. -- Async execution, persistence, or a textual query language. +- Publishing crates, creating a release, or merging consumer work. +- Generalizing the reference-specific tooling adapter without a second real + schema-backed adapter. +- Adding a textual query language, universal evaluator, collaboration model, + or speculative revision branches. +- Counting an adapter as evidence merely because its types compile. + +## Evidence sequence + +1. Audit the two proposed release crates for public invariants, documentation, + package contents, and commitments that known near-term work would overturn. +2. Adapt Overstory's retained inspection tree to `addressable_tree`. Require the + integration to replace traversal or selection glue and demonstrate rich + resolution, pins, or budgeted queries against real snapshots. +3. Adapt a Layerstack composed stage without moving interned path, storage, or + composition ownership. Require it to expose a genuine tree-host seam and to + preserve Layerstack's path semantics. +4. Explore a Ruthere presence facet that carries current Addressable focus. Keep + presence and collaboration separate, and retain this consumer only if it + exercises locator or pin semantics rather than adding decorative wrapping. +5. Fold only consumer-earned corrections into Addressable. Record meaningful + public semantic changes in an ADR and migration note. +6. Prepare release notes and package metadata for `addressable` and + `addressable_tree`; keep the reference, tooling, and tour packages + unpublished. +7. Run package inspection plus the full workspace gates, review the resulting + API and consumer diffs, and open draft or review-ready PRs without merging. -## Steps +## Evidence so far -1. Define a host-owned node projection trait and reusable tree runtime. -2. Prove exact, relative, pinned, cardinality, budget, deduplication, handle, - suspend/resume, and in-place commit behavior in tests and rustdoc. -3. Make the Basilica assembly projection a second `TreeHost`; retain its - dependency-specific evaluator only for graph and cross-view axes. -4. Replace Exedra's `InstancePath` machinery with structured Addressable exact - addresses and a host-owned index. -5. Implement the small tree projection in `exedra_assembly`, retain its material - explanation/edit policy, and remove Basilica-specific resolution/selection - helpers. -6. Validate and submit the Addressable and Exedra changes as separate PRs. +- Overstory can borrow a retained inspection snapshot at its existing revision + and resolve, query, budget, pin, and recover generational handles. The adapter + is additive and does not replace Overstory's topology-aware live outline + patch, so it is evidence for the core API rather than a consumer PR yet. +- Layerstack can borrow a composed `Stage` without leaking `PathId` into durable + identity. It exposed the need for host-reported predicate work: composed + field matching now charges one unit to find the field stack plus one per + opinion. +- Ruthere can carry a typed pin as an application-owned presence facet through + its real visibility, replacement, cursor, and expiry behavior. It exposed + unsafe manual pin construction and the need to state that current `SpaceId` + text is runtime-scoped. Addressable does not absorb Ruthere presence or any + collaboration algorithm. ## Risks -- A trait shaped too narrowly around Exedra. Prove the assembly seam against - both Exedra and the Basilica reference domain while leaving Basilica's graph - axes domain-owned. -- Runtime mutation bypassing revisions. Expose immutable host access, preserve - the clock on extraction, and provide an in-place commit that advances once. -- Treating exact address text as domain storage. Hosts store structured - `AbsoluteAddress` values; string forms remain serialization only. +- A UI-tree or USD-tree adapter may accidentally make Addressable own labels, + storage, or domain traversal policy. Keep those decisions in the host. +- Runtime `SpaceId` values may be mistaken for durable cross-process space + names when locators enter presence or tooling payloads. Either make that + lifetime explicit or add a consumer-earned durable envelope before release. +- Existing reference and tooling crates demonstrate breadth but are not yet + reusable production boundaries. Do not publish them or describe their + reference-specific schemas as a stable generic protocol. +- Consumer branches can become dependency tangles. Use adapters and examples, + preserve one-way dependencies, and avoid cross-consumer coupling. diff --git a/STATUS.md b/STATUS.md index c2d2b1d..2f6145f 100644 --- a/STATUS.md +++ b/STATUS.md @@ -3,7 +3,9 @@ ## Current state The complete initial vertical slice landed on `main` on 2026-08-24. No crate -has been tagged or published. +has been tagged or published. `addressable` and `addressable_tree` are now the +proposed first `0.1.0` release crates; the reference, tooling, and tour packages +remain internal proofs. The workspace contains five packages: @@ -29,8 +31,10 @@ The reference slice exercises every lifecycle item required by the initial architecture: 1. One arch referent has distinct north and south assembly occurrences. -2. Exact, relative, and pinned locators have canonical round-trip documents and - resolve with rich outcomes; pinned rebinding has a regression test. +2. Exact and relative locators have delimiter-safe runtime-scoped textual + round trips. Pins are constructed from one resolved location, reject + cross-space text, and resolve with rich outcomes; pinned rebinding has a + regression test. 3. Typed queries cross explicitly between assembly and dependency views. Query cardinality is restricted to the sealed `One`, `Optional`, and `Many` markers. Ordering, deduplication, cycle policy, and four work budgets are @@ -44,7 +48,8 @@ architecture: revision, value, and capability preconditions, and return undo information. 7. Query deltas are replayed and compared with full recomputation. Replay rejects another space, live-query stream, or cross-space transition without - partial effect. + partial effect. Revisions cannot move backward or carry changes without + advancing; an empty same-revision poll remains valid. 8. One arch referent maps to two independently addressable catalog results while retaining correspondence evidence. 9. The dynamic adapter declares its view/facet/value schema, reconstructs typed @@ -81,8 +86,9 @@ The contracts are real; the first execution is intentionally modest: - `addressable_tree` deliberately covers rooted canonical-address trees only; other relationship views keep specialized evaluators; - tree hosts yield projected nodes lazily, can index referent occurrences, and - use ordered sets for cycle detection and deduplication; -- runtime extraction preserves the revision needed by `resume`, while + use ordered sets for cycle detection and deduplication; predicate matching + reports host-defined work charged against the query budget; +- runtime extraction preserves the revision needed by `from_revision`, while validated in-place commits advance the clock without cloning a whole host. These are replaceable host choices, not placeholders in the core semantic @@ -106,22 +112,29 @@ cargo +1.88 check -p addressable -p addressable_tree --locked --target x86_64-un cargo run -p addressable_tour --locked ``` -Results: 26 unit tests and 16 doctests pass; strict Clippy and warning-denied -rustdoc pass; native stable, Rust 1.88, bare-metal `no_std`, and WebAssembly -core checks pass; repository formatting, typo, SPDX-header, and whitespace -checks pass. - -## Repository decisions retained by the owner - -All packages remain `publish = false`. No merge, release, publication, or -sibling-repository edit was performed. The repository includes the standard -forest-rs Apache-2.0 and MIT license texts matching its workspace metadata. +Results: 37 unit tests and 18 doctests or compile-fail laws pass; strict Clippy +and warning-denied rustdoc pass; native stable, Rust 1.88, bare-metal `no_std`, +and WebAssembly core checks pass; repository formatting, typo, SPDX-header, and +whitespace checks pass. `addressable` verifies from its packaged archive; both +proposed release crates produce registry-normalized archives containing their +README, changelog, and Apache-2.0/MIT license texts. ## Next architectural evidence -The Exedra consumer exposed a genuine shared host seam, and the Basilica -assembly view now proves it against a second storage model. An outline/tree UI -is a credible next consumer: occurrence-aware rows, stable addresses, ordering, -budgets, and multiple views already fit. Pull live row deltas, lazy ranges, or -other execution machinery forward only when that consumer supplies concrete -behavior and deletion. +The Exedra consumer exposed a genuine shared host seam, and Basilica proves it +against a second storage model. Three isolated consumer experiments sharpened +the release boundary further: + +- an Overstory inspection snapshot borrows into the tree runtime at its own + revision, but its topology-aware live outline patch should not be replaced by + the flat generic delta; +- a Layerstack composed stage keeps storage, path interning, and composition + policy while reporting real opinion-resolution work to query budgets; +- a Ruthere presence facet carries a typed focus pin through visibility, + replacement, cursors, and expiry without making Addressable own presence or + collaboration. + +These are evidence branches, not automatic integration candidates: the +Overstory and Layerstack adapters currently add inspection capability without +deleting consumer code. Durable cross-process space naming remains deliberately +unclaimed until a transport consumer can prove its envelope. diff --git a/crates/addressable/CHANGELOG.md b/crates/addressable/CHANGELOG.md new file mode 100644 index 0000000..c7b143f --- /dev/null +++ b/crates/addressable/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## 0.1.0 - 2026-08-27 + +Initial release of the typed `no_std + alloc` vocabulary: + +- structured absolute and relative addresses, locators, locations, pins, and + runtime-scoped revisions; +- explicit query cardinality, ordering, deduplication, cycle policy, and + traversal budgets; +- exhaustive rich resolution outcomes and revision-scoped resolved handles; +- typed endpoints, explanations, guards, transactions, replayable live-query + deltas, and evidence-preserving correspondence; +- atomic delta replay rejects cross-space transitions, revision regression, and + unclocked changes while preserving empty same-revision polls. diff --git a/crates/addressable/Cargo.toml b/crates/addressable/Cargo.toml index 5b29ea0..ea0f6f4 100644 --- a/crates/addressable/Cargo.toml +++ b/crates/addressable/Cargo.toml @@ -6,9 +6,14 @@ rust-version.workspace = true license.workspace = true repository.workspace = true description = "Typed no_std vocabulary for addressable structured object spaces" +readme = "README.md" keywords = ["address", "graph", "no-std", "query"] categories = ["data-structures", "no-std"] -publish = false [lints] workspace = true + +[package.metadata.docs.rs] +all-features = true +default-target = "x86_64-unknown-linux-gnu" +targets = [] diff --git a/crates/addressable/LICENSE-APACHE b/crates/addressable/LICENSE-APACHE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/crates/addressable/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/addressable/LICENSE-MIT b/crates/addressable/LICENSE-MIT new file mode 100644 index 0000000..9cf1062 --- /dev/null +++ b/crates/addressable/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/addressable/README.md b/crates/addressable/README.md new file mode 100644 index 0000000..48ceec0 --- /dev/null +++ b/crates/addressable/README.md @@ -0,0 +1,74 @@ +# addressable + +`addressable` is the typed, `no_std + alloc` vocabulary for locating, +inspecting, watching, and safely editing values in structured object spaces. +It keeps six things deliberately distinct: + +- a semantic referent; +- one contextual occurrence of that referent; +- the exact address of that occurrence; +- the named view in which the address is meaningful; +- the runtime space instance and revision that were observed; +- a runtime-local handle used only as an accelerator. + +The crate owns no storage engine, graph evaluator, async runtime, or universal +value type. A domain host constructs contextual values and implements the +resolution, query, read, watch, or edit operations it supports. + +## From an observed location to a safe pin + +A host normally returns a `Location` from resolution or query execution. Turn +that one observation into an exact pinned reference with +`Pinned::from_location`: + +```rust +use addressable::{AbsoluteAddress, Location, Pinned, Revision, SpaceId}; + +enum DocumentSpace {} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum DocumentView { + Outline, +} + +let space = SpaceId::::new(1); +let location = Location::new( + DocumentView::Outline, + Revision::new(space, 4), + 42_u64, // referent + 7_u64, // occurrence + AbsoluteAddress::parse("/chapter/section")?, +); +let pinned = Pinned::from_location(&location); + +assert_eq!(pinned.expected_referent(), &42); +assert_eq!(pinned.expected_revision(), location.revision()); +# Ok::<(), addressable::AddressError>(()) +``` + +The host later resolves that pin to an exhaustive `Resolution`: resolved, +absent, ambiguous, stale, moved, rebound, unsupported, budget-limited, partial, +or capability-unavailable. It must never silently accept rebinding. + +`SpaceId`, `Revision`, and the textual forms of `Locator` and `Pinned` are +runtime-scoped. Persist or exchange them only when the host preserves the same +space-id assignment. An `AbsoluteAddress` can be durable when its domain makes +that guarantee. + +For reusable locator and query execution over a host-owned rooted tree, see +[`addressable_tree`](https://crates.io/crates/addressable_tree). + +## Minimum supported Rust version + +This crate has been verified to compile with Rust 1.88 and later. + +## License + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or + ) + +at your option. diff --git a/crates/addressable_tree/CHANGELOG.md b/crates/addressable_tree/CHANGELOG.md new file mode 100644 index 0000000..3f48ed1 --- /dev/null +++ b/crates/addressable_tree/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## 0.1.0 - 2026-08-27 + +Initial release of reusable `no_std + alloc` execution over host-owned rooted +trees and forests: + +- exact, relative, and pinned locator resolution; +- deterministic tree queries with explicit cardinality and budgets; +- host-reported predicate work accounting; +- indexed movement detection through overridable referent occurrence lookup; +- revision-safe host commits, replacement, extraction, and reconstruction; +- borrowed-host support through `TreeHost for &H`. diff --git a/crates/addressable_tree/Cargo.toml b/crates/addressable_tree/Cargo.toml index 64fc260..6595f6c 100644 --- a/crates/addressable_tree/Cargo.toml +++ b/crates/addressable_tree/Cargo.toml @@ -9,10 +9,14 @@ description = "Reusable no_std Addressable execution for host-owned rooted trees readme = "README.md" keywords = ["addressing", "tree", "query", "no_std"] categories = ["data-structures", "no-std"] -publish = false [lints] workspace = true [dependencies] addressable.workspace = true + +[package.metadata.docs.rs] +all-features = true +default-target = "x86_64-unknown-linux-gnu" +targets = [] diff --git a/crates/addressable_tree/LICENSE-APACHE b/crates/addressable_tree/LICENSE-APACHE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/crates/addressable_tree/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/addressable_tree/LICENSE-MIT b/crates/addressable_tree/LICENSE-MIT new file mode 100644 index 0000000..9cf1062 --- /dev/null +++ b/crates/addressable_tree/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/addressable_tree/README.md b/crates/addressable_tree/README.md index c7bc390..017aba9 100644 --- a/crates/addressable_tree/README.md +++ b/crates/addressable_tree/README.md @@ -1,14 +1,78 @@ # addressable_tree -`addressable_tree` executes Addressable locators and queries over a host-owned -rooted tree or forest. Hosts keep their storage, indexes, node values, domain -predicates, and runtime handles; the reusable runtime owns revision context, -rich resolution, tree traversal, cardinality, ordering, deduplication, cycle -policy, and budgets. - -Implement `TreeHost` for the domain storage type, construct `TreeRuntime` with a -host-assigned `SpaceId`, and use the runtime's `resolve`, `resolve_pinned`, -`query_many`, `query_one`, `query_optional`, and `resolved_handle` methods. - -The crate is always `no_std + alloc`, owns no storage engine, and depends only -on `addressable`. +`addressable_tree` executes Addressable locators and typed queries over a +host-owned rooted tree or forest. The crate is always `no_std + alloc` and +depends only on `addressable`. + +The host keeps its storage, indexes, node values, domain predicates, mutation +policy, and runtime handles. `TreeRuntime` owns the shared behavior: + +- exact, relative, and pinned resolution with exhaustive outcomes; +- children, descendants, and parent traversal; +- explicit cardinality, ordering, deduplication, cycle policy, and budgets; +- revision validation and revision-scoped resolved handles. + +## Host and runtime lifecycle + +Implement `TreeHost` for the domain storage type. Its methods project +`TreeNode` values lazily; they do not transfer storage ownership. Predicate +evaluation returns `PredicateMatch`, including the domain work charged against +the query budget. + +Bind a new host instance with `TreeRuntime::new`. A host snapshot that already +owns a revision uses `TreeRuntime::from_revision`. Apply validated in-place +mutations through `TreeRuntime::commit`, which advances the revision even if a +mutation unwinds after changing the host. `into_host` returns both the host and +the revision needed to reconstruct that same runtime identity. + +```rust +use addressable::{Query, Resolution, SpaceId}; +use addressable_tree::{TreeAxis, TreeRuntime}; +# use addressable::{AbsoluteAddress}; +# use addressable_tree::{PredicateMatch, TreeHost, TreeNode}; +# enum Space {} +# #[derive(Clone, Copy, PartialEq, Eq)] enum View { Outline } +# enum Predicate { Any } +# struct Host; +# impl TreeHost for Host { +# type Space = Space; +# type View = View; +# type Referent = u64; +# type Occurrence = u64; +# type Handle = u64; +# type Predicate = Predicate; +# fn supports_view(&self, view: &View) -> bool { *view == View::Outline } +# fn node_at(&self, _: &View, address: &AbsoluteAddress) -> Option> { +# (address.depth() == 0).then(|| TreeNode::new(1, 1, AbsoluteAddress::root(), Some(1))) +# } +# fn nodes(&self, view: &View) -> impl Iterator> { self.node_at(view, &AbsoluteAddress::root()).into_iter() } +# fn children(&self, _: &View, _: &u64) -> impl Iterator> { core::iter::empty() } +# fn parent(&self, _: &View, _: &u64) -> Option> { None } +# fn matches(&self, _: &TreeNode, _: &Predicate) -> PredicateMatch { PredicateMatch::new(true, 1) } +# } + +let runtime = TreeRuntime::new(SpaceId::::new(1), Host); +let root = runtime.root_locator(View::Outline); +assert!(matches!(runtime.resolve(&root), Resolution::Resolved(_))); + +let children = runtime.query_many( + &Query::many(root).traverse(TreeAxis::Children), +)?; +assert!(children.items().is_empty()); +# Ok::<(), addressable::QueryError>(()) +``` + +## Minimum supported Rust version + +This crate has been verified to compile with Rust 1.88 and later. + +## License + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or + ) + +at your option.