Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
20 changes: 13 additions & 7 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,15 +43,22 @@ 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:

```sh
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:
Expand Down Expand Up @@ -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.
Expand Down
47 changes: 47 additions & 0 deletions crates/addressable/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<str>);

Expand Down Expand Up @@ -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<S> {
segments: Box<[Name]>,
marker: PhantomData<fn() -> S>,
Expand Down Expand Up @@ -226,6 +234,10 @@ impl<S> fmt::Display for AbsoluteAddress<S> {
}

/// 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<S> {
upward: u32,
segments: Box<[Name]>,
Expand Down Expand Up @@ -393,6 +405,29 @@ pub enum LocatorKind<S> {
}

/// 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::<Space>::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<S, V> {
space: SpaceId<S>,
Expand Down Expand Up @@ -517,6 +552,14 @@ pub enum LocatorParseError<E> {
}

/// 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<S, V, I> {
locator: Locator<S, V>,
Expand All @@ -526,6 +569,10 @@ pub struct Pinned<S, V, I> {

impl<S, V, I> Pinned<S, V, I> {
/// 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<S, V>,
Expand Down
31 changes: 29 additions & 2 deletions crates/addressable/src/correspondence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@
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<T, P> {
target: T,
provenance: P,
}

impl<T, P> CorrespondenceTarget<T, P> {
/// 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 }
Expand All @@ -33,14 +37,33 @@ impl<T, P> CorrespondenceTarget<T, P> {
}

/// 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<F, T, P> {
source: F,
targets: Box<[CorrespondenceTarget<T, P>]>,
}

impl<F, T, P> Correspondence<F, T, P> {
/// 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<Item = CorrespondenceTarget<T, P>>) -> Self {
Self {
Expand Down Expand Up @@ -110,6 +133,10 @@ impl<F, T, P> Correspondence<F, T, P> {
}

/// 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<A, B> {
first: A,
Expand Down
32 changes: 30 additions & 2 deletions crates/addressable/src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<S, I, V, C = ()> {
expected_referent: I,
Expand All @@ -17,7 +27,10 @@ pub struct Guard<S, I, V, C = ()> {
}

impl<S, I, V> Guard<S, I, V, ()> {
/// 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,
Expand All @@ -29,7 +42,7 @@ impl<S, I, V> Guard<S, I, V, ()> {
}

impl<S, I, V, C> Guard<S, I, V, C> {
/// Creates a complete guarded-mutation precondition.
/// Creates a guarded-mutation precondition from one coherent observation.
#[must_use]
pub const fn new(
expected_referent: I,
Expand Down Expand Up @@ -71,6 +84,9 @@ impl<S, I, V, C> Guard<S, I, V, C> {
}

/// 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.
Expand All @@ -80,13 +96,25 @@ 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.
Atomic,
}

/// 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<S, O> {
selection_revision: Revision<S>,
Expand Down
55 changes: 53 additions & 2 deletions crates/addressable/src/explain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
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<T, P> {
value: T,
provenance: P,
}

impl<T, P> Opinion<T, P> {
/// 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 }
Expand All @@ -33,6 +36,12 @@ impl<T, P> Opinion<T, P> {
}

/// 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<S, T, P, R> {
subject: S,
Expand All @@ -42,7 +51,11 @@ pub struct Explained<S, T, P, R> {
}

impl<S, T, P, R> Explained<S, T, P, R> {
/// 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<Item = Opinion<T, P>>,
Expand Down Expand Up @@ -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");
}
}
Loading