diff --git a/README.md b/README.md index 9896d0c..98842a3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ It preserves distinctions that string paths and runtime handles usually erase: - an occurrence is where that thing appears in a particular view; - an endpoint is a typed facet on a located owner; - a space-typed revision says which instance and state were resolved; -- a resolved handle is a runtime capability, never durable identity. +- a resolved handle is a revision-scoped runtime token, never durable identity. The same arch referent can therefore appear as north and south assembly occurrences without being duplicated. A caller can query both, deduplicate by @@ -77,6 +77,10 @@ let applied = space .transact(Transaction::apply(space.revision(), [edit])) .expect("edit applies"); let delta = watch.poll(&space).expect("watch advances"); + +assert_eq!(preview.changes().len(), 1); +assert_eq!(applied.changes().len(), 1); +assert!(!delta.changes().is_empty()); ``` The full tour also resolves exact, relative, and pinned locators; crosses @@ -88,6 +92,10 @@ guarded operation through the dynamic schema boundary: cargo run -p addressable_tour ``` +The executable presents those transitions as five named chapters, so it can be +read from top to bottom or run as a narrated overview. Focused rustdoc examples +on the workflow types use the same call paths and run as doctests. + ## Semantic contracts - Text is parsed into validated segmented addresses. It is not the in-memory diff --git a/STATUS.md b/STATUS.md index 44259df..38c1f46 100644 --- a/STATUS.md +++ b/STATUS.md @@ -2,11 +2,10 @@ ## Current state -The complete initial vertical slice was implemented on 2026-08-24 on branch -`codex/bootstrap-addressable`. The branch has been pushed for review; it has -not been merged, tagged, or published. +The complete initial vertical slice landed on `main` on 2026-08-24. No crate +has been tagged or published. -The workspace contains four underscore-named packages: +The workspace contains four packages: - `addressable`: dependency-free, always `no_std + alloc` semantic vocabulary; - `addressable_reference`: a `std` scanning basilica host and second catalog @@ -44,8 +43,10 @@ architecture: 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 - endpoints and guards, and delegates to the same transaction method. Typed and - dynamic operation equivalence is tested, including undo data. + endpoints and guards, and delegates to the same transaction method. A read + returns the space and revision needed to form its guard without reaching + around the adapter. Typed and dynamic operation equivalence is tested, + including undo data. The tour runs all nine points through public APIs: @@ -53,6 +54,11 @@ The tour runs all nine points through public APIs: cargo run -p addressable_tour --locked ``` +The public rustdoc now describes the lifecycle of caller-created and +host-produced types, links each pivotal result to its producing and consuming +operations, and reserves doctests for real workflows and static laws. The tour +presents the same lifecycle as five narrated chapters. + ## Deliberately simple execution The contracts are real; the first execution is intentionally modest: @@ -89,7 +95,7 @@ cargo +1.88 check -p addressable --locked --target x86_64-unknown-none cargo run -p addressable_tour --locked ``` -Results: 23 unit tests and 4 doctests pass; strict Clippy and warning-denied +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. diff --git a/crates/addressable/src/address.rs b/crates/addressable/src/address.rs index 29bf65e..613d9d3 100644 --- a/crates/addressable/src/address.rs +++ b/crates/addressable/src/address.rs @@ -15,6 +15,10 @@ use core::{ use crate::{Revision, SpaceId}; /// One validated address segment. +/// +/// [`AbsoluteAddress`] and [`RelativeAddress`] parsing create names for callers. +/// Hosts that already have structured segments can validate them with +/// [`Self::new`] and pass them to the corresponding `from_names` constructor. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Name(Box); @@ -64,6 +68,10 @@ pub enum NameError { } /// A normalized, structured absolute address in typed space `S`. +/// +/// 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). pub struct AbsoluteAddress { segments: Box<[Name]>, marker: PhantomData S>, @@ -226,6 +234,10 @@ impl fmt::Display for AbsoluteAddress { } /// A normalized structured address relative to an explicit base. +/// +/// Obtain one with [`Self::parse`] or [`AbsoluteAddress::relative_to`]. Resolve +/// it directly with [`AbsoluteAddress::join`] or preserve the recipe in +/// [`Locator::relative`]. pub struct RelativeAddress { upward: u32, segments: Box<[Name]>, @@ -393,6 +405,29 @@ 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`]. +/// +/// ``` +/// use addressable::{AbsoluteAddress, Locator, RelativeAddress, SpaceId}; +/// +/// enum Space {} +/// #[derive(Clone, Copy)] +/// enum View { Assembly } +/// +/// let space = SpaceId::::new(7); +/// let locator = Locator::relative( +/// space, +/// View::Assembly, +/// AbsoluteAddress::parse("/basilica/nave")?, +/// RelativeAddress::parse("../transept")?, +/// ); +/// assert_eq!(locator.to_absolute()?.to_string(), "/basilica/transept"); +/// # Ok::<(), addressable::AddressError>(()) +/// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Locator { space: SpaceId, @@ -517,6 +552,14 @@ 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 +/// [`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. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Pinned { locator: Locator, @@ -526,6 +569,10 @@ pub struct Pinned { impl Pinned { /// Pins a locator to identity observed at `expected_revision`. + /// + /// 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. #[must_use] pub const fn new( locator: Locator, diff --git a/crates/addressable/src/correspondence.rs b/crates/addressable/src/correspondence.rs index 9e962f2..1139694 100644 --- a/crates/addressable/src/correspondence.rs +++ b/crates/addressable/src/correspondence.rs @@ -6,6 +6,10 @@ use alloc::{boxed::Box, vec::Vec}; /// One correspondence target and the evidence for that mapping. +/// +/// Hosts construct these inside a [`Correspondence`]; callers inspect the +/// destination with [`Self::target`] and the mapping evidence with +/// [`Self::provenance`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct CorrespondenceTarget { target: T, @@ -13,7 +17,7 @@ pub struct CorrespondenceTarget { } impl CorrespondenceTarget { - /// Creates one evidence-bearing target. + /// Creates one evidence-bearing target on behalf of a mapping host. #[must_use] pub const fn new(target: T, provenance: P) -> Self { Self { target, provenance } @@ -33,6 +37,25 @@ impl CorrespondenceTarget { } /// A partial, possibly one-to-many mapping from one source value. +/// +/// A domain host normally produces a correspondence when mapping between two +/// object spaces. Callers must handle zero, one, or several targets and can +/// inspect the provenance attached to each target. [`Self::compose`] extends a +/// mapping without discarding evidence from either leg. +/// +/// ``` +/// use addressable::{Correspondence, CorrespondenceTarget}; +/// +/// let mapping = Correspondence::new( +/// "shared-arch", +/// [ +/// CorrespondenceTarget::new("north-result", "north occurrence"), +/// CorrespondenceTarget::new("south-result", "south occurrence"), +/// ], +/// ); +/// assert!(mapping.is_ambiguous()); +/// assert_eq!(mapping.targets()[0].provenance(), &"north occurrence"); +/// ``` #[derive(Clone, Debug, PartialEq, Eq)] pub struct Correspondence { source: F, @@ -40,7 +63,7 @@ pub struct Correspondence { } impl Correspondence { - /// Creates a correspondence, including an empty partial result. + /// Creates a correspondence on behalf of a host, including an empty result. #[must_use] pub fn new(source: F, targets: impl IntoIterator>) -> Self { Self { @@ -110,6 +133,10 @@ impl Correspondence { } /// Evidence retained from both legs of correspondence composition. +/// +/// [`Correspondence::compose`] produces this for each composed target. Callers +/// inspect [`Self::first`] and [`Self::second`] when auditing the complete +/// mapping route. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ComposedEvidence { first: A, diff --git a/crates/addressable/src/edit.rs b/crates/addressable/src/edit.rs index bd1290b..391f5b8 100644 --- a/crates/addressable/src/edit.rs +++ b/crates/addressable/src/edit.rs @@ -8,6 +8,16 @@ use alloc::vec::Vec; use crate::Revision; /// Preconditions required before applying an addressed mutation. +/// +/// A caller forms a guard after selecting and reading a target. All four fields +/// must describe that same observation: the resolved semantic referent, the +/// revision that governed selection and reading, the value or fingerprint then +/// read, and any capability required by the domain operation. +/// +/// A guard does not identify an endpoint or perform a mutation. A domain +/// operation pairs it with its endpoint and proposed value, a [`Transaction`] +/// carries one or more operations, and the receiving host validates every +/// precondition at submission. A guard is evidence, not a lock. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Guard { expected_referent: I, @@ -17,7 +27,10 @@ pub struct Guard { } impl Guard { - /// Creates an identity, revision, and value guard with no extra capability token. + /// Creates a guard for a domain that requires no separate capability token. + /// + /// This omits only the capability token; the host must still validate the + /// referent, revision, and observed value. #[must_use] pub const fn at( expected_referent: I, @@ -29,7 +42,7 @@ impl Guard { } impl Guard { - /// Creates a complete guarded-mutation precondition. + /// Creates a guarded-mutation precondition from one coherent observation. #[must_use] pub const fn new( expected_referent: I, @@ -71,6 +84,9 @@ impl Guard { } /// Whether a transaction is previewed or committed. +/// +/// Selected by [`Transaction::dry_run`] or [`Transaction::apply`] and returned +/// in domain transaction reports. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TransactionMode { /// Validate and report impact without changing state. @@ -80,6 +96,9 @@ pub enum TransactionMode { } /// Behavior when one operation in a transaction conflicts. +/// +/// Read this from [`Transaction::failure_policy`]. Addressable currently +/// exposes only all-or-nothing execution. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FailurePolicy { /// No operation is observable unless every operation validates. @@ -87,6 +106,15 @@ pub enum FailurePolicy { } /// A snapshot-scoped collection of typed operations. +/// +/// Build operations from guarded endpoints, then choose [`Self::dry_run`] or +/// [`Self::apply`] using the revision at which the target set was selected. +/// Operations should retain their own target and [`Guard`]; `O` remains typed +/// so each host can define the mutations it supports. +/// +/// Addressable packages the request but does not execute it. The receiving +/// host validates the transaction, enforces [`FailurePolicy::Atomic`], and +/// defines its typed success report and conflict errors. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Transaction { selection_revision: Revision, diff --git a/crates/addressable/src/explain.rs b/crates/addressable/src/explain.rs index 7a85fc4..8c3069b 100644 --- a/crates/addressable/src/explain.rs +++ b/crates/addressable/src/explain.rs @@ -6,6 +6,9 @@ use alloc::{boxed::Box, vec::Vec}; /// One typed candidate value and its domain-owned provenance. +/// +/// Hosts construct opinions inside an [`Explained`] result. Callers normally +/// inspect the complete ordered slice through [`Explained::opinions`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Opinion { value: T, @@ -13,7 +16,7 @@ pub struct Opinion { } impl Opinion { - /// Creates one value opinion. + /// Creates one value opinion on behalf of an explaining host. #[must_use] pub const fn new(value: T, provenance: P) -> Self { Self { value, provenance } @@ -33,6 +36,12 @@ impl Opinion { } /// A winning typed value, alternatives, provenance, and domain-owned reason. +/// +/// A host produces this while reading or explaining a typed subject according +/// to domain policy. Callers use [`Self::value`] for the effective value and +/// inspect [`Self::opinions`] plus [`Self::reason`] when they need the evidence +/// behind it. The subject lets callers associate the explanation with durable +/// semantic identity instead of relying only on the route used to read it. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Explained { subject: S, @@ -42,7 +51,11 @@ pub struct Explained { } impl Explained { - /// Creates an explanation and validates its winner index. + /// Creates an explanation on behalf of a host and validates its winner index. + /// + /// `opinions` must be in the host's documented strength order. `winner` + /// identifies the effective opinion; Addressable does not choose it or + /// interpret the domain-owned reason and provenance values. pub fn new( subject: S, opinions: impl IntoIterator>, @@ -103,3 +116,41 @@ pub enum ExplainError { /// The winner index did not identify an opinion. WinnerOutOfBounds, } + +#[cfg(test)] +mod tests { + use super::{ExplainError, Explained, Opinion}; + + #[test] + fn explanation_requires_an_opinion() { + let result = Explained::<&str, i64, &str, &str>::new("arch", [], 0, "policy"); + + assert_eq!(result, Err(ExplainError::NoOpinions)); + } + + #[test] + fn explanation_requires_a_valid_winner() { + let result = Explained::new("arch", [Opinion::new(120_i64, "authored")], 1, "policy"); + + assert_eq!(result, Err(ExplainError::WinnerOutOfBounds)); + } + + #[test] + fn explanation_returns_the_selected_value_and_evidence() { + let explanation = Explained::new( + "arch", + [ + Opinion::new(40_i64, "default"), + Opinion::new(120_i64, "authored"), + ], + 1, + "authored overrides default", + ) + .expect("winner identifies an opinion"); + + assert_eq!(explanation.subject(), &"arch"); + assert_eq!(explanation.value(), &120); + assert_eq!(explanation.opinions()[1].provenance(), &"authored"); + assert_eq!(explanation.reason(), &"authored overrides default"); + } +} diff --git a/crates/addressable/src/identity.rs b/crates/addressable/src/identity.rs index 951df2c..7285453 100644 --- a/crates/addressable/src/identity.rs +++ b/crates/addressable/src/identity.rs @@ -16,7 +16,8 @@ use crate::AbsoluteAddress; /// /// The marker `S` prevents ids from unrelated domain types from being mixed. /// Values are assigned by the host; Addressable does not require a global id -/// generator or atomics. +/// generator or atomics. Hosts place the id in locators and revisions; callers +/// normally obtain it from a domain host rather than inventing it. pub struct SpaceId { raw: u64, marker: PhantomData S>, @@ -82,7 +83,9 @@ impl SpaceId { /// Monotonic revision scoped to one typed address-space instance. /// /// Keeping the [`SpaceId`] inside the value prevents equal numeric counters -/// from unrelated space instances from comparing as the same revision. +/// from unrelated space instances from comparing as the same revision. A host +/// creates and advances revisions, returns them in contextual results, and +/// validates them when callers submit pins, guards, transactions, or deltas. pub struct Revision { space: SpaceId, sequence: u64, @@ -172,6 +175,11 @@ impl Revision { /// `R` is durable referent identity and `O` is contextual occurrence identity. /// The two are intentionally stored separately even when a domain happens to /// use the same representation for both. +/// +/// A host normally produces a location while resolving a +/// [`Locator`](crate::Locator) or executing a [`Query`](crate::Query). Callers +/// inspect its context, retain its durable identities, or combine it with a +/// typed facet using [`Endpoint`]. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Location { view: V, @@ -182,7 +190,7 @@ pub struct Location { } impl Location { - /// Creates resolved occurrence context. + /// Creates resolved occurrence context on behalf of a domain host. #[must_use] pub const fn new( view: V, @@ -250,6 +258,11 @@ impl Location { } /// A resolved referent value paired with the context through which it was found. +/// +/// A host can use this as the return type of a read that yields the referent +/// value itself but must not discard the location, revision, view, or occurrence +/// through which it was obtained. Addressable does not produce this pair +/// automatically; a host chooses it when that return shape matches its API. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Located { referent: T, @@ -257,7 +270,7 @@ pub struct Located { } impl Located { - /// Pairs a referent value with its resolved location. + /// Pairs a referent value with its resolved location on behalf of a host. #[must_use] pub const fn new(referent: T, location: L) -> Self { Self { referent, location } @@ -283,6 +296,11 @@ impl Located { } /// A typed addressable facet on a located owner. +/// +/// Callers normally create an endpoint from a host-produced [`Location`] and a +/// domain-defined facet marker, then pass it to a matching host read, explain, +/// or edit API. The facet type prevents unrelated values from being read or +/// written through the same owner by accident. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Endpoint { owner: L, @@ -320,6 +338,11 @@ impl Endpoint { /// `H` may be an arena slot, generational handle, interned id, or another /// runtime accelerator. This wrapper carries context but intentionally has no /// textual serialization API. +/// +/// A domain may produce this from a validated [`Location`] and accept it in +/// host-specific fast paths. Callers must reacquire it after the host revision +/// changes. Addressable itself defines no operation on `H` and deliberately +/// provides no persistence or automatic freshness check for it. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ResolvedHandle { revision: Revision, @@ -327,7 +350,7 @@ pub struct ResolvedHandle { } impl ResolvedHandle { - /// Creates a revision-scoped resolved handle. + /// Wraps a host-local handle at the revision where the host resolved it. #[must_use] pub const fn new(revision: Revision, handle: H) -> Self { Self { revision, handle } diff --git a/crates/addressable/src/lib.rs b/crates/addressable/src/lib.rs index 28bcf8f..1948585 100644 --- a/crates/addressable/src/lib.rs +++ b/crates/addressable/src/lib.rs @@ -10,6 +10,28 @@ //! //! The crate is always `no_std` and uses `alloc` for owned structured values. //! +//! # Vocabulary lifecycle +//! +//! Addressable separates values a caller prepares from contextual values a +//! host produces: +//! +//! 1. A caller prepares a [`Locator`], [`Pinned`] locator, or [`Query`]. +//! 2. A host resolves or executes it, producing a [`Location`] or +//! [`QueryResults`]. +//! 3. A located owner and a domain facet form an [`Endpoint`]. A host can read +//! that endpoint as an [`Explained`] value or represent a revision-scoped +//! runtime accelerator with [`ResolvedHandle`]. +//! 4. A caller turns the identity, revision, and value it observed into a +//! [`Guard`], then submits typed operations in a [`Transaction`]. +//! 5. A live-query host produces [`QuerySnapshot`] and [`QueryDelta`] values; +//! consumers replay deltas with [`QuerySnapshot::apply`]. +//! +//! Constructors on contextual result types are public for host +//! implementations. Ordinary callers usually obtain those types from the +//! domain host rather than constructing them directly. +//! +//! # Structured addresses +//! //! ``` //! use addressable::{AbsoluteAddress, RelativeAddress}; //! diff --git a/crates/addressable/src/live.rs b/crates/addressable/src/live.rs index 2e98c54..d0ea707 100644 --- a/crates/addressable/src/live.rs +++ b/crates/addressable/src/live.rs @@ -17,6 +17,8 @@ use crate::Revision; /// /// The id is interpreted together with the space carried by a [`Revision`]. /// Addressable does not prescribe allocation or require atomics. +/// Live-query hosts create and retain it; consumers receive it through +/// [`QuerySnapshot::live_query`] and [`QueryDelta::live_query`]. pub struct LiveQueryId { raw: u64, marker: PhantomData S>, @@ -83,6 +85,9 @@ impl LiveQueryId { } /// Identity used to track live result entries. +/// +/// A live-query host records this in snapshots and deltas according to the +/// query's deduplication contract. Consumers inspect it before replay. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ResultIdentity { /// Contextual occurrence identity. @@ -94,6 +99,9 @@ pub enum ResultIdentity { } /// One stable live-query result entry. +/// +/// Live-query hosts construct entries; consumers use [`Self::key`] to track +/// stable identity and [`Self::value`] for the current located result. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResultEntry { key: K, @@ -101,7 +109,7 @@ pub struct ResultEntry { } impl ResultEntry { - /// Creates a keyed result entry. + /// Creates a keyed result entry on behalf of a live-query host. #[must_use] pub const fn new(key: K, value: T) -> Self { Self { key, value } @@ -121,6 +129,40 @@ impl ResultEntry { } /// A complete live-query result at one revision. +/// +/// A live-query host produces an initial snapshot and subsequent +/// [`QueryDelta`] values. Consumers retain the snapshot and call +/// [`Self::apply`] for each delta in order. Replay is atomic: an invalid delta +/// leaves the snapshot unchanged. +/// +/// ``` +/// use addressable::{ +/// LiveQueryId, QueryDelta, QuerySnapshot, ResultEntry, ResultIdentity, +/// Revision, SpaceId, +/// }; +/// +/// #[derive(Debug, PartialEq, Eq)] +/// enum Space {} +/// let space = SpaceId::::new(1); +/// let stream = LiveQueryId::new(9); +/// let mut current = QuerySnapshot::new( +/// stream, +/// Revision::new(space, 3), +/// ResultIdentity::Entry, +/// [ResultEntry::new("north", 120_i64)], +/// ); +/// let next = QuerySnapshot::new( +/// stream, +/// Revision::new(space, 4), +/// ResultIdentity::Entry, +/// [ResultEntry::new("north", 80_i64)], +/// ); +/// let delta = QueryDelta::between(¤t, &next)?; +/// current.apply(&delta)?; +/// +/// assert_eq!(current, next); +/// # Ok::<(), addressable::DeltaError>(()) +/// ``` #[derive(Clone, Debug, PartialEq, Eq)] pub struct QuerySnapshot { live_query: LiveQueryId, @@ -130,7 +172,7 @@ pub struct QuerySnapshot { } impl QuerySnapshot { - /// Creates a complete snapshot. + /// Creates a complete snapshot on behalf of a live-query host. #[must_use] pub fn new( live_query: LiveQueryId, @@ -258,6 +300,9 @@ where } /// One replayable structural change in a live query. +/// +/// Hosts emit these through [`QueryDelta::changes`]. Consumers can render the +/// individual events or replay the whole delta with [`QuerySnapshot::apply`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum QueryChange { /// Insert a new entry at an ordered index. @@ -304,6 +349,9 @@ pub enum QueryChange { } /// A coherent revision-to-revision live-query delta. +/// +/// Hosts emit deltas in revision order. Consumers inspect [`Self::changes`] or +/// replay the complete transition with [`QuerySnapshot::apply`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct QueryDelta { live_query: LiveQueryId, @@ -314,7 +362,7 @@ pub struct QueryDelta { } impl QueryDelta { - /// Creates a delta from already ordered structural changes. + /// Creates a delta from ordered structural changes on behalf of a host. #[must_use] pub fn new( live_query: LiveQueryId, @@ -469,6 +517,9 @@ fn ensure_unique(entries: &[ResultEntry]) -> Result<(), Delta } /// Failure to construct or atomically replay a live delta. +/// +/// Returned by [`QueryDelta::between`] and [`QuerySnapshot::apply`]. On replay +/// failure, the destination snapshot remains unchanged. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum DeltaError { /// Snapshot and delta belong to different live queries. diff --git a/crates/addressable/src/query.rs b/crates/addressable/src/query.rs index d9a09e8..d16ae9f 100644 --- a/crates/addressable/src/query.rs +++ b/crates/addressable/src/query.rs @@ -9,18 +9,30 @@ use core::marker::PhantomData; use crate::BudgetExceeded; /// Marker for a query that must return exactly one result. +/// +/// Select it with [`Query::one`] or [`Query::with_cardinality`], then call the +/// host's one-result execution method. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct One; /// Marker for a query that may return zero or one result. +/// +/// Select it with [`Query::optional`] or [`Query::with_cardinality`], then call +/// the host's optional-result execution method. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Optional; /// Marker for a query that may return several results. +/// +/// Select it with [`Query::many`] or [`Query::with_cardinality`], then call the +/// host's many-result execution or watch method. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Many; /// Runtime representation of the static cardinality marker. +/// +/// Returned by [`Query::cardinality`] and used by hosts when reporting a +/// [`QueryError::Cardinality`] mismatch. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CardinalityKind { /// Exactly one result is required. @@ -64,6 +76,9 @@ impl Cardinality for Many { } /// One host-defined query operation. +/// +/// Callers normally append these through [`Query::traverse`] and +/// [`Query::filter`]. Hosts inspect them through [`Query::steps`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum QueryStep { /// Traverse one typed domain axis. @@ -73,6 +88,8 @@ pub enum QueryStep { } /// Ordering promised by query execution. +/// +/// Pass this to [`Query::order`]; hosts must honor the selected contract. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ResultOrdering { /// Preserve deterministic traversal order. @@ -84,6 +101,9 @@ pub enum ResultOrdering { } /// Result deduplication identity. +/// +/// Pass this to [`Query::deduplicate`] to choose whether repeated occurrences +/// or referents remain visible. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Deduplication { /// Preserve every result entry, including repeats. @@ -95,6 +115,9 @@ pub enum Deduplication { } /// Identity used to detect revisitation during cyclic traversal. +/// +/// Use this inside [`CyclePolicy::SkipVisited`], then pass the policy to +/// [`Query::cycles`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum VisitIdentity { /// A distinct occurrence is a distinct visit. @@ -104,6 +127,9 @@ pub enum VisitIdentity { } /// Declared behavior when traversal encounters a cycle. +/// +/// Pass this to [`Query::cycles`]. A host returns [`QueryError::Cycle`] when +/// `Error` is selected and revisitation occurs. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CyclePolicy { /// Stop and return a cycle error. @@ -113,6 +139,9 @@ pub enum CyclePolicy { } /// Explicit upper bounds for query execution. +/// +/// Construct all four limits with [`Self::new`] and pass them to +/// [`Query::budget`]. Hosts report the first exceeded dimension. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TraversalBudget { /// Maximum axis-traversal depth. @@ -145,6 +174,9 @@ impl Default for TraversalBudget { } /// Shared policy that every query carries explicitly. +/// +/// Callers normally set individual fields through the fluent [`Query`] methods. +/// Hosts obtain the complete copy through [`Query::semantics`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct QuerySemantics { /// Promised result ordering. @@ -172,6 +204,36 @@ impl Default for QuerySemantics { /// /// `L`, `A`, and `P` are the host's locator, axis, and predicate types. `C` /// records result cardinality at the call site. +/// +/// Callers construct a query with [`Self::many`], [`Query::one`], or +/// [`Query::optional`], add domain axes and predicates, select explicit +/// semantics, and pass it to the matching host execution method. Addressable +/// builds and exposes this portable query representation; the host owns its +/// execution. +/// +/// ``` +/// use addressable::{ +/// CardinalityKind, CyclePolicy, Deduplication, Query, ResultOrdering, +/// VisitIdentity, +/// }; +/// +/// enum Axis { Descendants } +/// enum Predicate { IsArch } +/// let query = Query::many("/basilica") +/// .traverse(Axis::Descendants) +/// .filter(Predicate::IsArch) +/// .deduplicate(Deduplication::Occurrence) +/// .order(ResultOrdering::Stable) +/// .cycles(CyclePolicy::SkipVisited(VisitIdentity::Occurrence)); +/// +/// assert_eq!(query.cardinality(), CardinalityKind::Many); +/// assert_eq!(query.semantics().deduplication, Deduplication::Occurrence); +/// assert_eq!(query.semantics().ordering, ResultOrdering::Stable); +/// assert_eq!( +/// query.semantics().cycle_policy, +/// CyclePolicy::SkipVisited(VisitIdentity::Occurrence), +/// ); +/// ``` #[derive(Clone, Debug, PartialEq, Eq)] pub struct Query { start: L, @@ -303,7 +365,10 @@ impl Query { } } -/// Host-independent query failure. +/// Host-independent query failure returned by query execution. +/// +/// Callers can distinguish an unresolved start, cardinality mismatch, cycle, +/// budget exhaustion, and unsupported domain step without parsing diagnostics. #[derive(Clone, Debug, PartialEq, Eq)] pub enum QueryError { /// The start locator did not resolve to an ordinary result. @@ -324,6 +389,9 @@ pub enum QueryError { } /// Measured work performed by a query execution. +/// +/// Hosts return this inside [`QueryResults`] or another cardinality-shaped +/// result. Callers can use it for diagnostics and explicit budget tuning. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct QueryStats { /// Nodes inspected, including nodes rejected by predicates. @@ -335,6 +403,9 @@ pub struct QueryStats { } /// Query items paired with measured execution work. +/// +/// A host returns this from many-result query execution. Callers inspect +/// [`Self::items`] and can use [`Self::stats`] for budgeting or diagnostics. #[derive(Clone, Debug, PartialEq, Eq)] pub struct QueryResults { items: Box<[T]>, @@ -342,7 +413,7 @@ pub struct QueryResults { } impl QueryResults { - /// Creates a measured result collection. + /// Creates a measured result collection on behalf of a query host. #[must_use] pub fn new(items: impl IntoIterator, stats: QueryStats) -> Self { Self { diff --git a/crates/addressable/src/resolution.rs b/crates/addressable/src/resolution.rs index 6bac878..a98f02b 100644 --- a/crates/addressable/src/resolution.rs +++ b/crates/addressable/src/resolution.rs @@ -9,6 +9,11 @@ use crate::Revision; /// A resolution outcome that preserves absence, ambiguity, staleness, movement, /// and rebinding. +/// +/// Domain hosts return this from locator resolution. Callers should match the +/// rich variants when movement or rebinding needs different handling; +/// [`Self::resolved`] is for workflows where every exceptional outcome can be +/// collapsed to absence. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum Resolution { @@ -71,6 +76,9 @@ impl Resolution { } /// Why otherwise valid resolution is partial. +/// +/// Obtain this from [`Resolution::Partial`] and decide whether the accompanying +/// values are useful for the caller's task. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum PartialReason { @@ -83,6 +91,8 @@ pub enum PartialReason { } /// The budget dimension that stopped work. +/// +/// Returned inside [`BudgetExceeded`] by resolution or query execution. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BudgetDimension { /// Maximum traversal depth. @@ -96,6 +106,10 @@ pub enum BudgetDimension { } /// Evidence that one declared traversal budget was exceeded. +/// +/// Hosts construct this when a [`TraversalBudget`](crate::TraversalBudget) +/// limit is crossed; callers can inspect the dimension, declared limit, and +/// first value beyond it. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BudgetExceeded { dimension: BudgetDimension, diff --git a/crates/addressable_reference/src/catalog.rs b/crates/addressable_reference/src/catalog.rs index bf1bf21..364de48 100644 --- a/crates/addressable_reference/src/catalog.rs +++ b/crates/addressable_reference/src/catalog.rs @@ -60,16 +60,24 @@ impl CatalogOccurrenceId { } /// Resolved catalog result occurrence. +/// +/// Produced by [`Catalog::resolve`] or as a target of +/// [`Basilica::correspond_to_catalog`]. pub type CatalogLocation = Location; -/// View-qualified catalog locator. +/// View-qualified catalog locator accepted by [`Catalog::resolve`]. pub type CatalogLocator = Locator; -/// Rich catalog resolution outcome. +/// Rich catalog resolution outcome produced by [`Catalog::resolve`]. pub type CatalogResolution = Resolution>; /// Provenance for one basilica-to-catalog mapping. +/// +/// Obtain this from the targets returned by +/// [`Basilica::correspond_to_catalog`]. It records both the source basilica +/// instance and the particular assembly occurrence that produced a ranked +/// catalog result. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct CatalogEvidence { source_space: SpaceId, @@ -107,6 +115,10 @@ struct CatalogEntry { } /// Deterministic second address space containing ranked basilica results. +/// +/// Use [`Self::resolve`] for catalog-native addresses or +/// [`Basilica::correspond_to_catalog`] to map a basilica referent into ranked +/// catalog occurrences with evidence. #[derive(Clone, Debug)] pub struct Catalog { id: SpaceId, @@ -171,6 +183,27 @@ impl Catalog { impl Basilica { /// Maps one semantic basilica feature into zero or more catalog result occurrences. + /// + /// The result is deliberately zero-to-many. Inspect each target's + /// [`CatalogEvidence`] rather than assuming the first occurrence is unique. + /// + /// ``` + /// use addressable::SpaceId; + /// use addressable_reference::{ + /// Basilica, BasilicaSpace, Catalog, CatalogSpace, FeatureId, + /// }; + /// + /// let basilica = Basilica::new(SpaceId::::new(1)); + /// let catalog = Catalog::new(SpaceId::::new(2)); + /// let mapping = basilica.correspond_to_catalog(FeatureId::new(3), &catalog); + /// + /// assert!(mapping.is_ambiguous()); + /// assert_eq!(mapping.targets().len(), 2); + /// assert_ne!( + /// mapping.targets()[0].provenance().source_occurrence(), + /// mapping.targets()[1].provenance().source_occurrence(), + /// ); + /// ``` #[must_use] pub fn correspond_to_catalog( &self, diff --git a/crates/addressable_reference/src/lib.rs b/crates/addressable_reference/src/lib.rs index 4e60827..b0c1145 100644 --- a/crates/addressable_reference/src/lib.rs +++ b/crates/addressable_reference/src/lib.rs @@ -8,6 +8,12 @@ //! live deltas, guarded transactions, and correspondence into [`Catalog`]. //! It uses linear scans so the semantic contracts remain visible. //! +//! # Resolve and query +//! +//! [`Basilica::root_locator`] is the shortest entry point. Resolution produces +//! a [`BasilicaLocation`]; query cardinality selects [`Basilica::query_many`], +//! [`Basilica::query_one`], or [`Basilica::query_optional`]. +//! //! ``` //! use addressable::{CyclePolicy, Deduplication, Query, SpaceId, VisitIdentity}; //! use addressable_reference::{ @@ -24,6 +30,56 @@ //! assert_eq!(arches.items().len(), 2); //! # Ok::<(), addressable::QueryError>(()) //! ``` +//! +//! # Read, explain, edit, and watch +//! +//! A location becomes an endpoint when paired with [`Load`]. Read the endpoint +//! before constructing its [`Guard`](addressable::Guard); the observed referent, +//! revision, and value are the mutation preconditions. +//! +//! ``` +//! use addressable::{ +//! CyclePolicy, Deduplication, Endpoint, Guard, Query, SpaceId, Transaction, +//! VisitIdentity, +//! }; +//! use addressable_reference::{ +//! Basilica, BasilicaAxis, BasilicaPredicate, BasilicaSpace, EditCapability, +//! FeatureKind, Load, SetLoad, +//! }; +//! +//! let mut space = Basilica::new(SpaceId::::new(1)); +//! let query = Query::many(space.root_locator()) +//! .traverse(BasilicaAxis::Descendants) +//! .filter(BasilicaPredicate::Kind(FeatureKind::Arch)) +//! .deduplicate(Deduplication::Occurrence) +//! .cycles(CyclePolicy::SkipVisited(VisitIdentity::Occurrence)); +//! let mut watch = space.watch(query.clone()).expect("watch starts"); +//! let location = space +//! .query_many(&query) +//! .expect("query succeeds") +//! .items()[0] +//! .clone(); +//! let endpoint = Endpoint::new(location.clone(), Load); +//! let explained = space.read_load(&endpoint).expect("load reads"); +//! let edit = SetLoad::new( +//! endpoint, +//! 80, +//! Guard::new( +//! *location.referent(), +//! space.revision(), +//! *explained.value(), +//! EditCapability::SetLoad, +//! ), +//! ); +//! +//! let report = space +//! .transact(Transaction::apply(space.revision(), [edit])) +//! .expect("guarded transaction applies"); +//! let delta = watch.poll(&space).expect("watch advances"); +//! +//! assert_eq!(report.changes().len(), 1); +//! assert!(!delta.changes().is_empty()); +//! ``` mod catalog; mod model; diff --git a/crates/addressable_reference/src/model.rs b/crates/addressable_reference/src/model.rs index 9849cef..3d451ae 100644 --- a/crates/addressable_reference/src/model.rs +++ b/crates/addressable_reference/src/model.rs @@ -78,6 +78,11 @@ impl EdgeId { } /// Runtime-local dense feature slot. +/// +/// [`Basilica::resolved_handle`](crate::Basilica::resolved_handle) produces +/// this inside a revision-scoped [`ResolvedHandle`](addressable::ResolvedHandle). +/// The scanning reference exposes the boundary for inspection but has no +/// slot-based read operation. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SlotHandle(u32); @@ -175,23 +180,50 @@ pub enum BasilicaPredicate { } /// Resolved occurrence type for the basilica space. +/// +/// Normally produced by [`Basilica::resolve`](crate::Basilica::resolve) or the +/// `query_*` methods. Pair a location with [`Load`] to call +/// [`Basilica::read_load`](crate::Basilica::read_load), or pass it to +/// [`Basilica::resolved_handle`](crate::Basilica::resolved_handle) for a +/// revision-scoped runtime accelerator. pub type BasilicaLocation = Location; /// View-qualified locator type for the basilica space. +/// +/// Obtain the root from [`Basilica::root_locator`](crate::Basilica::root_locator) +/// or construct an exact or relative [`Locator`]. Pass it to +/// [`Basilica::resolve`](crate::Basilica::resolve), pin it, or use it as a +/// [`Query`] start. pub type BasilicaLocator = Locator; -/// Rich resolution outcome for a basilica occurrence. +/// Rich resolution outcome produced by basilica resolution. +/// +/// Match every outcome when resolving pins; [`Resolution::resolved`] is a +/// convenience only when all exceptional outcomes can be treated alike. pub type BasilicaResolution = Resolution>; /// Typed basilica query, defaulting to many-result cardinality. +/// +/// Construct it through [`Query::many`], [`Query::one`], or +/// [`Query::optional`], then execute it with the corresponding `Basilica` +/// method. Many-result queries can also be passed to +/// [`Basilica::watch`](crate::Basilica::watch). pub type BasilicaQuery = Query; /// Marker for the typed effective-load endpoint. +/// +/// Combine this marker with a [`BasilicaLocation`] using +/// [`Endpoint::new`](addressable::Endpoint::new). The resulting endpoint is +/// accepted by [`Basilica::read_load`](crate::Basilica::read_load) and +/// [`SetLoad::new`](crate::SetLoad::new). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub struct Load; /// Capability required to change a load endpoint. +/// +/// Place [`Self::SetLoad`] in the [`Guard`](addressable::Guard) carried by a +/// [`SetLoad`](crate::SetLoad) operation. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum EditCapability { /// Author an effective load opinion. @@ -199,6 +231,9 @@ pub enum EditCapability { } /// Domain-owned provenance for one load opinion. +/// +/// Obtain this through the opinions returned by +/// [`Basilica::read_load`](crate::Basilica::read_load). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum LoadProvenance { /// Explicitly authored load. @@ -214,6 +249,9 @@ pub enum LoadProvenance { } /// Domain-owned reason explaining the winning load opinion. +/// +/// Obtain this from the explanation returned by +/// [`Basilica::read_load`](crate::Basilica::read_load). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum LoadReason { /// The authored opinion has greater strength than the default. diff --git a/crates/addressable_reference/src/mutation.rs b/crates/addressable_reference/src/mutation.rs index 75848a4..77b3806 100644 --- a/crates/addressable_reference/src/mutation.rs +++ b/crates/addressable_reference/src/mutation.rs @@ -12,6 +12,48 @@ use crate::{ }; /// Typed operation that authors one effective load. +/// +/// Construct this from a [`BasilicaLocation`] paired with [`Load`], the proposed +/// value, and a guard derived from +/// [`Basilica::read_load`](crate::Basilica::read_load). Submit it inside an +/// [`Transaction`] to [`Basilica::transact`]. +/// +/// ``` +/// use addressable::{ +/// AbsoluteAddress, Endpoint, Guard, Locator, Resolution, SpaceId, Transaction, +/// }; +/// use addressable_reference::{ +/// Basilica, BasilicaSpace, BasilicaView, EditCapability, Load, SetLoad, +/// }; +/// +/// let mut space = Basilica::new(SpaceId::::new(1)); +/// let locator = Locator::exact( +/// space.id(), +/// BasilicaView::Assembly, +/// AbsoluteAddress::parse("/basilica/nave/north_arch").expect("valid address"), +/// ); +/// let Resolution::Resolved(arch) = space.resolve(&locator) else { +/// panic!("north arch must resolve"); +/// }; +/// let endpoint = Endpoint::new(arch.clone(), Load); +/// let explained = space.read_load(&endpoint).expect("load reads"); +/// let edit = SetLoad::new( +/// endpoint, +/// 50, +/// Guard::new( +/// *arch.referent(), +/// arch.revision(), +/// *explained.value(), +/// EditCapability::SetLoad, +/// ), +/// ); +/// let report = space +/// .transact(Transaction::apply(arch.revision(), [edit])) +/// .expect("guarded edit applies"); +/// +/// assert_eq!(report.changes()[0].current(), 50); +/// assert_eq!(space.revision(), report.revision_after()); +/// ``` #[derive(Clone, Debug, PartialEq, Eq)] pub struct SetLoad { endpoint: Endpoint, @@ -54,6 +96,9 @@ impl SetLoad { } /// One effective value change reported by a transaction. +/// +/// Obtain these from [`TransactionReport::changes`] after a successful dry run +/// or apply. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct LoadChange { referent: FeatureId, @@ -82,6 +127,10 @@ impl LoadChange { } /// Undo information for one applied authored opinion. +/// +/// Obtain these from [`TransactionReport::undo`]. A caller must still construct +/// and submit a fresh guarded transaction; this record is not an executable +/// command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct UndoLoad { referent: FeatureId, @@ -110,6 +159,10 @@ impl UndoLoad { } /// Successful dry-run or applied transaction report. +/// +/// Produced by [`Basilica::transact`]. Inspect [`Self::changes`] for effective +/// value changes and [`Self::undo`] for the authored state needed to build a +/// separately guarded undo operation. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TransactionReport { mode: TransactionMode, @@ -151,7 +204,10 @@ impl TransactionReport { } } -/// Atomic transaction conflict. No operation is observable when this is returned. +/// Atomic transaction conflict returned by [`Basilica::transact`]. +/// +/// No operation is observable when any variant is returned. The operation +/// index identifies the failing member of the caller-ordered transaction. #[derive(Clone, Debug, PartialEq, Eq)] pub enum TransactionConflict { /// The bulk selection snapshot is no longer current. @@ -218,6 +274,11 @@ struct PreparedLoad { impl Basilica { /// Atomically validates and previews or applies typed load operations. + /// + /// Every operation is validated before any operation becomes observable. + /// Build each guard from the value and revision actually read, rather than + /// from a locator alone. [`SetLoad`] shows the complete resolve, read, + /// guard, operation, and submission workflow. pub fn transact( &mut self, transaction: Transaction, diff --git a/crates/addressable_reference/src/space.rs b/crates/addressable_reference/src/space.rs index 748051f..f63e1ca 100644 --- a/crates/addressable_reference/src/space.rs +++ b/crates/addressable_reference/src/space.rs @@ -19,6 +19,11 @@ use crate::model::{ }; /// Measured single-value or optional query output. +/// +/// [`Basilica::query_one`] and [`Basilica::query_optional`] produce this shape +/// so their cardinality-specific value does not lose the common query work +/// measurements. Use [`Self::value`] for the result and [`Self::stats`] for +/// diagnostics or budget tuning. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Measured { value: T, @@ -26,7 +31,7 @@ pub struct Measured { } impl Measured { - /// Pairs a cardinality-shaped value with measured query work. + /// Pairs a cardinality-shaped value with measured query work for a host. #[must_use] pub const fn new(value: T, stats: QueryStats) -> Self { Self { value, stats } @@ -52,6 +57,10 @@ impl Measured { } /// The complete scanning reference basilica space. +/// +/// Start with [`Self::root_locator`], then resolve, query, watch, or form typed +/// endpoints. This host deliberately uses scanning implementations so the +/// semantics remain visible independently of indexing or storage choices. #[derive(Clone, Debug)] pub struct Basilica { pub(crate) id: SpaceId, @@ -156,6 +165,20 @@ impl Basilica { } /// Resolves an exact or relative locator with rich outcome semantics. + /// + /// The resolved [`BasilicaLocation`] can seed an [`Endpoint`] or be passed + /// to [`Self::resolved_handle`]. + /// + /// ``` + /// use addressable::{Resolution, SpaceId}; + /// use addressable_reference::{Basilica, BasilicaSpace}; + /// + /// let space = Basilica::new(SpaceId::::new(1)); + /// let Resolution::Resolved(root) = space.resolve(&space.root_locator()) else { + /// panic!("reference root must resolve"); + /// }; + /// assert_eq!(root.address().to_string(), "/basilica"); + /// ``` #[must_use] pub fn resolve(&self, locator: &BasilicaLocator) -> BasilicaResolution { if locator.space() != self.id { @@ -173,6 +196,29 @@ 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. + /// + /// ``` + /// use addressable::{Pinned, Resolution, SpaceId}; + /// use addressable_reference::{Basilica, BasilicaSpace}; + /// + /// let space = Basilica::new(SpaceId::::new(1)); + /// let locator = space.root_locator(); + /// let Resolution::Resolved(root) = space.resolve(&locator) else { + /// panic!("reference root must resolve"); + /// }; + /// let pinned = Pinned::new(locator, *root.referent(), root.revision()); + /// let Resolution::Resolved(root_again) = space.resolve_pinned(&pinned) else { + /// panic!("unchanged pin must resolve"); + /// }; + /// + /// assert_eq!(root_again.referent(), root.referent()); + /// assert_eq!(root_again.revision(), root.revision()); + /// ``` #[must_use] pub fn resolve_pinned( &self, @@ -227,7 +273,7 @@ impl Basilica { } } - /// Executes a many-result query. + /// Executes a many-result query, returning locations and measured work. pub fn query_many( &self, query: &BasilicaQuery, @@ -272,6 +318,12 @@ impl Basilica { } /// Resolves a revision-scoped runtime feature slot. + /// + /// This scanning reference exposes its [`SlotHandle`] to demonstrate the + /// boundary between durable identity and revision-scoped runtime identity. + /// It deliberately has no handle-based read path because scanning its small + /// vectors does not benefit from one. Production hosts may define such fast + /// paths; a caller must re-resolve the handle after the revision changes. pub fn resolved_handle( &self, location: &BasilicaLocation, @@ -287,6 +339,30 @@ impl Basilica { } /// Reads and explains the effective typed load endpoint. + /// + /// Use the returned value, its subject, and the space revision when forming + /// the [`Guard`](addressable::Guard) for a [`SetLoad`](crate::SetLoad) + /// operation. + /// + /// ``` + /// use addressable::{AbsoluteAddress, Endpoint, Locator, Resolution, SpaceId}; + /// use addressable_reference::{Basilica, BasilicaSpace, BasilicaView, Load}; + /// + /// let space = Basilica::new(SpaceId::::new(1)); + /// let locator = Locator::exact( + /// space.id(), + /// BasilicaView::Assembly, + /// AbsoluteAddress::parse("/basilica/nave/north_arch").expect("valid address"), + /// ); + /// let Resolution::Resolved(arch) = space.resolve(&locator) else { + /// panic!("north arch must resolve"); + /// }; + /// let explained = space + /// .read_load(&Endpoint::new(arch, Load)) + /// .expect("load reads"); + /// assert_eq!(explained.value(), &120); + /// assert!(!explained.opinions().is_empty()); + /// ``` pub fn read_load( &self, endpoint: &Endpoint, @@ -569,7 +645,10 @@ impl Basilica { } } -/// Failure to read through a resolved endpoint or handle. +/// Failure to validate a resolved location for an endpoint read or handle lookup. +/// +/// Returned by [`Basilica::read_load`] and [`Basilica::resolved_handle`], and +/// nested in transaction conflicts when an endpoint is no longer usable. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ReadError { /// The location belongs to another runtime space instance. diff --git a/crates/addressable_reference/src/watch.rs b/crates/addressable_reference/src/watch.rs index fbca95c..a44c378 100644 --- a/crates/addressable_reference/src/watch.rs +++ b/crates/addressable_reference/src/watch.rs @@ -11,6 +11,24 @@ use addressable::{ use crate::{Basilica, BasilicaLocation, BasilicaQuery, BasilicaSpace, OccurrenceId}; /// A live many-result query tracked by occurrence identity. +/// +/// Produced by [`Basilica::watch`]. Retain or clone [`Self::snapshot`], call +/// [`Self::poll`] after the host may have changed, and replay the returned delta +/// with [`QuerySnapshot::apply`](addressable::QuerySnapshot::apply). +/// +/// ``` +/// use addressable::{Deduplication, Query, SpaceId}; +/// use addressable_reference::{Basilica, BasilicaSpace}; +/// +/// let mut space = Basilica::new(SpaceId::::new(1)); +/// let query = Query::many(space.root_locator()) +/// .deduplicate(Deduplication::Occurrence); +/// let mut watch = space.watch(query).expect("watch starts"); +/// let mut replayed = watch.snapshot().clone(); +/// let delta = watch.poll(&space).expect("watch advances"); +/// replayed.apply(&delta).expect("delta replays"); +/// assert_eq!(&replayed, watch.snapshot()); +/// ``` #[derive(Clone, Debug)] pub struct BasilicaWatch { query: BasilicaQuery, @@ -45,7 +63,7 @@ impl BasilicaWatch { } impl Basilica { - /// Starts a scanning live query. + /// Starts a scanning live query and returns its initial snapshot. /// /// This first watcher deliberately accepts occurrence deduplication only, /// making its stable entry identity explicit. @@ -82,6 +100,9 @@ fn snapshot( } /// Failure to start or advance a basilica watch. +/// +/// Returned by [`Basilica::watch`] and [`BasilicaWatch::poll`]. The variants +/// distinguish query-shape limitations from recomputation and delta failures. #[derive(Clone, Debug, PartialEq, Eq)] pub enum WatchError { /// This watcher requires occurrence-deduplicated results. diff --git a/crates/addressable_tooling/src/lib.rs b/crates/addressable_tooling/src/lib.rs index d2716bb..a2eeeda 100644 --- a/crates/addressable_tooling/src/lib.rs +++ b/crates/addressable_tooling/src/lib.rs @@ -6,6 +6,17 @@ //! Erasure is deliberately confined to this crate. [`ReferenceTool`] validates //! schema names and dynamic value kinds, then reconstructs the same typed //! endpoint, guard, and transaction used by ordinary Rust callers. +//! +//! # Dynamic workflow +//! +//! 1. Inspect [`ReferenceTool::schema`] for accepted view, facet, value-kind, +//! and capability names. +//! 2. Construct a [`DynamicEndpoint`] and pass it to [`ReferenceTool::read`]. +//! 3. Form a [`DynamicGuard`] directly from the returned +//! [`DynamicExplanation`], then submit a [`DynamicTransaction`]. +//! +//! The complete example on [`ReferenceTool`] is a tooling-only call path: it +//! never reaches around the adapter to recover typed state. use std::{string::String, vec::Vec}; @@ -19,6 +30,9 @@ use addressable_reference::{ }; /// Dynamic value kind declared by a tooling schema. +/// +/// Read this from `FacetSchema::value_kind` or obtain it from +/// [`DynamicValue::kind`] before constructing a request. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum DynamicKind { /// Signed 64-bit integer. @@ -30,6 +44,8 @@ pub enum DynamicKind { /// Value crossing the schema-backed tooling boundary. /// /// This enum is not used by `addressable` or by typed reference storage. +/// Callers construct values according to `FacetSchema::value_kind`; read and +/// transaction reports return values in the same representation. #[derive(Clone, Debug, PartialEq, Eq)] pub enum DynamicValue { /// Signed 64-bit integer. @@ -61,6 +77,9 @@ pub enum ToolCapability { } /// Schema for one named address-space view. +/// +/// Returned as part of [`ReferenceTool::schema`]; tooling does not need to +/// construct this reference adapter's schema itself. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ViewSchema { /// Stable dynamic name. @@ -68,6 +87,9 @@ pub struct ViewSchema { } /// Schema for one addressable facet. +/// +/// Returned as part of [`ReferenceTool::schema`]. Its name is accepted by +/// `DynamicEndpoint::facet`, and its value kind governs reads and sets. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FacetSchema { /// Stable dynamic name. @@ -79,6 +101,9 @@ pub struct FacetSchema { } /// Declared dynamic schema for one typed object-space adapter. +/// +/// Obtain this from [`ReferenceTool::schema`] before constructing locators or +/// endpoint requests. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ObjectSpaceSchema { /// Stable schema identity. @@ -110,6 +135,10 @@ const BASILICA_SCHEMA: ObjectSpaceSchema = ObjectSpaceSchema { }; /// Erased but schema-qualified locator. +/// +/// Construct this from a runtime space id plus view and address syntax declared +/// by [`ObjectSpaceSchema`]. It becomes the owner of a [`DynamicEndpoint`] and +/// is validated by [`ReferenceTool::read`] or [`ReferenceTool::transact`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DynamicLocator { /// Runtime space id. @@ -121,6 +150,10 @@ pub struct DynamicLocator { } /// Erased typed-facet endpoint. +/// +/// Callers construct this from a [`DynamicLocator`] and a facet name from +/// `ObjectSpaceSchema::facets`. Pass it to [`ReferenceTool::read`] or include +/// it in a [`DynamicSet`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DynamicEndpoint { /// Located owner recipe. @@ -130,6 +163,10 @@ pub struct DynamicEndpoint { } /// Erased preconditions for one set operation. +/// +/// Copy the referent, space, revision, and value from the +/// [`DynamicExplanation`] returned by [`ReferenceTool::read`]. The adapter +/// validates all four values before delegating to the typed transaction path. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DynamicGuard { /// Expected durable semantic identity. @@ -143,6 +180,9 @@ pub struct DynamicGuard { } /// One erased guarded set operation. +/// +/// Combine a [`DynamicEndpoint`], a schema-compatible proposed value, and a +/// [`DynamicGuard`], then include it in `DynamicTransaction::operations`. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DynamicSet { /// Addressed facet. @@ -154,6 +194,9 @@ pub struct DynamicSet { } /// Snapshot-scoped dynamic transaction request. +/// +/// Use the space and revision from the same [`DynamicExplanation`] that supplied +/// each operation's guard. Submit the request to [`ReferenceTool::transact`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DynamicTransaction { /// Runtime space in which the target set was selected. @@ -167,6 +210,9 @@ pub struct DynamicTransaction { } /// One erased opinion in a structured explanation. +/// +/// Produced inside `DynamicExplanation::opinions` by +/// [`ReferenceTool::read`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DynamicOpinion { /// Typed value after erasure. @@ -178,10 +224,18 @@ pub struct DynamicOpinion { } /// Structured dynamic value explanation. +/// +/// Produced by [`ReferenceTool::read`]. The subject, space, revision, and value +/// are exactly the observation needed to construct a [`DynamicGuard`]; opinions +/// and reason explain how the effective value was selected. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DynamicExplanation { /// Durable semantic subject identity. pub subject: u64, + /// Runtime space in which the value was observed. + pub space: u64, + /// Space-local revision at which the value was observed. + pub revision: u64, /// Effective value. pub value: DynamicValue, /// Candidate opinions in typed domain strength order. @@ -192,7 +246,7 @@ pub struct DynamicExplanation { pub reason: &'static str, } -/// One effective dynamic change. +/// One effective dynamic change produced in a [`DynamicTransactionReport`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DynamicChange { /// Durable semantic subject identity. @@ -203,7 +257,10 @@ pub struct DynamicChange { pub current: i64, } -/// Dynamic transaction report produced from the typed report. +/// Dynamic transaction report produced by [`ReferenceTool::transact`]. +/// +/// Inspect [`Self::changes`] for effective changes and [`Self::undo`] for the +/// preconditions required by a future, separately guarded undo operation. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DynamicTransactionReport { /// Preview or apply mode. @@ -219,6 +276,9 @@ pub struct DynamicTransactionReport { } /// Dynamic form of one typed authored-load undo record. +/// +/// Returned in `DynamicTransactionReport::undo`; it is information for +/// constructing a future guarded request, not an immediately executable token. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DynamicUndo { /// Durable semantic subject identity. @@ -230,6 +290,49 @@ pub struct DynamicUndo { } /// Schema-backed adapter around one typed basilica host. +/// +/// This is the entry point for dynamic callers. Read an endpoint first, then +/// derive every transaction precondition from that observation. +/// +/// ``` +/// use addressable::{SpaceId, TransactionMode}; +/// use addressable_reference::{Basilica, BasilicaSpace}; +/// use addressable_tooling::{ +/// DynamicEndpoint, DynamicGuard, DynamicLocator, DynamicSet, +/// DynamicTransaction, DynamicValue, ReferenceTool, +/// }; +/// +/// let mut basilica = Basilica::new(SpaceId::::new(1)); +/// let mut tool = ReferenceTool::new(&mut basilica); +/// let endpoint = DynamicEndpoint { +/// owner: DynamicLocator { +/// space: 1, +/// view: "assembly".into(), +/// address: "/basilica/nave/north_arch".into(), +/// }, +/// facet: "load".into(), +/// }; +/// let observed = tool.read(&endpoint).expect("dynamic read succeeds"); +/// let report = tool +/// .transact(DynamicTransaction { +/// selection_space: observed.space, +/// selection_revision: observed.revision, +/// mode: TransactionMode::Apply, +/// operations: vec![DynamicSet { +/// endpoint, +/// value: DynamicValue::Integer(80), +/// guard: DynamicGuard { +/// expected_referent: observed.subject, +/// expected_space: observed.space, +/// expected_revision: observed.revision, +/// expected_value: observed.value, +/// }, +/// }], +/// }) +/// .expect("guarded dynamic transaction applies"); +/// +/// assert_eq!(report.changes[0].current, 80); +/// ``` #[derive(Debug)] pub struct ReferenceTool<'a> { space: &'a mut Basilica, @@ -249,12 +352,17 @@ impl<'a> ReferenceTool<'a> { } /// Reads and explains an endpoint after recovering its typed schema. + /// + /// The returned explanation carries every observation needed to form a + /// guarded set request against this value. pub fn read(&self, endpoint: &DynamicEndpoint) -> Result { let endpoint = self.typed_endpoint(endpoint)?; let explained = self.space.read_load(&endpoint).map_err(ToolError::Read)?; let opinions = explained.opinions().iter().map(dynamic_opinion).collect(); Ok(DynamicExplanation { subject: explained.subject().get(), + space: self.space.id().get(), + revision: self.space.revision().get(), value: DynamicValue::Integer(*explained.value()), opinions, winner: explained.winner(), @@ -268,6 +376,10 @@ impl<'a> ReferenceTool<'a> { } /// Validates and delegates a dynamic transaction to the typed host path. + /// + /// Use one prior [`Self::read`] result to populate the request's selection + /// context and each operation guard, as shown in the [`ReferenceTool`] + /// example. pub fn transact( &mut self, request: DynamicTransaction, @@ -387,7 +499,11 @@ fn dynamic_opinion(opinion: &Opinion) -> DynamicOpinion { } } -/// Failure at the schema-backed dynamic boundary. +/// Failure returned by [`ReferenceTool::read`] or [`ReferenceTool::transact`]. +/// +/// Schema and value errors are rejected before delegation. Typed read and +/// transaction failures remain distinguishable in [`Self::Read`] and +/// [`Self::Conflict`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum ToolError { /// The locator named another runtime space instance. @@ -474,24 +590,25 @@ mod tests { }, facet: "load".into(), }; - let dynamic_space_id = dynamic_space.id().get(); let mut tool = ReferenceTool::new(&mut dynamic_space); assert_eq!(tool.schema().facets[0].name, "load"); let before = tool.read(&dynamic_endpoint).expect("dynamic read succeeds"); assert_eq!(before.value, DynamicValue::Integer(120)); + assert_eq!(before.space, 1); + assert_eq!(before.revision, 0); let report = tool .transact(DynamicTransaction { - selection_space: dynamic_space_id, - selection_revision: 0, + selection_space: before.space, + selection_revision: before.revision, mode: TransactionMode::Apply, operations: vec![DynamicSet { endpoint: dynamic_endpoint.clone(), value: DynamicValue::Integer(80), guard: DynamicGuard { - expected_referent: 3, - expected_space: dynamic_space_id, - expected_revision: 0, - expected_value: DynamicValue::Integer(120), + expected_referent: before.subject, + expected_space: before.space, + expected_revision: before.revision, + expected_value: before.value, }, }], }) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 089ca4d..ad084f4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -198,6 +198,11 @@ The dynamic layer should be schema-backed and capable of recovering type and capability information. It must not force the typed core to store every value in one universal enum. +A dynamic read must return the observation context needed to guard a later +mutation, including runtime space and revision. Requiring a tooling caller to +reach through the adapter to typed host state would make the erased boundary +illusory and could pair a value with the wrong snapshot. + ## 10. Laws worth making executable The initial implementation should turn these into tests or conformance cases: diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 2aa1a26..a9cb8dd 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -57,3 +57,9 @@ second source makes disagreement between composition legs unrepresentable. `DynamicTransaction` adds `selection_space`, and `DynamicGuard` adds `expected_space`. Set both to the runtime space id from the dynamic locator. The tooling adapter validates them before reconstructing typed revisions. + +`DynamicExplanation` now carries the `space` and `revision` at which its value +was observed. Dynamic callers should copy those fields into +`DynamicTransaction::{selection_space, selection_revision}` and +`DynamicGuard::{expected_space, expected_revision}` instead of reaching through +the adapter to typed host state. diff --git a/examples/addressable_tour/src/main.rs b/examples/addressable_tour/src/main.rs index 17ac572..f147bfd 100644 --- a/examples/addressable_tour/src/main.rs +++ b/examples/addressable_tour/src/main.rs @@ -2,15 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT //! Executable complete vertical slice for Addressable. +//! +//! Each function is one chapter in the lifecycle. The assertions keep the tour +//! useful as conformance coverage, while the output explains the transitions a +//! caller would follow. use addressable::{ AbsoluteAddress, CyclePolicy, Deduplication, Endpoint, Guard, Locator, Pinned, Query, - Resolution, ResultOrdering, SpaceId, Transaction, TransactionMode, TraversalBudget, - VisitIdentity, + RelativeAddress, Resolution, ResultOrdering, SpaceId, Transaction, TransactionMode, + TraversalBudget, VisitIdentity, }; use addressable_reference::{ - Basilica, BasilicaAxis, BasilicaPredicate, BasilicaSpace, BasilicaView, Catalog, CatalogSpace, - EditCapability, FeatureKind, Load, SetLoad, + Basilica, BasilicaAxis, BasilicaLocator, BasilicaPredicate, BasilicaQuery, BasilicaSpace, + BasilicaView, Catalog, CatalogSpace, EditCapability, FeatureId, FeatureKind, Load, SetLoad, }; use addressable_tooling::{ DynamicEndpoint, DynamicGuard, DynamicLocator, DynamicSet, DynamicTransaction, DynamicValue, @@ -21,6 +25,21 @@ fn main() { let mut basilica = Basilica::new(SpaceId::::new(1)); let catalog = Catalog::new(SpaceId::::new(2)); + let arch = addresses_and_identity(&basilica); + let loaded_arches = queries_and_views(&basilica); + let (slot, applied_revision) = explain_edit_and_watch(&mut basilica, &loaded_arches); + let catalog_targets = correspondence(&basilica, &catalog, arch); + let dynamic_revision = dynamic_tooling(&mut basilica); + + println!( + "\nTour complete: referent {}, slot {slot}, typed revision {applied_revision}, catalog targets {catalog_targets}, dynamic revision {dynamic_revision}", + arch.get(), + ); +} + +fn addresses_and_identity(basilica: &Basilica) -> FeatureId { + println!("1. Resolve structured addresses and preserve identity"); + let north_locator = Locator::exact( basilica.id(), BasilicaView::Assembly, @@ -30,7 +49,7 @@ fn main() { basilica.id(), BasilicaView::Assembly, AbsoluteAddress::parse("/basilica/nave").expect("valid relative base"), - addressable::RelativeAddress::parse("south_arch").expect("valid relative path"), + RelativeAddress::parse("south_arch").expect("valid relative path"), ); let Resolution::Resolved(north) = basilica.resolve(&north_locator) else { panic!("north arch must resolve"); @@ -38,38 +57,55 @@ fn main() { let Resolution::Resolved(south) = basilica.resolve(&south_locator) else { panic!("south arch must resolve"); }; + let relative_document = south_locator.to_string(); let decoded_relative = relative_document - .parse::() + .parse::() .expect("canonical relative locator must parse"); assert_eq!( decoded_relative, south_locator, - "relative locator serialization must round-trip" + "relative locator serialization must round-trip", ); assert_eq!( north.referent(), south.referent(), - "shared arches must retain one semantic referent" + "shared arches must retain one semantic referent", ); assert_ne!( north.occurrence(), south.occurrence(), - "north and south appearances must remain distinct" + "north and south appearances must remain distinct", ); let pinned = Pinned::new(north_locator, *north.referent(), basilica.revision()); let pinned_document = pinned.to_string(); let decoded_pin = pinned_document - .parse::>() + .parse::>() .expect("canonical pin must parse"); assert_eq!( decoded_pin, pinned, - "pinned locator serialization must round-trip" + "pinned locator serialization must round-trip", ); assert!( matches!(basilica.resolve_pinned(&pinned), Resolution::Resolved(_)), - "an unchanged pin must resolve normally" + "an unchanged pin must resolve normally", + ); + let edge_count = basilica.edge_ids().len(); + assert!( + edge_count > 0, + "relationship occurrences must have their own identities", + ); + + println!( + " north and south share referent {} at revision {}; {edge_count} relationships retain separate identities", + north.referent().get(), + north.revision().get(), ); + *north.referent() +} + +fn queries_and_views(basilica: &Basilica) -> BasilicaQuery { + println!("2. Query with explicit cardinality, identity, order, cycles, and budget"); let loaded_arches = Query::many(basilica.root_locator()) .traverse(BasilicaAxis::Descendants) @@ -85,7 +121,7 @@ fn main() { assert_eq!( results.items().len(), 2, - "occurrence deduplication must preserve both arches" + "occurrence deduplication must preserve both arches", ); let dependency_query = Query::many(basilica.root_locator()) @@ -102,14 +138,25 @@ fn main() { .items() .len(), 3, - "dependency traversal must visit nave, arch, and vault exactly once" + "dependency traversal must visit nave, arch, and vault exactly once", + ); + + println!( + " found {} arch occurrences after visiting {} nodes", + results.items().len(), + results.stats().visited_nodes, ); + loaded_arches +} + +fn explain_edit_and_watch(basilica: &mut Basilica, query: &BasilicaQuery) -> (u32, u64) { + println!("3. Explain a typed endpoint, guard an edit, and replay its live delta"); let mut watch = basilica - .watch(loaded_arches.clone()) + .watch(query.clone()) .expect("occurrence watch starts"); let mut replayed = watch.snapshot().clone(); - let arch = results.items()[0].clone(); + let arch = basilica.query_many(query).expect("query succeeds").items()[0].clone(); let endpoint = Endpoint::new(arch.clone(), Load); let explained = basilica .read_load(&endpoint) @@ -120,7 +167,7 @@ fn main() { assert_eq!( *explained.value(), 120, - "the authored load must win over the default" + "the authored load must win over the default", ); let edit = SetLoad::new( @@ -139,7 +186,7 @@ fn main() { assert_eq!( preview.mode(), TransactionMode::DryRun, - "preview must not be reported as an apply" + "preview must not be reported as an apply", ); let applied = basilica .transact(Transaction::apply(basilica.revision(), [edit])) @@ -148,32 +195,53 @@ fn main() { assert_eq!( applied.undo().len(), 1, - "the applied change must carry undo information" + "the applied change must carry undo information", ); - let delta = watch.poll(&basilica).expect("watch advances coherently"); + let delta = watch.poll(basilica).expect("watch advances coherently"); replayed.apply(&delta).expect("delta replays"); assert_eq!( &replayed, watch.snapshot(), - "delta replay must equal full recomputation" + "delta replay must equal full recomputation", ); assert!( watch.snapshot().entries().is_empty(), - "both occurrences must leave the load-filtered query" + "both occurrences must leave the load-filtered query", ); - let correspondence = basilica.correspond_to_catalog(*arch.referent(), &catalog); + println!( + " load 120 → 80 at revision {}; replay removed both watched occurrences", + applied.revision_after().get(), + ); + (handle.handle().get(), applied.revision_after().get()) +} + +fn correspondence(basilica: &Basilica, catalog: &Catalog, arch: FeatureId) -> usize { + println!("4. Map a referent into another object space without losing evidence"); + + let correspondence = basilica.correspond_to_catalog(arch, catalog); assert!( correspondence.is_ambiguous(), - "one shared feature must map to two catalog results" + "one shared feature must map to two catalog results", ); assert_eq!( correspondence.targets().len(), 2, - "both result occurrences must retain correspondence evidence" + "both result occurrences must retain correspondence evidence", ); + println!( + " referent {} maps to {} catalog occurrences with separate provenance", + arch.get(), + correspondence.targets().len(), + ); + correspondence.targets().len() +} + +fn dynamic_tooling(basilica: &mut Basilica) -> u64 { + println!("5. Repeat the guarded edit through the schema-backed dynamic boundary"); + let dynamic_endpoint = DynamicEndpoint { owner: DynamicLocator { space: basilica.id().get(), @@ -182,56 +250,50 @@ fn main() { }, facet: "load".into(), }; - let dynamic_space = basilica.id().get(); - let current_revision = basilica.revision().get(); - let dynamic_report = { - let mut tool = ReferenceTool::new(&mut basilica); - assert_eq!( - tool.schema().name, - "addressable.reference.basilica/v1", - "dynamic calls must be governed by the declared schema" - ); - assert_eq!( - tool.read(&dynamic_endpoint) - .expect("dynamic explanation succeeds") - .value, - DynamicValue::Integer(80), - "dynamic reads must agree with the typed effective value" - ); - tool.transact(DynamicTransaction { - selection_space: dynamic_space, - selection_revision: current_revision, + let mut tool = ReferenceTool::new(basilica); + assert_eq!( + tool.schema().name, + "addressable.reference.basilica/v1", + "dynamic calls must be governed by the declared schema", + ); + let observed = tool + .read(&dynamic_endpoint) + .expect("dynamic explanation succeeds"); + assert_eq!( + observed.value, + DynamicValue::Integer(80), + "dynamic reads must agree with the typed effective value", + ); + let report = tool + .transact(DynamicTransaction { + selection_space: observed.space, + selection_revision: observed.revision, mode: TransactionMode::Apply, operations: vec![DynamicSet { endpoint: dynamic_endpoint, value: DynamicValue::Integer(120), guard: DynamicGuard { - expected_referent: arch.referent().get(), - expected_space: dynamic_space, - expected_revision: current_revision, - expected_value: DynamicValue::Integer(80), + expected_referent: observed.subject, + expected_space: observed.space, + expected_revision: observed.revision, + expected_value: observed.value, }, }], }) - .expect("dynamic operation delegates to typed transaction") - }; + .expect("dynamic operation delegates to typed transaction"); assert_eq!( - dynamic_report.changes[0].current, 120, - "dynamic set must delegate to the typed transaction" + report.changes[0].current, 120, + "dynamic set must delegate to the typed transaction", ); assert_eq!( - dynamic_report.undo.len(), + report.undo.len(), 1, - "dynamic callers must receive typed undo information" + "dynamic callers must receive typed undo information", ); println!( - "Addressable slice complete: referent {}, occurrences 2, edge ids {}, slot {}, revisions 0→{}, catalog targets {}, dynamic revision {}", - arch.referent().get(), - basilica.edge_ids().len(), - handle.handle().get(), - applied.revision_after().get(), - correspondence.targets().len(), - dynamic_report.revision_after, + " dynamic read supplied its own guard context; load restored to 120 at revision {}", + report.revision_after, ); + report.revision_after }