From 816a7c05ae959e25cb0152950ca51bbb2503488a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Apr 2026 20:19:58 +0000 Subject: [PATCH 1/2] feat(archetype): scaffold lance-graph-archetype crate (DU-2.1..2.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the Archetype Transcode Crate to the workspace: ECS-style Component + Processor traits, World meta-state, CommandBroker deferred-mutation queue, and ArchetypeError — all Arrow-backed, inside-BBB. 12 unit tests pass. Workspace member entry added to root Cargo.toml. https://claude.ai/code/session_01SbYsmmbPf9YQuYbHZN52Zh --- .claude/plans/archetype-scaffold-v1.md | 90 ++++++++++++ Cargo.lock | 9 ++ Cargo.toml | 1 + crates/lance-graph-archetype/Cargo.toml | 19 +++ .../src/command_broker.rs | 99 +++++++++++++ crates/lance-graph-archetype/src/component.rs | 73 ++++++++++ crates/lance-graph-archetype/src/error.rs | 73 ++++++++++ crates/lance-graph-archetype/src/lib.rs | 42 ++++++ crates/lance-graph-archetype/src/processor.rs | 77 +++++++++++ crates/lance-graph-archetype/src/world.rs | 130 ++++++++++++++++++ 10 files changed, 613 insertions(+) create mode 100644 .claude/plans/archetype-scaffold-v1.md create mode 100644 crates/lance-graph-archetype/Cargo.toml create mode 100644 crates/lance-graph-archetype/src/command_broker.rs create mode 100644 crates/lance-graph-archetype/src/component.rs create mode 100644 crates/lance-graph-archetype/src/error.rs create mode 100644 crates/lance-graph-archetype/src/lib.rs create mode 100644 crates/lance-graph-archetype/src/processor.rs create mode 100644 crates/lance-graph-archetype/src/world.rs diff --git a/.claude/plans/archetype-scaffold-v1.md b/.claude/plans/archetype-scaffold-v1.md new file mode 100644 index 00000000..885b2c31 --- /dev/null +++ b/.claude/plans/archetype-scaffold-v1.md @@ -0,0 +1,90 @@ +# Archetype Transcode Crate Scaffold — v1 + +> **Status:** In progress (2026-04-24) +> **Owner:** @archetype-specialist, @truth-architect +> **Scope:** NEW crate `crates/lance-graph-archetype/`; deps only on `lance-graph-contract`, `arrow`, `lance` (peer-dep, optional). +> **Depends on:** ADR-0001 Decision 1 (transcode-not-bridge). No runtime dependency on upstream Python. + +## Goal + +Flip `lance-graph-archetype` from "does-not-exist" to "scaffolded-and-locked." Ship the 6 foundational trait/struct files per ADR-0001 Decision 1. No runtime behaviour yet — this is the LOCKED-MAPPING-INCOMPLETE → LOCKED-AND-SCAFFOLDED pivot. + +## Deliverables + +- **DU-2.1** — `crates/lance-graph-archetype/Cargo.toml` + `src/lib.rs` + workspace `members` entry in root `Cargo.toml`. +- **DU-2.2** — `src/component.rs: pub trait Component { fn arrow_field() -> arrow::datatypes::Field; fn type_id() -> &'static str; }` plus a test-only `MockComponent` impl asserting trait-object construction. +- **DU-2.3** — `src/processor.rs: pub trait Processor { fn matches(schema: &arrow::datatypes::Schema) -> bool; fn process(batch: arrow::record_batch::RecordBatch) -> Result; }`. +- **DU-2.4** — `src/world.rs: pub struct World { tick: u64, dataset_uri: String }` with `new() / tick() / current_tick() / fork(&self, branch: &str) / at_tick(&self, tick: u64)` methods. `fork()` and `at_tick()` return `Err(ArchetypeError::Unimplemented { method: "..." })` stubs — docstrings tie to ADR-0001:61-72 / 95. +- **DU-2.5** — `src/command_broker.rs: pub struct CommandBroker { queue: Vec, ... }` + `pub enum Command { Spawn, Despawn, Update }` — channel-based drain interface with `submit() / drain()` method stubs. +- **DU-2.6** — `src/error.rs: pub enum ArchetypeError { Unimplemented { method: &'static str }, SchemaMismatch { ... }, LanceIo(...) }` with `thiserror::Error` impl. + +## Non-goals (explicit) + +- Runtime World tick behaviour — stubs only. +- `AsyncProcessor` (Python async equivalent) — future follow-up. +- Entity=`PersonaCard` wiring — DU-2.7, later PR. +- Lance dataset integration beyond the `dataset_uri: String` placeholder — the `fork()` → `lance::checkout(branch)` wiring is DU-2.8. + +## Acceptance criteria + +- `cargo check -p lance-graph-archetype` compiles cleanly. +- `cargo test -p lance-graph-archetype` — minimum 4 tests pass (one per core trait + one per stub-returns-Unimplemented). +- `cargo test --workspace` — no regressions in other crates. +- Root `Cargo.toml` workspace.members updated. +- `STATUS_BOARD.md` DU-2 row status: Queued → In progress. +- Verdict flip in `.claude/plans/unified-integration-v1.md §6`: Archetype row `LOCKED-MAPPING-INCOMPLETE` → `LOCKED-AND-SCAFFOLDED`. +- `.claude/board/INTEGRATION_PLANS.md` — prepend entry pointing to this plan file. +- `.claude/board/LATEST_STATE.md § Contract Inventory` — add a new block for `lance-graph-archetype` naming the shipped types. +- `.claude/board/EPIPHANIES.md` — prepend short FINDING entry noting scaffold landed. + +## Architecture notes + +Per ADR-0001 Decision 1 (`.claude/adr/0001-archetype-transcode-stack.md:14-102`): this crate defines its OWN Rust interface. It does NOT mirror the Python `VangelisTech/archetype` API. The Python repo is a DESIGN SPEC, not a runtime dependency. "Upstream Python API unstable" is NOT a blocker. + +Per ADR-0001 Decision 3 (`adr/0001-archetype-transcode-stack.md:320-334`): BBB invariant bans `Vsa16kF32` / `RoleKey` / `NarsTruth` / `BlackboardEntry` from crossing the membrane. Archetype types defined in this crate are INSIDE-BBB; they do NOT appear on `CognitiveEventRow`. The scalar projection for "archetype tick happened" is already covered by `CognitiveEventRow.cycle_fp_hi/lo` + `MetaWord`. + +Mapping (locked, do not re-litigate): + +| ECS concept | lance-graph-contract type | This crate | +|---|---|---| +| Entity | `contract::persona::PersonaCard` | imported, not redefined | +| World | `contract::a2a_blackboard::Blackboard` (runtime) + `World { dataset_uri, tick }` (archetype meta) | the latter is new here | +| Tick | `contract::collapse_gate::GateDecision` fire | imported, not redefined | +| Component | trait in this crate | **DU-2.2** | +| Processor | trait in this crate | **DU-2.3** | +| CommandBroker | struct in this crate | **DU-2.5** | + +## File layout + +``` +crates/lance-graph-archetype/ + Cargo.toml + src/ + lib.rs # pub use component::*; etc. + component.rs # trait Component + processor.rs # trait Processor + world.rs # struct World + command_broker.rs # struct CommandBroker, enum Command + error.rs # enum ArchetypeError (thiserror) +``` + +## Test layout + +Each module gets a `#[cfg(test)] mod tests` with at minimum one test. Minimum 4 tests total: + +1. `component::tests::mock_component_has_arrow_field` +2. `processor::tests::trait_object_is_constructable` +3. `world::tests::fork_returns_unimplemented` +4. `world::tests::tick_increments` + +## Dependencies + +```toml +[dependencies] +lance-graph-contract = { path = "../lance-graph-contract" } +arrow = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +# nothing initially +``` diff --git a/Cargo.lock b/Cargo.lock index 343d007b..2fa94b32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4173,6 +4173,15 @@ dependencies = [ "url", ] +[[package]] +name = "lance-graph-archetype" +version = "0.1.0" +dependencies = [ + "arrow", + "lance-graph-contract", + "thiserror 2.0.17", +] + [[package]] name = "lance-graph-benches" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2c7fcf62..69a98356 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/lance-graph-contract", "crates/neural-debug", "crates/lance-graph-callcenter", + "crates/lance-graph-archetype", ] exclude = [ # Python bindings (upstream-inherited, opt-in via --manifest-path) diff --git a/crates/lance-graph-archetype/Cargo.toml b/crates/lance-graph-archetype/Cargo.toml new file mode 100644 index 00000000..19b51d5f --- /dev/null +++ b/crates/lance-graph-archetype/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "lance-graph-archetype" +version = "0.1.0" +edition = "2021" +description = "Archetype transcode scaffold (ECS-style types transcoded to Arrow/Lance). Per ADR-0001 Decision 1: defines its OWN Rust interface (not a mirror of upstream Python). Inside-BBB only — types in this crate never cross the CognitiveEventRow membrane." +license = "Apache-2.0" +keywords = ["lance", "graph", "archetype", "ecs", "transcode"] + +[dependencies] +lance-graph-contract = { path = "../lance-graph-contract" } +# NOTE: plan specified { workspace = true } for arrow/thiserror, but this +# workspace has no shared [workspace.dependencies] table today; using +# explicit versions consistent with the rest of the codebase (arrow 57, +# thiserror 2). See PR description for the single-line deviation. +arrow = "57" +thiserror = "2" + +[dev-dependencies] +# nothing initially diff --git a/crates/lance-graph-archetype/src/command_broker.rs b/crates/lance-graph-archetype/src/command_broker.rs new file mode 100644 index 00000000..99516ab8 --- /dev/null +++ b/crates/lance-graph-archetype/src/command_broker.rs @@ -0,0 +1,99 @@ +//! `CommandBroker` — a channel-based queue for deferred world mutations. +//! +//! Per ADR-0001 Decision 1, `CommandBroker` is the archetype-side +//! equivalent of Bevy's `Commands` / the Python ECS `CommandBroker`: +//! a FIFO queue of world mutations that accumulates during a +//! Processor pass and is drained by the World at tick boundaries. +//! +//! Stub-only at this stage — `submit` accepts commands, `drain` returns +//! what was submitted in order. Actual application-to-World logic lands +//! in DU-2.7 together with Entity wiring. + +/// A deferred world-mutation command. The three variants cover the +/// ECS-standard operations (spawn a new entity, despawn an existing +/// one, or update components on one). Payloads are opaque at the +/// scaffold stage; DU-2.7 will parameterise them over concrete +/// `Component` types. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Command { + /// Spawn a new entity. The `u64` is a placeholder for the + /// eventually-to-be-typed component bundle identifier. + Spawn(u64), + + /// Despawn an entity by its integer ID. + Despawn(u64), + + /// Update the entity identified by the first `u64` with the + /// component-bundle identifier in the second `u64`. + Update(u64, u64), +} + +/// FIFO queue of deferred commands. +/// +/// Used by Processors to schedule world mutations without mutating the +/// World mid-pass (which would break the Arrow-batch transcode model). +/// The World's tick driver calls `drain` at tick boundaries and applies +/// the commands in order. The scaffold uses a `Vec`; DU-2.7 +/// may upgrade to a `std::sync::mpsc::channel` for multi-processor +/// concurrency. +#[derive(Debug, Default, Clone)] +pub struct CommandBroker { + queue: Vec, +} + +impl CommandBroker { + /// Construct an empty broker. No allocation is performed until the + /// first `submit`. + pub fn new() -> Self { + Self { queue: Vec::new() } + } + + /// Enqueue a command. O(1) amortised. + pub fn submit(&mut self, cmd: Command) { + self.queue.push(cmd); + } + + /// Drain all queued commands in insertion order. Returns an owned + /// `Vec`; the broker is empty afterwards. O(n). + pub fn drain(&mut self) -> Vec { + std::mem::take(&mut self.queue) + } + + /// Read the current queue length without draining. + pub fn len(&self) -> usize { + self.queue.len() + } + + /// `true` iff no commands are queued. + pub fn is_empty(&self) -> bool { + self.queue.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_broker_is_empty() { + let b = CommandBroker::new(); + assert_eq!(b.len(), 0); + assert!(b.is_empty()); + } + + #[test] + fn submit_and_drain_preserves_order() { + let mut b = CommandBroker::new(); + b.submit(Command::Spawn(1)); + b.submit(Command::Update(1, 7)); + b.submit(Command::Despawn(1)); + assert_eq!(b.len(), 3); + + let drained = b.drain(); + assert_eq!( + drained, + vec![Command::Spawn(1), Command::Update(1, 7), Command::Despawn(1)] + ); + assert!(b.is_empty()); + } +} diff --git a/crates/lance-graph-archetype/src/component.rs b/crates/lance-graph-archetype/src/component.rs new file mode 100644 index 00000000..b111322a --- /dev/null +++ b/crates/lance-graph-archetype/src/component.rs @@ -0,0 +1,73 @@ +//! The `Component` trait — ECS-style component definition transcoded to Arrow. +//! +//! Per ADR-0001 Decision 1 (Archetype Transcode, not bridge): a `Component` +//! is a Rust-side type that declares how it projects into an Arrow +//! `Field`. The transcode surface is Arrow because every downstream +//! consumer of this crate lands in a Lance dataset; the `arrow_field` +//! method is what a `Processor` keys its `matches(schema)` check against. +//! +//! This trait deliberately stays Sized-agnostic at the scaffold stage — +//! only associated functions, no self-receiver. Implementors declare +//! static metadata (field shape, type ID) and the runtime machinery +//! lives elsewhere. + +use arrow::datatypes::Field; + +/// An ECS-style component that knows how to project itself into an Arrow +/// `Field`. Components do not carry row data at this stage — they declare +/// SHAPE. Row data flows through `RecordBatch`es handed to `Processor`. +/// +/// **BBB-invariant:** component types defined by implementors live +/// INSIDE-BBB. They do not cross the external membrane (see +/// `lance_graph_contract::external_membrane`). The scalar projection +/// "a component tick happened" is carried by `CognitiveEventRow`'s +/// existing columns (`cycle_fp_hi/lo`, `MetaWord`); this crate does +/// not extend that row. +pub trait Component { + /// Arrow field descriptor for this component. Called once at + /// `Processor::matches` time, not per-row. Implementors should + /// return a `Field` with a stable name and dtype. + fn arrow_field() -> Field; + + /// Stable string identifier for this component type. Used by the + /// `CommandBroker` drain path to address entities-by-component + /// without relying on Rust's `TypeId` (which is not stable across + /// builds). Convention: `"::"`. + fn type_id() -> &'static str; +} + +#[cfg(test)] +mod tests { + use super::*; + use arrow::datatypes::DataType; + + /// Test-only component used to assert that the trait is implementable + /// and that its metadata is reachable without constructing a value. + struct MockComponent; + + impl Component for MockComponent { + fn arrow_field() -> Field { + Field::new("mock_component", DataType::Int64, false) + } + + fn type_id() -> &'static str { + "lance_graph_archetype::tests::MockComponent" + } + } + + #[test] + fn mock_component_has_arrow_field() { + let field = MockComponent::arrow_field(); + assert_eq!(field.name(), "mock_component"); + assert_eq!(field.data_type(), &DataType::Int64); + assert!(!field.is_nullable()); + } + + #[test] + fn mock_component_type_id_is_stable() { + assert_eq!( + MockComponent::type_id(), + "lance_graph_archetype::tests::MockComponent" + ); + } +} diff --git a/crates/lance-graph-archetype/src/error.rs b/crates/lance-graph-archetype/src/error.rs new file mode 100644 index 00000000..815515ed --- /dev/null +++ b/crates/lance-graph-archetype/src/error.rs @@ -0,0 +1,73 @@ +//! Error type for the archetype transcode crate. +//! +//! Per ADR-0001 Decision 1, this crate defines its own error surface rather +//! than mirroring the Python `VangelisTech/archetype` exceptions. The +//! variants below are scoped to the scaffold (DU-2.1..2.6) — Lance I/O +//! wiring for `World::fork` / `World::at_tick` is deliberately parked +//! behind DU-2.8. + +use thiserror::Error; + +/// Top-level error type for archetype transcode operations. +/// +/// All fallible methods in this crate return `Result`. +/// The `Unimplemented` variant is used for stubs that will be wired in +/// follow-up deliverables; see `World::fork` / `World::at_tick` for the +/// canonical example. +#[derive(Debug, Error)] +pub enum ArchetypeError { + /// A stub method that has not yet been wired. The `method` field names + /// the specific method (for example, `"World::fork"`). Once the + /// corresponding deliverable (DU-2.7 / DU-2.8) lands, the variant + /// stays but the call site no longer returns it. + #[error("archetype method `{method}` is not yet implemented (scaffold stub)")] + Unimplemented { + /// Fully-qualified method name, for example `"World::fork"`. + method: &'static str, + }, + + /// A `Processor::process` invocation received a `RecordBatch` whose + /// schema does not match what the processor declared via `matches`. + /// The `expected` / `actual` fields are human-readable descriptions; + /// no Arrow schema equality is defined at the scaffold stage. + #[error("schema mismatch: expected {expected}, got {actual}")] + SchemaMismatch { + /// Human-readable description of the expected schema. + expected: String, + /// Human-readable description of the actual schema that arrived. + actual: String, + }, + + /// Placeholder for Lance dataset I/O errors. Once DU-2.8 wires + /// `lance::checkout(branch)` into `World::fork`, the inner type will + /// be upgraded from `String` to `lance::Error`. Today it carries a + /// bare message — no `lance` dependency on this PR per the plan's + /// Non-goals section. + #[error("lance I/O error: {0}")] + LanceIo(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unimplemented_carries_method_name() { + let err = ArchetypeError::Unimplemented { method: "World::fork" }; + let msg = format!("{err}"); + assert!(msg.contains("World::fork")); + assert!(msg.contains("not yet implemented")); + } + + #[test] + fn schema_mismatch_formats() { + let err = ArchetypeError::SchemaMismatch { + expected: "Schema{a: Int32}".to_string(), + actual: "Schema{a: Utf8}".to_string(), + }; + let msg = format!("{err}"); + assert!(msg.contains("expected")); + assert!(msg.contains("Int32")); + assert!(msg.contains("Utf8")); + } +} diff --git a/crates/lance-graph-archetype/src/lib.rs b/crates/lance-graph-archetype/src/lib.rs new file mode 100644 index 00000000..570b3def --- /dev/null +++ b/crates/lance-graph-archetype/src/lib.rs @@ -0,0 +1,42 @@ +//! # lance-graph-archetype +//! +//! Archetype transcode scaffold. Per ADR-0001 Decision 1, this crate +//! defines its OWN Rust interface for ECS-style components, processors, +//! a world, and a command broker — it is NOT a mirror of the Python +//! `VangelisTech/archetype` API. The Python repository is a design +//! spec; there is no runtime dependency on it. +//! +//! Per ADR-0001 Decision 3, every type defined here is INSIDE-BBB: +//! none of them cross the external membrane onto +//! `CognitiveEventRow`. The scalar projection for "an archetype tick +//! happened" is already carried by +//! `CognitiveEventRow.cycle_fp_hi/lo` + `MetaWord`. +//! +//! ## Module layout +//! +//! - [`component`] — the `Component` trait (Arrow-field projection). +//! - [`processor`] — the `Processor` trait (RecordBatch transform). +//! - [`world`] — `World` meta-state (tick + dataset URI; `fork` / `at_tick` +//! are stubs pending DU-2.8). +//! - [`command_broker`] — `CommandBroker` + `Command` (deferred world +//! mutations, drained at tick boundaries). +//! - [`error`] — `ArchetypeError` (thiserror-backed). +//! +//! ## Status +//! +//! Scaffold only (DU-2.1..2.6). No runtime behaviour yet. See +//! `.claude/plans/archetype-scaffold-v1.md` for scope. + +#![deny(missing_docs)] + +pub mod command_broker; +pub mod component; +pub mod error; +pub mod processor; +pub mod world; + +pub use command_broker::{Command, CommandBroker}; +pub use component::Component; +pub use error::ArchetypeError; +pub use processor::Processor; +pub use world::World; diff --git a/crates/lance-graph-archetype/src/processor.rs b/crates/lance-graph-archetype/src/processor.rs new file mode 100644 index 00000000..f6ea615a --- /dev/null +++ b/crates/lance-graph-archetype/src/processor.rs @@ -0,0 +1,77 @@ +//! The `Processor` trait — a function from `RecordBatch` to `RecordBatch` +//! gated by a schema predicate. +//! +//! Per ADR-0001 Decision 1, Processors transcode Python's ECS-system +//! concept into Rust. A processor is NOT a method on a world; it is a +//! free-standing operator that declares (a) which schemas it matches +//! and (b) how it transforms a matching batch. The world drives the +//! dispatch in a later deliverable (DU-2.7+); today only the trait +//! lives here. + +use arrow::datatypes::Schema; +use arrow::record_batch::RecordBatch; + +use crate::error::ArchetypeError; + +/// A transcode operator on Arrow `RecordBatch`es. +/// +/// Implementors declare a schema matcher and a batch transformer. The +/// scaffold trait uses associated functions (no `&self`) so that the +/// World-level dispatcher can hold a `&'static` table of processors +/// without needing heap allocation. A later deliverable may introduce +/// a `dyn Processor` object-safe variant alongside. +pub trait Processor { + /// Return `true` iff this processor can operate on a batch with the + /// given schema. Called once per batch before `process`; mismatches + /// surface via the enclosing loop, not via `ArchetypeError`. + fn matches(schema: &Schema) -> bool; + + /// Transform the input batch. Implementations MUST return a schema + /// that is either identical to the input or a schema-migration + /// documented in the crate README. On schema violations, return + /// `ArchetypeError::SchemaMismatch`. + fn process(batch: RecordBatch) -> Result; +} + +#[cfg(test)] +mod tests { + use super::*; + use arrow::array::{ArrayRef, Int64Array}; + use arrow::datatypes::{DataType, Field, Schema}; + use std::sync::Arc; + + /// Identity processor — matches any single-column Int64 schema and + /// returns the input unchanged. Exists purely to prove the trait + /// is constructable. + struct IdentityProcessor; + + impl Processor for IdentityProcessor { + fn matches(schema: &Schema) -> bool { + schema.fields().len() == 1 + && schema.field(0).data_type() == &DataType::Int64 + } + + fn process(batch: RecordBatch) -> Result { + Ok(batch) + } + } + + #[test] + fn trait_object_is_constructable() { + let schema = Schema::new(vec![Field::new("x", DataType::Int64, false)]); + assert!(IdentityProcessor::matches(&schema)); + + let arr: ArrayRef = Arc::new(Int64Array::from(vec![1_i64, 2, 3])); + let batch = RecordBatch::try_new(Arc::new(schema), vec![arr]).unwrap(); + + let out = IdentityProcessor::process(batch).unwrap(); + assert_eq!(out.num_rows(), 3); + assert_eq!(out.num_columns(), 1); + } + + #[test] + fn matches_rejects_wrong_schema() { + let schema = Schema::new(vec![Field::new("x", DataType::Utf8, false)]); + assert!(!IdentityProcessor::matches(&schema)); + } +} diff --git a/crates/lance-graph-archetype/src/world.rs b/crates/lance-graph-archetype/src/world.rs new file mode 100644 index 00000000..54b5a4c8 --- /dev/null +++ b/crates/lance-graph-archetype/src/world.rs @@ -0,0 +1,130 @@ +//! `World` — archetype meta-state carrier. +//! +//! Per ADR-0001 Decision 1, `World` is the archetype-side meta-state +//! that pairs a Lance dataset URI with a logical tick counter. This is +//! DISTINCT from the runtime blackboard +//! (`lance_graph_contract::a2a_blackboard::Blackboard`), which carries +//! per-round expert entries. The pairing is intentional — see the +//! mapping table in the plan's Architecture notes section. +//! +//! Stub-only at this stage: `fork` and `at_tick` return +//! `ArchetypeError::Unimplemented` until DU-2.8 wires them to +//! `lance::checkout(branch)` / dataset version pinning respectively. +//! Docstring anchors tie to ADR-0001 §61-72 (dataset branching) and +//! §95 (tick semantics). + +use crate::error::ArchetypeError; + +/// Archetype meta-state: a dataset URI and a monotonic tick counter. +/// +/// The dataset URI points at the Lance dataset that backs this world's +/// archetype storage; it is a `String` placeholder on purpose — wiring +/// to an actual `lance::Dataset` is DU-2.8, deliberately not on this +/// PR (see plan's Non-goals section). +#[derive(Debug, Clone)] +pub struct World { + /// Logical tick counter. Starts at 0 and advances by 1 per `tick()`. + /// Not related to wall-clock time; archetype Processors may fire + /// multiple times within a single host cycle. + tick: u64, + + /// URI of the backing Lance dataset (scheme + path). Today this is + /// opaque — the `fork`/`at_tick` methods that would interpret it + /// are stubs. Kept pub(crate)-readable via `dataset_uri()` so that + /// downstream tests can assert round-trips. + dataset_uri: String, +} + +impl World { + /// Construct a new world at tick 0 pinned to the given dataset URI. + /// No I/O is performed; the URI is stored verbatim. + pub fn new(dataset_uri: impl Into) -> Self { + Self { + tick: 0, + dataset_uri: dataset_uri.into(), + } + } + + /// Advance the tick counter by 1. Returns the new tick value. + /// Overflow is not checked at the scaffold stage — any realistic + /// workload will rotate ticks via `at_tick` well before u64 wraps. + pub fn tick(&mut self) -> u64 { + self.tick = self.tick.saturating_add(1); + self.tick + } + + /// Read the current tick without advancing. + pub fn current_tick(&self) -> u64 { + self.tick + } + + /// Read the dataset URI this world is pinned to. + pub fn dataset_uri(&self) -> &str { + &self.dataset_uri + } + + /// Fork this world onto a new dataset branch. Per ADR-0001 §61-72, + /// this will call `lance::checkout(branch)` and return a fresh + /// `World` pinned to the branch HEAD. + /// + /// **Not implemented yet.** DU-2.8 will wire the Lance call; today + /// returns `ArchetypeError::Unimplemented { method: "World::fork" }`. + pub fn fork(&self, _branch: &str) -> Result { + Err(ArchetypeError::Unimplemented { method: "World::fork" }) + } + + /// Rewind (or fast-forward) this world to a specific tick. Per + /// ADR-0001 §95, this will pin the Lance dataset version that + /// corresponds to `tick`. + /// + /// **Not implemented yet.** DU-2.8 will wire the dataset-version + /// lookup; today returns + /// `ArchetypeError::Unimplemented { method: "World::at_tick" }`. + pub fn at_tick(&self, _tick: u64) -> Result { + Err(ArchetypeError::Unimplemented { method: "World::at_tick" }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_world_is_at_tick_zero() { + let w = World::new("lance://tmp/archetype"); + assert_eq!(w.current_tick(), 0); + assert_eq!(w.dataset_uri(), "lance://tmp/archetype"); + } + + #[test] + fn tick_increments() { + let mut w = World::new("lance://tmp/archetype"); + assert_eq!(w.tick(), 1); + assert_eq!(w.tick(), 2); + assert_eq!(w.current_tick(), 2); + } + + #[test] + fn fork_returns_unimplemented() { + let w = World::new("lance://tmp/archetype"); + let err = w.fork("experiment").unwrap_err(); + match err { + ArchetypeError::Unimplemented { method } => { + assert_eq!(method, "World::fork"); + } + other => panic!("expected Unimplemented, got {other:?}"), + } + } + + #[test] + fn at_tick_returns_unimplemented() { + let w = World::new("lance://tmp/archetype"); + let err = w.at_tick(42).unwrap_err(); + match err { + ArchetypeError::Unimplemented { method } => { + assert_eq!(method, "World::at_tick"); + } + other => panic!("expected Unimplemented, got {other:?}"), + } + } +} From 1e71fc63d5d4bbc98aa40323bc85d654f7443c7b Mon Sep 17 00:00:00 2001 From: AdaWorldAPI Date: Fri, 24 Apr 2026 22:20:51 +0200 Subject: [PATCH 2/2] chore(log): add AGENT_LOG entry for archetype scaffold v2 https://claude.ai/code/session_01SbYsmmbPf9YQuYbHZN52Zh --- .claude/board/AGENT_LOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .claude/board/AGENT_LOG.md diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md new file mode 100644 index 00000000..41f10847 --- /dev/null +++ b/.claude/board/AGENT_LOG.md @@ -0,0 +1,12 @@ +# AGENT_LOG + +Append-only log of agent sessions. Prepend new entries at the top. + +--- + +## 2026-04-24T16:30 — Archetype scaffold v2 (sonnet, claude/archetype-crate-scaffold) + +**D-ids:** DU-2.1..2.6 +**Commit:** `816a7c0` +**Tests:** 12 pass +**Outcome:** Shipped `lance-graph-archetype` crate scaffold: Component + Processor traits (Arrow-backed), World meta-state with tick/fork/at_tick stubs, CommandBroker FIFO queue, ArchetypeError (thiserror). Added to root workspace members. No compile errors; 12 unit tests green.