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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ jobs:
save-if: ${{ github.event_name != 'merge_group' }}

- name: check bare-metal core
run: cargo check -p addressable --locked --target x86_64-unknown-none
run: cargo check -p addressable -p addressable_tree --locked --target x86_64-unknown-none

- name: check WebAssembly core
run: cargo check -p addressable --locked --target wasm32-unknown-unknown
run: cargo check -p addressable -p addressable_tree --locked --target wasm32-unknown-unknown

test:
name: cargo test
Expand Down Expand Up @@ -136,7 +136,7 @@ jobs:
run: cargo check --workspace --all-targets --all-features --locked

- name: check bare-metal core
run: cargo check -p addressable --locked --target x86_64-unknown-none
run: cargo check -p addressable -p addressable_tree --locked --target x86_64-unknown-none

docs:
name: rustdoc
Expand Down
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"crates/addressable",
"crates/addressable_reference",
"crates/addressable_tooling",
"crates/addressable_tree",
"examples/addressable_tour",
]

Expand All @@ -18,6 +19,7 @@ repository = "https://github.com/forest-rs/addressable"
addressable = { path = "crates/addressable", version = "0.1.0" }
addressable_reference = { path = "crates/addressable_reference", version = "0.1.0" }
addressable_tooling = { path = "crates/addressable_tooling", version = "0.1.0" }
addressable_tree = { path = "crates/addressable_tree", version = "0.1.0" }

[workspace.lints]
# LINEBENDER LINT SET - Cargo.toml - v8
Expand Down
40 changes: 40 additions & 0 deletions PLANS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Addressable tree runtime consumer slice

## Goal

Turn the first real Exedra consumer into subtraction by moving reusable rooted
tree resolution and query execution into a small `no_std + alloc`
`addressable_tree` crate. Exedra should retain its storage and domain policies
while deleting its custom path type, recursive path lookup, and consumer-local
assembly-query executor. Preserve the revision clock across extraction, and
make in-place commits the normal mutation path.

## Non-goals

- A compulsory storage engine or index.
- Generic endpoint value or mutation traits before a second domain proves them.
- Async execution, persistence, or a textual query language.

## Steps

1. Define a host-owned node projection trait and reusable tree runtime.
2. Prove exact, relative, pinned, cardinality, budget, deduplication, handle,
suspend/resume, and in-place commit behavior in tests and rustdoc.
3. Make the Basilica assembly projection a second `TreeHost`; retain its
dependency-specific evaluator only for graph and cross-view axes.
4. Replace Exedra's `InstancePath` machinery with structured Addressable exact
addresses and a host-owned index.
5. Implement the small tree projection in `exedra_assembly`, retain its material
explanation/edit policy, and remove Basilica-specific resolution/selection
helpers.
6. Validate and submit the Addressable and Exedra changes as separate PRs.

## Risks

- A trait shaped too narrowly around Exedra. Prove the assembly seam against
both Exedra and the Basilica reference domain while leaving Basilica's graph
axes domain-owned.
- Runtime mutation bypassing revisions. Expose immutable host access, preserve
the clock on extraction, and provide an in-place commit that advances once.
- Treating exact address text as domain storage. Hosts store structured
`AbsoluteAddress` values; string forms remain serialization only.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ guarded edit, and observe a coherent live delta.
| Crate | Boundary |
|---|---|
| `addressable` | Dependency-free `no_std + alloc` vocabulary, structured addresses, query IR, live deltas, guards, and correspondence |
| `addressable_tree` | Reusable `no_std + alloc` resolution and query execution over host-owned rooted trees |
| `addressable_reference` | `std` scanning basilica and catalog spaces exercising the complete lifecycle |
| `addressable_tooling` | Schema-backed erased adapter that delegates to the typed reference API |
| `addressable_tour` | Separate executable proof; no example-only dependencies enter production crates |

Dependencies flow in one direction:

```text
addressable <- addressable_reference <- addressable_tooling <- addressable_tour
addressable <- addressable_tree <- addressable_reference <- addressable_tooling <- addressable_tour
```

## Typed use
Expand Down Expand Up @@ -120,7 +121,10 @@ path.
See [`MANDATE.md`](MANDATE.md) for the durable purpose,
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the mature design target,
[`docs/adr/0001-initial-workspace-and-vertical-slice.md`](docs/adr/0001-initial-workspace-and-vertical-slice.md)
for the initial crate decision, and [`STATUS.md`](STATUS.md) for current state.
for the initial crate decision,
[`docs/adr/0002-tree-runtime-from-exedra.md`](docs/adr/0002-tree-runtime-from-exedra.md)
for the consumer-derived tree runtime, and [`STATUS.md`](STATUS.md) for current
state.
See [`docs/MIGRATION.md`](docs/MIGRATION.md) when updating code written against
the earlier bootstrap draft.

Expand Down
37 changes: 24 additions & 13 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
The complete initial vertical slice landed on `main` on 2026-08-24. No crate
has been tagged or published.

The workspace contains four packages:
The workspace contains five packages:

- `addressable`: dependency-free, always `no_std + alloc` semantic vocabulary;
- `addressable_tree`: reusable `no_std + alloc` resolution and typed query
execution over host-owned rooted trees;
- `addressable_reference`: a `std` scanning basilica host and second catalog
space;
- `addressable_tooling`: schema-backed dynamic adaptation through typed host
Expand All @@ -16,6 +18,8 @@ The workspace contains four packages:

The crate decision, fences, invariants, and resolved bootstrap questions are in
[`docs/adr/0001-initial-workspace-and-vertical-slice.md`](docs/adr/0001-initial-workspace-and-vertical-slice.md).
The first consumer-derived execution boundary is in
[`docs/adr/0002-tree-runtime-from-exedra.md`](docs/adr/0002-tree-runtime-from-exedra.md).
The local forest-rs convention survey is in
[`docs/CONVENTIONS.md`](docs/CONVENTIONS.md).

Expand All @@ -30,7 +34,8 @@ architecture:
3. Typed queries cross explicitly between assembly and dependency views. Query
cardinality is restricted to the sealed `One`, `Optional`, and `Many`
markers. Ordering, deduplication, cycle policy, and four work budgets are
explicit. The dependency graph contains a real cycle.
explicit. Pure assembly queries execute through `addressable_tree`; the
dependency graph retains its custom evaluator and contains a real cycle.
4. A typed `Load` endpoint returns effective value, alternatives, provenance,
and a domain-owned winning reason.
5. A scanning watch maintains occurrence-identified query results under an
Expand Down Expand Up @@ -71,8 +76,14 @@ The contracts are real; the first execution is intentionally modest:
- the catalog correspondence is in-memory and deterministic;
- the tooling schema is reference-specific until a second real adapter proves a
generic protocol;
- there is no textual query language, async runtime, persistent journal,
production index, or consumer adapter yet.
- there is no textual query language, async runtime, persistent journal, or
production index;
- `addressable_tree` deliberately covers rooted canonical-address trees only;
other relationship views keep specialized evaluators;
- tree hosts yield projected nodes lazily, can index referent occurrences, and
use ordered sets for cycle detection and deduplication;
- runtime extraction preserves the revision needed by `resume`, while
validated in-place commits advance the clock without cloning a whole host.

These are replaceable host choices, not placeholders in the core semantic
types. No production or development dependencies were added.
Expand All @@ -88,10 +99,10 @@ cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --all-features --locked
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --locked --no-deps --document-private-items
cargo check -p addressable --locked --target x86_64-unknown-none
cargo check -p addressable --locked --target wasm32-unknown-unknown
cargo check -p addressable -p addressable_tree --locked --target x86_64-unknown-none
cargo check -p addressable -p addressable_tree --locked --target wasm32-unknown-unknown
cargo +1.88 check --workspace --all-targets --all-features --locked
cargo +1.88 check -p addressable --locked --target x86_64-unknown-none
cargo +1.88 check -p addressable -p addressable_tree --locked --target x86_64-unknown-none
cargo run -p addressable_tour --locked
```

Expand All @@ -108,9 +119,9 @@ forest-rs Apache-2.0 and MIT license texts matching its workspace metadata.

## Next architectural evidence

The next meaningful step is one real consumer adapter, selected by consumer
need rather than by expanding the generic core speculatively. A consumer should
reuse the existing durable vocabulary while supplying its own typed identities,
views, axes, predicates, endpoints, values, provenance, and evaluator. If that
adapter reveals a genuine shared host trait or dynamic schema protocol, record
the evidence in a new ADR before moving ownership between crates.
The Exedra consumer exposed a genuine shared host seam, and the Basilica
assembly view now proves it against a second storage model. An outline/tree UI
is a credible next consumer: occurrence-aware rows, stable addresses, ordering,
budgets, and multiple views already fit. Pull live row deltas, lazy ranges, or
other execution machinery forward only when that consumer supplies concrete
behavior and deletion.
2 changes: 1 addition & 1 deletion crates/addressable/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pub use live::{
DeltaError, LiveQueryId, QueryChange, QueryDelta, QuerySnapshot, ResultEntry, ResultIdentity,
};
pub use query::{
Cardinality, CardinalityKind, CyclePolicy, Deduplication, Many, One, Optional, Query,
Cardinality, CardinalityKind, CyclePolicy, Deduplication, Many, Measured, One, Optional, Query,
QueryError, QueryResults, QuerySemantics, QueryStats, QueryStep, ResultOrdering,
TraversalBudget, VisitIdentity,
};
Expand Down
18 changes: 0 additions & 18 deletions crates/addressable/src/live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,15 +281,6 @@ where
}
entries.insert(*to, entry);
}
QueryChange::Rebound { index, old, new } => {
let Some(existing) = entries.get_mut(*index) else {
return Err(DeltaError::IndexOutOfBounds { index: *index });
};
if existing != old || old.key != new.key {
return Err(DeltaError::EntryMismatch);
}
*existing = new.clone();
}
}
}

Expand Down Expand Up @@ -337,15 +328,6 @@ pub enum QueryChange<K, T> {
/// New index.
to: usize,
},
/// Keep result-entry identity while reporting a changed referent binding.
Rebound {
/// Stable index at this point in the delta stream.
index: usize,
/// Previous binding.
old: ResultEntry<K, T>,
/// New binding with the same result-entry key.
new: ResultEntry<K, T>,
},
}

/// A coherent revision-to-revision live-query delta.
Expand Down
118 changes: 117 additions & 1 deletion crates/addressable/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use alloc::{boxed::Box, vec::Vec};
use core::marker::PhantomData;

use crate::BudgetExceeded;
use crate::{BudgetDimension, BudgetExceeded};

/// Marker for a query that must return exactly one result.
///
Expand Down Expand Up @@ -402,6 +402,93 @@ pub struct QueryStats {
pub max_depth_reached: u32,
}

impl QueryStats {
/// Charges node visits, host work, and traversal depth against a budget.
///
/// Query evaluators call this as work becomes observable. The first
/// exceeded dimension is returned with its limit and observed value.
pub fn charge(
&mut self,
budget: TraversalBudget,
nodes: u32,
work: u32,
depth: u32,
) -> Result<(), QueryError> {
self.visited_nodes = self.visited_nodes.saturating_add(nodes);
self.work_units = self.work_units.saturating_add(work);
self.max_depth_reached = self.max_depth_reached.max(depth);
if depth > budget.max_depth {
return Err(QueryError::BudgetExceeded(BudgetExceeded::new(
BudgetDimension::Depth,
budget.max_depth,
depth,
)));
}
if self.visited_nodes > budget.max_nodes {
return Err(QueryError::BudgetExceeded(BudgetExceeded::new(
BudgetDimension::Nodes,
budget.max_nodes,
self.visited_nodes,
)));
}
if self.work_units > budget.max_work {
return Err(QueryError::BudgetExceeded(BudgetExceeded::new(
BudgetDimension::Work,
budget.max_work,
self.work_units,
)));
}
Ok(())
}

/// Charges only host-defined work at the current maximum depth.
pub fn charge_work(&mut self, budget: TraversalBudget, work: u32) -> Result<(), QueryError> {
self.charge(budget, 0, work, self.max_depth_reached)
}
}

/// A cardinality-shaped result paired with measured query work.
///
/// Hosts produce this from [`QueryResults::require_one`] or
/// [`QueryResults::require_optional`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Measured<T> {
value: T,
stats: QueryStats,
}

impl<T> Measured<T> {
/// Pairs a cardinality-shaped result with its measured query work.
#[must_use]
pub const fn new(value: T, stats: QueryStats) -> Self {
Self { value, stats }
}

/// Returns the cardinality-shaped value.
#[must_use]
pub const fn value(&self) -> &T {
&self.value
}

/// Returns measured query work.
#[must_use]
pub const fn stats(&self) -> QueryStats {
self.stats
}

/// Decomposes the measured result.
#[must_use]
pub fn into_parts(self) -> (T, QueryStats) {
(self.value, self.stats)
}

/// Consumes the measurement and returns only its value.
#[must_use]
pub fn into_value(self) -> T {
self.value
}
}

/// Query items paired with measured execution work.
///
/// A host returns this from many-result query execution. Callers inspect
Expand Down Expand Up @@ -439,6 +526,35 @@ impl<T> QueryResults<T> {
pub fn into_parts(self) -> (Box<[T]>, QueryStats) {
(self.items, self.stats)
}

/// Requires exactly one item while preserving measured work.
pub fn require_one(self) -> Result<Measured<T>, QueryError> {
let actual = self.items.len();
if actual != 1 {
return Err(QueryError::Cardinality {
expected: CardinalityKind::One,
actual,
});
}
let item = self
.items
.into_vec()
.pop()
.expect("cardinality was checked as exactly one");
Ok(Measured::new(item, self.stats))
}

/// Allows zero or one item while preserving measured work.
pub fn require_optional(self) -> Result<Measured<Option<T>>, QueryError> {
let actual = self.items.len();
if actual > 1 {
return Err(QueryError::Cardinality {
expected: CardinalityKind::Optional,
actual,
});
}
Ok(Measured::new(self.items.into_vec().pop(), self.stats))
}
}

#[cfg(test)]
Expand Down
Loading