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
12 changes: 12 additions & 0 deletions .claude/board/AGENT_LOG.md
Original file line number Diff line number Diff line change
@@ -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.
90 changes: 90 additions & 0 deletions .claude/plans/archetype-scaffold-v1.md
Original file line number Diff line number Diff line change
@@ -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<arrow::record_batch::RecordBatch, ArchetypeError>; }`.
- **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<Command>, ... }` + `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
```
9 changes: 9 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions crates/lance-graph-archetype/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
99 changes: 99 additions & 0 deletions crates/lance-graph-archetype/src/command_broker.rs
Original file line number Diff line number Diff line change
@@ -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<Command>`; DU-2.7
/// may upgrade to a `std::sync::mpsc::channel` for multi-processor
/// concurrency.
#[derive(Debug, Default, Clone)]
pub struct CommandBroker {
queue: Vec<Command>,
}

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<Command>`; the broker is empty afterwards. O(n).
pub fn drain(&mut self) -> Vec<Command> {
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());
}
}
73 changes: 73 additions & 0 deletions crates/lance-graph-archetype/src/component.rs
Original file line number Diff line number Diff line change
@@ -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: `"<crate>::<type>"`.
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"
);
}
}
73 changes: 73 additions & 0 deletions crates/lance-graph-archetype/src/error.rs
Original file line number Diff line number Diff line change
@@ -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<T, ArchetypeError>`.
/// 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"));
}
}
Loading
Loading