From 158692ec2f0e30f91f7784e637baee766cdb1275 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Tue, 25 Aug 2026 01:10:40 +0700 Subject: [PATCH 1/2] Make dynamic observations self-contained Dynamic guards require the space and revision that produced their observed value. Returning only the subject and value forced dynamic callers to reach through ReferenceTool into Basilica. Carry that context in DynamicExplanation so tooling can construct a guarded transaction using only the dynamic boundary. --- crates/addressable_tooling/src/lib.rs | 137 ++++++++++++++++++++++++-- docs/ARCHITECTURE.md | 5 + docs/MIGRATION.md | 6 ++ 3 files changed, 138 insertions(+), 10 deletions(-) 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. From 41ca04bd53aa3188cadc43eac5862d43861bce17 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Tue, 25 Aug 2026 01:10:51 +0700 Subject: [PATCH 2/2] Make rustdoc teach Addressable workflows Explain who constructs or receives each pivotal type, where its evidence comes from, and which operation consumes it. Reserve doctests for real workflows and static laws, and keep isolated constructor invariants in unit tests. Reshape the executable tour into five narrated chapters while retaining its semantic assertions. --- README.md | 10 +- STATUS.md | 20 +- crates/addressable/src/address.rs | 47 +++++ crates/addressable/src/correspondence.rs | 31 +++- crates/addressable/src/edit.rs | 32 +++- crates/addressable/src/explain.rs | 55 +++++- crates/addressable/src/identity.rs | 33 +++- crates/addressable/src/lib.rs | 22 +++ crates/addressable/src/live.rs | 57 +++++- crates/addressable/src/query.rs | 75 +++++++- crates/addressable/src/resolution.rs | 14 ++ crates/addressable_reference/src/catalog.rs | 37 +++- crates/addressable_reference/src/lib.rs | 56 ++++++ crates/addressable_reference/src/model.rs | 40 +++- crates/addressable_reference/src/mutation.rs | 63 ++++++- crates/addressable_reference/src/space.rs | 85 ++++++++- crates/addressable_reference/src/watch.rs | 23 ++- examples/addressable_tour/src/main.rs | 184 +++++++++++++------ 18 files changed, 791 insertions(+), 93 deletions(-) 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/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 }