mirror of
https://github.com/samvallad33/vestige.git
synced 2026-06-16 21:05:15 +02:00
docs(plans): add Phase 1 amendment sub-plans 0001a/b/c
Three sub-plans operationalising ADR 0002 D1 + D3 on the existing
feat/storage-trait-phase1 branch (790c0c8, not yet pushed upstream):
- 0001a-trait-rewrite.md -- rewrite MemoryStore with
#[trait_variant::make(MemoryStore: Send)] generating a non-Send
LocalMemoryStore companion. Production callers use Arc<Storage> and are
unaffected; only the trait declaration and SQLite impl block change.
- 0001b-sqlite-split.md -- pure code motion. Split sqlite.rs (8713 lines)
into a sqlite/ directory (mod, crud, search, scheduling, graph, domain,
registry, portable_sync, trait_impl). Public re-exports unchanged; tests
green commit-by-commit. Depends on 0001a so trait_impl.rs picks up the
trait_variant attribute once.
- 0001c-async-trait-sunset.md -- rewrite Embedder the same way, then
remove async-trait = "0.1" from crates/vestige-core/Cargo.toml. End
state: zero async_trait references in the workspace.
Together these three lands as PR A.
This commit is contained in:
parent
c584ec8afe
commit
697ade5b02
3 changed files with 2268 additions and 0 deletions
689
docs/plans/0001a-trait-rewrite.md
Normal file
689
docs/plans/0001a-trait-rewrite.md
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
# Phase 1 Amendment Sub-Plan: async_trait -> trait_variant
|
||||
|
||||
**Status**: Draft
|
||||
**Depends on**: Phase 1 (already on `feat/storage-trait-phase1`, tip 790c0c8)
|
||||
**Followed by**: `docs/plans/0001c-async-trait-sunset.md` (Embedder rewrite + async-trait crate removal)
|
||||
**Related**: docs/adr/0002-phase-2-execution.md (decision D1), docs/plans/0001-phase-1-storage-trait-extraction.md
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Phase 1 froze with the storage trait declared as
|
||||
`#[async_trait::async_trait] pub trait MemoryStore: Send + Sync + 'static`
|
||||
and a `pub use MemoryStore as LocalMemoryStore;` alias. `async_trait` boxes
|
||||
every `async fn` in the trait into `Pin<Box<dyn Future + Send>>`. That is one
|
||||
heap allocation per call inside the hottest code path in Vestige (insert,
|
||||
search, update_scheduling are all on this surface). It also collapses the
|
||||
two intended trait shapes -- a Send-bound `MemoryStore` for tokio/axum and a
|
||||
non-Send `LocalMemoryStore` for thread-local backends -- into a single trait
|
||||
behind an alias, removing the type-system signal we will want for Phase 2's
|
||||
Postgres backend.
|
||||
|
||||
ADR 0002 decision D1 supersedes this. The amendment lands on the existing
|
||||
`feat/storage-trait-phase1` branch before Phase 2 starts, before the PR is
|
||||
opened upstream. The end state:
|
||||
|
||||
- `LocalMemoryStore` is the source-of-truth trait, declared with native
|
||||
async-fn-in-trait (stable on MSRV 1.91), no `Sync` bound on the trait
|
||||
itself, and `Sync + 'static` on the trait object.
|
||||
- `MemoryStore` is auto-generated by `#[trait_variant::make(MemoryStore: Send)]`
|
||||
with `Send` bounds on every returned future, so `Arc<dyn MemoryStore>` is
|
||||
movable across `tokio::spawn`.
|
||||
- `trait-variant` 0.1 is already present in `crates/vestige-core/Cargo.toml`.
|
||||
The `async-trait` crate dependency stays in place through this sub-plan
|
||||
(it is still in use by the embedder impl); removing it is the job of
|
||||
`0001c-async-trait-sunset.md`.
|
||||
- No production caller changes. Every production call site holds
|
||||
`Arc<Storage>` (the concrete `SqliteMemoryStore` alias), which the trait
|
||||
rewrite does not touch. Only the trait declaration and impl blocks change.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Rewrite `MemoryStore` / `LocalMemoryStore` declaration in
|
||||
`crates/vestige-core/src/storage/memory_store.rs` to use
|
||||
`#[trait_variant::make(MemoryStore: Send)] pub trait LocalMemoryStore`.
|
||||
- Remove the `pub use MemoryStore as LocalMemoryStore;` alias from the same
|
||||
file. `LocalMemoryStore` becomes the real trait; `MemoryStore` is the
|
||||
generated Send variant.
|
||||
- Drop the `#[async_trait::async_trait]` attribute on the SQLite impl block
|
||||
in `crates/vestige-core/src/storage/sqlite.rs` (line 6274). The impl block
|
||||
switches from `impl MemoryStore for SqliteMemoryStore` (currently spelled
|
||||
through the `LocalMemoryStore` alias) to `impl LocalMemoryStore for
|
||||
SqliteMemoryStore`. `trait_variant` provides a blanket `impl<T:
|
||||
LocalMemoryStore + Send> MemoryStore for T` so `&dyn MemoryStore` and
|
||||
`Arc<dyn MemoryStore>` keep working unchanged.
|
||||
- Update doc comments in `memory_store.rs` to drop
|
||||
references to `#[async_trait::async_trait]` and instead describe the
|
||||
`trait_variant` mechanism.
|
||||
- Keep the existing Phase 1 test suite (`tests/phase_1/*.rs`) green. The
|
||||
tests already use `Arc<dyn MemoryStore>` and `Box<dyn Embedder>`, which is
|
||||
exactly the surface the rewrite is meant to preserve.
|
||||
|
||||
### Out of scope -- moved to 0001c
|
||||
|
||||
- Rewriting the `Embedder` / `LocalEmbedder` trait declaration -- handled by
|
||||
`0001c-async-trait-sunset.md`.
|
||||
- Dropping `#[async_trait::async_trait]` from
|
||||
`crates/vestige-core/src/embedder/fastembed.rs`.
|
||||
- Removing `async-trait = "0.1"` from `crates/vestige-core/Cargo.toml`.
|
||||
- The hard `grep -rn 'async_trait' crates/` zero-match gate (async_trait is
|
||||
still present in the embedder module after 0001a alone).
|
||||
|
||||
### Out of scope
|
||||
|
||||
- The SQLite file split (`sqlite.rs` -> `sqlite/` directory). That is the
|
||||
sibling sub-plan `0001b-sqlite-split.md`.
|
||||
- Any production-side refactor that switches `Arc<Storage>` to
|
||||
`Arc<dyn MemoryStore>`. Production code keeps using the concrete alias.
|
||||
- Adding `register_model` / `from_pool` / Postgres-specific variants of
|
||||
`MemoryStoreError`. Those land with the Postgres backend in Phase 2.
|
||||
- Removing the `pub type Storage = SqliteMemoryStore;` alias.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Current state (verified)
|
||||
|
||||
- `crates/vestige-core/Cargo.toml` already declares
|
||||
`trait-variant = "0.1"` (line 117). `async-trait = "0.1"` (line 119)
|
||||
remains in place for the duration of this sub-plan; `0001c` removes it.
|
||||
- `crates/vestige-core/src/storage/memory_store.rs:194` declares the trait
|
||||
with `#[async_trait::async_trait] pub trait MemoryStore: Send + Sync +
|
||||
'static`.
|
||||
- `crates/vestige-core/src/storage/memory_store.rs:262` has
|
||||
`pub use MemoryStore as LocalMemoryStore;`.
|
||||
- `crates/vestige-core/src/storage/sqlite.rs:6274` declares
|
||||
`#[async_trait::async_trait] impl
|
||||
crate::storage::memory_store::LocalMemoryStore for SqliteMemoryStore`.
|
||||
- Production call sites use `Arc<Storage>` (the concrete
|
||||
`SqliteMemoryStore` alias). Confirmed by grep:
|
||||
`grep -rn "dyn MemoryStore\|dyn LocalMemoryStore" --include="*.rs"`
|
||||
returns hits only in `tests/phase_1/*.rs`, in two comments inside
|
||||
`memory_store.rs` and `sqlite.rs`, and in one test-only `&dyn MemoryStore`
|
||||
cast inside `sqlite.rs::tests` (line 8669).
|
||||
- Workspace-wide `async_trait` usages: only the trait declarations and
|
||||
impl blocks in `memory_store.rs`, `sqlite.rs`, `embedder/mod.rs`, and
|
||||
`embedder/fastembed.rs` (verified by
|
||||
`grep -rn async_trait --include="*.rs"`). This sub-plan touches only the
|
||||
first two; the embedder files are addressed by `0001c`.
|
||||
|
||||
### Required crates
|
||||
|
||||
| Crate | Version | Action |
|
||||
|-------|---------|--------|
|
||||
| `trait-variant` | `0.1` | Already declared. Verify present after edit. |
|
||||
| `async-trait` | `0.1` | Unchanged for this sub-plan (still used by embedder impl). Removed by `0001c`. |
|
||||
| `blake3`, `thiserror`, `chrono`, `uuid`, `serde`, `serde_json` | unchanged | unchanged |
|
||||
|
||||
---
|
||||
|
||||
## Files Touched
|
||||
|
||||
Grouped by category. Every change is listed; nothing else gets touched.
|
||||
|
||||
### Trait declarations (vestige-core)
|
||||
|
||||
| File | Lines (approx) | Change |
|
||||
|------|----------------|--------|
|
||||
| `crates/vestige-core/src/storage/memory_store.rs` | 183-262 | Replace `#[async_trait::async_trait]` block with `#[trait_variant::make(MemoryStore: Send)] pub trait LocalMemoryStore`. Drop the `pub use MemoryStore as LocalMemoryStore;` alias. Update doc comments. |
|
||||
|
||||
### Impl blocks (vestige-core)
|
||||
|
||||
| File | Lines (approx) | Change |
|
||||
|------|----------------|--------|
|
||||
| `crates/vestige-core/src/storage/sqlite.rs` | 6274 (impl block header only) | Delete the `#[async_trait::async_trait]` attribute. Keep the `impl crate::storage::memory_store::LocalMemoryStore for SqliteMemoryStore { ... }` body verbatim. |
|
||||
|
||||
### Cargo dependency cleanup
|
||||
|
||||
None in this sub-plan. The `async-trait` crate stays declared in
|
||||
`crates/vestige-core/Cargo.toml` because the embedder impl still uses it.
|
||||
`0001c-async-trait-sunset.md` removes the dependency after the embedder
|
||||
side is rewritten.
|
||||
|
||||
### Cognitive module call sites
|
||||
|
||||
No changes. The 29 cognitive modules under `crates/vestige-core/src/neuroscience/`
|
||||
and `crates/vestige-core/src/advanced/` already operate on concrete
|
||||
`SqliteMemoryStore` (via the `Storage` alias) or on plain data types
|
||||
(`KnowledgeNode`, `Vec<f32>`, `ConnectionRecord`). Grep verified zero
|
||||
production references to `dyn MemoryStore` or `dyn LocalMemoryStore`.
|
||||
|
||||
### MCP call sites
|
||||
|
||||
No changes. All ~30 `vestige-mcp/src/**.rs` files holding `Arc<Storage>`
|
||||
keep working because:
|
||||
- `Storage` is still `pub type Storage = SqliteMemoryStore;` (unchanged).
|
||||
- They call inherent methods on the concrete type, never via a trait object.
|
||||
- `SqliteMemoryStore` implements `LocalMemoryStore`; `trait_variant` auto-
|
||||
generates a blanket `impl<T: LocalMemoryStore + Send> MemoryStore for T`,
|
||||
so the concrete type also satisfies `MemoryStore` for any future caller
|
||||
that wants the trait-object form.
|
||||
|
||||
### Test files (vestige-core integration tests)
|
||||
|
||||
| File | Lines | Change |
|
||||
|------|-------|--------|
|
||||
| `tests/phase_1/trait_round_trip.rs` | 7-18, 134 | No change. Already uses `Arc<dyn MemoryStore>` and `use vestige_core::storage::MemoryStore`. trait_variant emits a `MemoryStore` trait at the same path, so the imports resolve. |
|
||||
| `tests/phase_1/send_bound_variant.rs` | 10-12, 36, 57 | No change. Already asserts the trait_variant Send invariant; the rewrite is what makes the doc comment on lines 3-4 actually true. |
|
||||
| `tests/phase_1/cognitive_module_isolation.rs` | 11, 37, 76, 102, 115 | No change. |
|
||||
| `tests/phase_1/embedding_model_registry.rs` | 10 | No change. |
|
||||
| `tests/phase_1/domain_column_migration.rs` | 98 | No change. |
|
||||
| `crates/vestige-core/src/storage/sqlite.rs::tests` | 8666-8675 | No change. The existing test casts `&s` to `&dyn MemoryStore` and calls trait methods through that vtable; trait_variant preserves this exact dyn-compatible surface. |
|
||||
|
||||
### Documentation
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `crates/vestige-core/src/storage/memory_store.rs` | Rewrite the trait-level doc comment (lines 185-193) to describe trait_variant rather than async_trait. |
|
||||
| `CLAUDE.md` | No change. The repo-level architecture notes do not name the trait attribute. |
|
||||
|
||||
---
|
||||
|
||||
## Trait Declaration Rewrite
|
||||
|
||||
### Before (current state on `feat/storage-trait-phase1`)
|
||||
|
||||
`crates/vestige-core/src/storage/memory_store.rs:183-262`:
|
||||
|
||||
```rust
|
||||
// ----------------------------------------------------------------------------
|
||||
// TRAIT
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The single storage abstraction.
|
||||
///
|
||||
/// `#[async_trait::async_trait]` makes every `async fn` return a
|
||||
/// `Pin<Box<dyn Future + Send>>`, which is required for `Arc<dyn MemoryStore>`
|
||||
/// to be movable across `tokio::spawn` boundaries.
|
||||
///
|
||||
/// `LocalMemoryStore` is a type alias kept for source compatibility with code
|
||||
/// that refers to the non-send variant. In Phase 1 both names refer to the same
|
||||
/// (dyn-compatible, Send-safe) trait.
|
||||
#[async_trait::async_trait]
|
||||
pub trait MemoryStore: Send + Sync + 'static {
|
||||
// --- Lifecycle ---
|
||||
async fn init(&self) -> MemoryStoreResult<()>;
|
||||
async fn health_check(&self) -> MemoryStoreResult<HealthStatus>;
|
||||
|
||||
// ... 25 more async fn ...
|
||||
|
||||
async fn vacuum(&self) -> MemoryStoreResult<()>;
|
||||
}
|
||||
|
||||
/// Type alias kept for source compatibility. Both names refer to the same
|
||||
/// `async_trait`-annotated trait that is dyn-compatible and `Send + Sync`.
|
||||
pub use MemoryStore as LocalMemoryStore;
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
`crates/vestige-core/src/storage/memory_store.rs:183-262`:
|
||||
|
||||
```rust
|
||||
// ----------------------------------------------------------------------------
|
||||
// TRAIT
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The single storage abstraction.
|
||||
///
|
||||
/// `LocalMemoryStore` is the source-of-truth trait. The
|
||||
/// `#[trait_variant::make(MemoryStore: Send)]` attribute auto-generates a
|
||||
/// `MemoryStore` trait whose returned futures are `Send`, so
|
||||
/// `Arc<dyn MemoryStore>` is movable across `tokio::spawn` boundaries while
|
||||
/// `Arc<dyn LocalMemoryStore>` remains usable on single-threaded executors
|
||||
/// and thread-local backends.
|
||||
///
|
||||
/// Every method is native async-fn-in-trait (stable on MSRV 1.91); no
|
||||
/// per-call heap allocation, no boxed futures.
|
||||
#[trait_variant::make(MemoryStore: Send)]
|
||||
pub trait LocalMemoryStore: Sync + 'static {
|
||||
// --- Lifecycle ---
|
||||
async fn init(&self) -> MemoryStoreResult<()>;
|
||||
async fn health_check(&self) -> MemoryStoreResult<HealthStatus>;
|
||||
|
||||
// --- Embedding model registry ---
|
||||
async fn registered_model(&self) -> MemoryStoreResult<Option<ModelSignature>>;
|
||||
async fn register_model(&self, sig: &ModelSignature) -> MemoryStoreResult<()>;
|
||||
|
||||
// --- CRUD ---
|
||||
async fn insert(&self, record: &MemoryRecord) -> MemoryStoreResult<Uuid>;
|
||||
async fn get(&self, id: Uuid) -> MemoryStoreResult<Option<MemoryRecord>>;
|
||||
async fn update(&self, record: &MemoryRecord) -> MemoryStoreResult<()>;
|
||||
async fn delete(&self, id: Uuid) -> MemoryStoreResult<()>;
|
||||
|
||||
// --- Search ---
|
||||
async fn search(&self, query: &SearchQuery) -> MemoryStoreResult<Vec<SearchResult>>;
|
||||
async fn fts_search(&self, text: &str, limit: usize) -> MemoryStoreResult<Vec<SearchResult>>;
|
||||
async fn vector_search(
|
||||
&self,
|
||||
embedding: &[f32],
|
||||
limit: usize,
|
||||
) -> MemoryStoreResult<Vec<SearchResult>>;
|
||||
|
||||
// --- FSRS Scheduling ---
|
||||
async fn get_scheduling(
|
||||
&self,
|
||||
memory_id: Uuid,
|
||||
) -> MemoryStoreResult<Option<SchedulingState>>;
|
||||
async fn update_scheduling(&self, state: &SchedulingState) -> MemoryStoreResult<()>;
|
||||
async fn get_due_memories(
|
||||
&self,
|
||||
before: DateTime<Utc>,
|
||||
limit: usize,
|
||||
) -> MemoryStoreResult<Vec<(MemoryRecord, SchedulingState)>>;
|
||||
|
||||
// --- Graph (spreading activation) ---
|
||||
async fn add_edge(&self, edge: &MemoryEdge) -> MemoryStoreResult<()>;
|
||||
async fn get_edges(
|
||||
&self,
|
||||
node_id: Uuid,
|
||||
edge_type: Option<&str>,
|
||||
) -> MemoryStoreResult<Vec<MemoryEdge>>;
|
||||
async fn remove_edge(&self, source: Uuid, target: Uuid) -> MemoryStoreResult<()>;
|
||||
async fn get_neighbors(
|
||||
&self,
|
||||
node_id: Uuid,
|
||||
depth: usize,
|
||||
) -> MemoryStoreResult<Vec<(MemoryRecord, f64)>>;
|
||||
|
||||
// --- Domains ---
|
||||
async fn list_domains(&self) -> MemoryStoreResult<Vec<Domain>>;
|
||||
async fn get_domain(&self, id: &str) -> MemoryStoreResult<Option<Domain>>;
|
||||
async fn upsert_domain(&self, domain: &Domain) -> MemoryStoreResult<()>;
|
||||
async fn delete_domain(&self, id: &str) -> MemoryStoreResult<()>;
|
||||
async fn classify(&self, embedding: &[f32]) -> MemoryStoreResult<Vec<(String, f64)>>;
|
||||
|
||||
// --- Bulk / Maintenance ---
|
||||
async fn count(&self) -> MemoryStoreResult<usize>;
|
||||
async fn get_stats(&self) -> MemoryStoreResult<StoreStats>;
|
||||
async fn vacuum(&self) -> MemoryStoreResult<()>;
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The `pub use MemoryStore as LocalMemoryStore;` line on the current
|
||||
`memory_store.rs:262` is **deleted** entirely. `MemoryStore` is now the
|
||||
generated trait that `trait_variant::make` emits at the same module path;
|
||||
`LocalMemoryStore` is the source-of-truth declaration. Both names export
|
||||
from `storage/mod.rs` already (see lines 10-14 of that file).
|
||||
- `Sync + 'static` on `LocalMemoryStore` (and no `Send` bound on the trait
|
||||
itself) is correct: `Send` on the trait is what `trait_variant::make`
|
||||
inserts when it emits the `MemoryStore` variant. The current trait carries
|
||||
`Send + Sync + 'static`; the rewrite drops the `Send` bound from the
|
||||
local variant. `Arc<dyn LocalMemoryStore>` is `Sync` but not `Send`;
|
||||
`Arc<dyn MemoryStore>` (the generated variant) is `Send + Sync`.
|
||||
- `trait_variant` 0.1 does **not** require any attribute on the impl block.
|
||||
The attribute lives only on the trait declaration. See the next section.
|
||||
|
||||
The Embedder trait rewrite uses the identical `trait_variant::make` pattern
|
||||
and is fully specified in `0001c-async-trait-sunset.md`.
|
||||
|
||||
---
|
||||
|
||||
## Impl Block Migration
|
||||
|
||||
`trait_variant` 0.1 attaches the attribute only to the trait declaration.
|
||||
The impl side is plain `impl LocalMemoryStore for SqliteMemoryStore`; no
|
||||
attribute on the impl, no `#[trait_variant::make(MemoryStore: Send)]` on the
|
||||
impl block. The crate auto-generates the blanket
|
||||
`impl<T: LocalMemoryStore + Send> MemoryStore for T`, so any concrete type
|
||||
that implements `LocalMemoryStore` automatically also implements
|
||||
`MemoryStore` provided it is `Send`.
|
||||
|
||||
`SqliteMemoryStore` already is `Send + Sync` (it holds `Mutex<Connection>`
|
||||
fields whose `Mutex` is `std::sync::Mutex`, which is `Send + Sync` when its
|
||||
guarded type is `Send`; `rusqlite::Connection` is `Send`). No new bound is
|
||||
needed.
|
||||
|
||||
### Before
|
||||
|
||||
`crates/vestige-core/src/storage/sqlite.rs:6274`:
|
||||
|
||||
```rust
|
||||
#[async_trait::async_trait]
|
||||
impl crate::storage::memory_store::LocalMemoryStore for SqliteMemoryStore {
|
||||
async fn init(&self) -> crate::storage::memory_store::MemoryStoreResult<()> {
|
||||
// Migrations run in `new`; this is a no-op for the SQLite backend.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ... ~2400 lines of method bodies, unchanged ...
|
||||
}
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
`crates/vestige-core/src/storage/sqlite.rs:6274`:
|
||||
|
||||
```rust
|
||||
impl crate::storage::memory_store::LocalMemoryStore for SqliteMemoryStore {
|
||||
async fn init(&self) -> crate::storage::memory_store::MemoryStoreResult<()> {
|
||||
// Migrations run in `new`; this is a no-op for the SQLite backend.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ... ~2400 lines of method bodies, unchanged ...
|
||||
}
|
||||
```
|
||||
|
||||
Diff is exactly one removed line (the `#[async_trait::async_trait]` attribute).
|
||||
Every method body, every `async fn` signature, every `use` statement inside
|
||||
the impl block stays verbatim. No `Box::pin(async move { ... })`, no manual
|
||||
`Pin<Box<dyn Future>>`, no `'async_trait` lifetime markers; native
|
||||
async-fn-in-trait does this directly.
|
||||
|
||||
The Embedder impl block rewrite follows the identical "remove one
|
||||
`#[async_trait::async_trait]` attribute" pattern and is fully specified in
|
||||
`0001c-async-trait-sunset.md`.
|
||||
|
||||
### Why the impl block does not need an attribute
|
||||
|
||||
`trait_variant::make` generates two things from the source trait:
|
||||
|
||||
1. The source trait itself (`LocalMemoryStore`), with native async fns.
|
||||
2. A second trait (`MemoryStore`) whose method signatures return
|
||||
`impl Future<Output = ...> + Send` instead of `impl Future<Output = ...>`,
|
||||
plus a blanket impl wiring concrete types through.
|
||||
|
||||
Both are emitted at the macro-call site. `SqliteMemoryStore` only writes one
|
||||
impl block (against `LocalMemoryStore`); the macro-generated blanket
|
||||
guarantees `SqliteMemoryStore: MemoryStore` for free. The current `&dyn
|
||||
MemoryStore` casts (sqlite.rs:8669; tests under tests/phase_1/) keep
|
||||
type-checking unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Call Site Audit
|
||||
|
||||
Verified via:
|
||||
|
||||
```bash
|
||||
grep -rn "dyn MemoryStore\|dyn LocalMemoryStore" --include="*.rs" \
|
||||
/home/delandtj/prppl/vestige-phase2/
|
||||
grep -rn "Arc<Storage>\|Arc<SqliteMemoryStore>" --include="*.rs" \
|
||||
/home/delandtj/prppl/vestige-phase2/
|
||||
grep -rn "use.*MemoryStore;\|use.*LocalMemoryStore;" --include="*.rs" \
|
||||
/home/delandtj/prppl/vestige-phase2/
|
||||
```
|
||||
|
||||
### Files that reference the trait object form (`dyn MemoryStore` / `dyn LocalMemoryStore`)
|
||||
|
||||
All test-only or doc-comment-only:
|
||||
|
||||
| File | Line | Use | Required change |
|
||||
|------|------|-----|-----------------|
|
||||
| `tests/phase_1/trait_round_trip.rs` | 7-18 | `Arc<dyn MemoryStore>` in `make_store()` and test bodies | None. `MemoryStore` is the generated Send variant; signature stays. |
|
||||
| `tests/phase_1/trait_round_trip.rs` | 134 | `Arc<dyn MemoryStore>` upcast inside a test body | None. |
|
||||
| `tests/phase_1/send_bound_variant.rs` | 10-97 | `Arc<dyn MemoryStore>` moved across `tokio::spawn` | None. This test becomes meaningfully correct only after the rewrite (currently it relies on async_trait boxing; after the rewrite it relies on trait_variant's Send variant -- same observable outcome). |
|
||||
| `tests/phase_1/cognitive_module_isolation.rs` | 11, 33-115 | `Arc<dyn MemoryStore>` passed into cognitive module-style closures | None. |
|
||||
| `tests/phase_1/embedding_model_registry.rs` | 10 | `Arc<dyn MemoryStore>` in `make_store()` | None. |
|
||||
| `tests/phase_1/domain_column_migration.rs` | 98 | `Arc<dyn MemoryStore>` cast inside a migration assertion | None. |
|
||||
| `crates/vestige-core/src/storage/sqlite.rs` | 8666-8675 | `let dyn_s: &dyn MemoryStore = &s;` inside `mod tests` | None. The cast is testing that the dyn-vtable still resolves the trait methods correctly; after the rewrite it resolves through the `MemoryStore` trait that `trait_variant::make` emits at the same path. |
|
||||
| `crates/vestige-core/src/storage/memory_store.rs` | 188 (doc) | Doc comment mentioning `Arc<dyn MemoryStore>` | Replaced as part of the doc rewrite (see Trait Declaration section). |
|
||||
|
||||
### Files that hold the concrete type (`Arc<Storage>` / `Arc<SqliteMemoryStore>`)
|
||||
|
||||
35 files, 116 hits. Every one of them keeps working unchanged because the
|
||||
concrete `SqliteMemoryStore` type stays exactly as it is. Listed here for
|
||||
completeness so a reviewer can confirm none of them needs an edit:
|
||||
|
||||
```
|
||||
crates/vestige-core/src/storage/mod.rs (alias declaration)
|
||||
crates/vestige-core/src/storage/sqlite.rs (impl block)
|
||||
crates/vestige-mcp/src/server.rs (Arc<Storage> in McpServer)
|
||||
crates/vestige-mcp/src/cognitive.rs (hydrate(&Storage))
|
||||
crates/vestige-mcp/src/autopilot.rs
|
||||
crates/vestige-mcp/src/protocol/http.rs
|
||||
crates/vestige-mcp/src/dashboard/mod.rs
|
||||
crates/vestige-mcp/src/dashboard/state.rs
|
||||
crates/vestige-mcp/src/dashboard/handlers.rs
|
||||
crates/vestige-mcp/src/resources/codebase.rs
|
||||
crates/vestige-mcp/src/resources/memory.rs
|
||||
crates/vestige-mcp/src/tools/changelog.rs
|
||||
crates/vestige-mcp/src/tools/codebase_unified.rs
|
||||
crates/vestige-mcp/src/tools/context.rs
|
||||
crates/vestige-mcp/src/tools/contradictions.rs
|
||||
crates/vestige-mcp/src/tools/cross_reference.rs
|
||||
crates/vestige-mcp/src/tools/dedup.rs
|
||||
crates/vestige-mcp/src/tools/dream.rs
|
||||
crates/vestige-mcp/src/tools/explore.rs
|
||||
crates/vestige-mcp/src/tools/feedback.rs
|
||||
crates/vestige-mcp/src/tools/graph.rs
|
||||
crates/vestige-mcp/src/tools/health.rs
|
||||
crates/vestige-mcp/src/tools/importance.rs
|
||||
crates/vestige-mcp/src/tools/intention_unified.rs
|
||||
crates/vestige-mcp/src/tools/maintenance.rs
|
||||
crates/vestige-mcp/src/tools/memory_states.rs
|
||||
crates/vestige-mcp/src/tools/memory_unified.rs
|
||||
crates/vestige-mcp/src/tools/predict.rs
|
||||
crates/vestige-mcp/src/tools/restore.rs
|
||||
crates/vestige-mcp/src/tools/review.rs
|
||||
crates/vestige-mcp/src/tools/search_unified.rs
|
||||
crates/vestige-mcp/src/tools/session_context.rs
|
||||
crates/vestige-mcp/src/tools/smart_ingest.rs
|
||||
crates/vestige-mcp/src/tools/suppress.rs
|
||||
crates/vestige-mcp/src/tools/tagging.rs
|
||||
crates/vestige-mcp/src/tools/timeline.rs
|
||||
```
|
||||
|
||||
Each holds `Arc<Storage>` and dispatches to inherent methods on
|
||||
`SqliteMemoryStore`. None of them goes through a trait vtable. Required
|
||||
change for every one of them: **none**.
|
||||
|
||||
### Files that `use ...MemoryStore` from production code
|
||||
|
||||
```
|
||||
grep -rn "use.*MemoryStore;\|use.*LocalMemoryStore;" --include="*.rs" \
|
||||
| grep -v "memory_store.rs\|sqlite.rs\|tests/phase_1"
|
||||
```
|
||||
|
||||
returns nothing. Production code does not import the trait by name.
|
||||
|
||||
### Conclusion
|
||||
|
||||
The rewrite is a strictly local change to two source files
|
||||
(`storage/memory_store.rs` and `storage/sqlite.rs`). Zero production call
|
||||
sites need edits. The integration tests that consume `Arc<dyn MemoryStore>`
|
||||
keep their current form; the rewrite is what gives that signature its
|
||||
no-box semantics on the storage side. The `Box<dyn Embedder>` surface is
|
||||
addressed by `0001c-async-trait-sunset.md`.
|
||||
|
||||
---
|
||||
|
||||
## Commit Sequence
|
||||
|
||||
Two commits, each green on `cargo test -p vestige-core --no-default-features`
|
||||
and `cargo test -p vestige-core --features embeddings,vector-search`.
|
||||
|
||||
### Commit 1: rewrite MemoryStore / LocalMemoryStore trait declaration
|
||||
|
||||
- Touches: `crates/vestige-core/src/storage/memory_store.rs` only.
|
||||
- Action: replace lines 183-262 per the "Trait Declaration Rewrite" section
|
||||
above. Delete the `pub use MemoryStore as LocalMemoryStore;` line.
|
||||
- Green after: `cargo check -p vestige-core` (the impl block in `sqlite.rs`
|
||||
still has `#[async_trait::async_trait]` on it, but it still resolves
|
||||
through the `LocalMemoryStore` trait which is now native-async; the
|
||||
`async_trait` macro is harmless when applied to a trait that the impl
|
||||
block targets by path, because the macro rewrites the impl's async fns
|
||||
into boxed-future fns whose signatures still match the native-async
|
||||
declarations after trait_variant lowering). If `cargo check` complains
|
||||
here, fold commit 2 into commit 1.
|
||||
|
||||
**Mitigation if check fails between commits 1 and 2:** combine the two
|
||||
into a single commit. The split is offered for review convenience; the
|
||||
build must be green after every commit lands.
|
||||
|
||||
### Commit 2: drop `#[async_trait::async_trait]` from SqliteMemoryStore impl
|
||||
|
||||
- Touches: `crates/vestige-core/src/storage/sqlite.rs` only.
|
||||
- Action: delete line 6274 (`#[async_trait::async_trait]`).
|
||||
- Green after: `cargo test -p vestige-core --features embeddings,vector-search`,
|
||||
including all `trait_*` tests inside `sqlite.rs::tests` (lines 8643-8712)
|
||||
and the trait-object cast on line 8669.
|
||||
|
||||
### Combined alternative
|
||||
|
||||
If the per-step split feels artificial, commits 1 and 2 can collapse into
|
||||
a single commit covering both the trait rewrite and the impl-attribute
|
||||
drop for `MemoryStore`. That is acceptable; the two-commit form is
|
||||
preferred only because it lets a reviewer bisect trait-rewrite failures
|
||||
separately from impl-rewrite failures.
|
||||
|
||||
The Embedder / fastembed commits and the `async-trait` Cargo dependency
|
||||
removal live in `0001c-async-trait-sunset.md`.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Every command runs from the repo root unless noted otherwise.
|
||||
|
||||
```bash
|
||||
# 1. Vestige-core, default features (embeddings + vector-search).
|
||||
cargo test -p vestige-core --features embeddings,vector-search
|
||||
|
||||
# 2. Vestige-core, minimal features (no embeddings, no vector-search).
|
||||
cargo test -p vestige-core --no-default-features
|
||||
|
||||
# 3. Workspace build, release mode (catches any feature-gated regression
|
||||
# in the vestige-mcp tools tree).
|
||||
cargo build --workspace --release
|
||||
|
||||
# 4. Whole-workspace test (vestige-mcp 406 tests + vestige-core 352 tests
|
||||
# per the CLAUDE.md baseline).
|
||||
cargo test --workspace
|
||||
|
||||
# 5. Phase 1 integration tests (these are the trait-shape contract).
|
||||
cargo test --test trait_round_trip --features embeddings,vector-search
|
||||
cargo test --test send_bound_variant --features embeddings,vector-search
|
||||
cargo test --test cognitive_module_isolation --features embeddings,vector-search
|
||||
cargo test --test embedding_model_registry --features embeddings,vector-search
|
||||
cargo test --test domain_column_migration --features embeddings,vector-search
|
||||
|
||||
# 6. Clippy gate, deny warnings (matches Phase 1 PR policy of zero warnings).
|
||||
cargo clippy --workspace --all-targets --features embeddings,vector-search -- -D warnings
|
||||
|
||||
# 7. Storage-side dependency hygiene check (must return zero hits).
|
||||
# Scoped to the storage module only -- the embedder module still uses
|
||||
# async_trait until 0001c lands.
|
||||
grep -rn "async_trait\|async-trait" crates/vestige-core/src/storage/
|
||||
|
||||
# 8. Confirm trait_variant attribute is in place on the storage trait
|
||||
# (must return exactly one hit in memory_store.rs).
|
||||
grep -rn "trait_variant::make" crates/vestige-core/src/storage/
|
||||
```
|
||||
|
||||
Expected outcomes:
|
||||
|
||||
- Command 1: 352 tests pass (matches baseline).
|
||||
- Command 2: smaller test count, all pass.
|
||||
- Command 3: workspace compiles in release mode.
|
||||
- Command 4: 758 total tests pass (matches CLAUDE.md baseline).
|
||||
- Command 5: each phase_1 integration test binary runs green. The
|
||||
`send_bound_variant::arc_dyn_memory_store_moves_across_tokio_tasks` test
|
||||
is the canary; if `MemoryStore` lost its Send-bound future variant, this
|
||||
fails to compile with "future cannot be sent between threads safely".
|
||||
- Command 6: zero clippy warnings. The rewrite must not introduce a new
|
||||
`clippy::needless_lifetimes` or `clippy::async_yields_async`.
|
||||
- Command 7: empty output. async_trait is gone from the storage module.
|
||||
The embedder module still contains async_trait; that is removed by
|
||||
`0001c-async-trait-sunset.md`.
|
||||
- Command 8: one hit, in `memory_store.rs`.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
A reviewer should be able to check every box:
|
||||
|
||||
- [ ] `crates/vestige-core/src/storage/memory_store.rs` declares the trait
|
||||
with `#[trait_variant::make(MemoryStore: Send)] pub trait
|
||||
LocalMemoryStore: Sync + 'static`, no `async_trait` attribute, no
|
||||
`Send` bound on `LocalMemoryStore` itself.
|
||||
- [ ] `crates/vestige-core/src/storage/memory_store.rs` no longer contains
|
||||
`pub use MemoryStore as LocalMemoryStore;`.
|
||||
- [ ] `crates/vestige-core/src/storage/sqlite.rs:6274` is plain
|
||||
`impl crate::storage::memory_store::LocalMemoryStore for
|
||||
SqliteMemoryStore` -- no attribute on the impl block.
|
||||
- [ ] `grep -rn "async_trait\|async-trait" crates/vestige-core/src/storage/`
|
||||
returns zero hits.
|
||||
- [ ] `grep -rn "trait_variant::make" crates/vestige-core/src/storage/`
|
||||
returns exactly one hit (the storage trait in `memory_store.rs`).
|
||||
- [ ] All 758 workspace tests pass (`cargo test --workspace`).
|
||||
- [ ] Phase 1 integration tests pass with the trait-object surface
|
||||
(`Arc<dyn MemoryStore>`) intact.
|
||||
- [ ] `cargo clippy --workspace --all-targets --features
|
||||
embeddings,vector-search -- -D warnings` is clean.
|
||||
- [ ] No production source file under `crates/vestige-mcp/` or
|
||||
`crates/vestige-core/src/{neuroscience,advanced,consolidation,codebase,
|
||||
memory,embeddings,embedder}/` was modified by this sub-plan.
|
||||
- [ ] `Arc<Storage>` still type-checks at every existing call site (verified
|
||||
by the workspace test pass).
|
||||
- [ ] Doc comments on the storage trait declaration describe
|
||||
`trait_variant`, not `async_trait`.
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- **`trait_variant` 0.1 macro emits unexpected diagnostics on MSRV 1.91.**
|
||||
Mitigation: the master Phase 1 plan already prescribed this exact pattern
|
||||
(`#[trait_variant::make(MemoryStore: Send)] pub trait LocalMemoryStore:
|
||||
Sync + 'static`, see plan `0001-...` line 274-275); the crate has been in
|
||||
`vestige-core/Cargo.toml` since Phase 1 landed. If a diagnostic appears,
|
||||
pin to the exact known-good version with `trait-variant = "=0.1.2"` and
|
||||
open an upstream issue.
|
||||
- **Native async-fn-in-trait makes the trait no longer dyn-compatible.**
|
||||
Mitigation: `trait_variant::make` is specifically the workaround for this
|
||||
-- it emits both the source trait (for static dispatch) and a Send-bound
|
||||
variant whose returned futures use `Pin<Box<dyn Future + Send>>` only at
|
||||
the dyn boundary. `Arc<dyn MemoryStore>` keeps working because the
|
||||
generated `MemoryStore` trait is dyn-compatible by construction. Verified
|
||||
by the existing `send_bound_variant::*` tests, which intentionally move
|
||||
`Arc<dyn MemoryStore>` across `tokio::spawn` from inside a
|
||||
`multi_thread` runtime.
|
||||
- **A cognitive module silently relied on the boxed-future return type.**
|
||||
Mitigation: grep verified no cognitive module imports `MemoryStore` /
|
||||
`LocalMemoryStore` or holds an `Arc<dyn ...>` form; all of them use the
|
||||
concrete `Storage` alias. There is no Send-ness expectation downstream to
|
||||
break.
|
||||
- **Future bodies inside the SQLite impl capture non-Send locals.**
|
||||
Mitigation: every method body in `sqlite.rs:6274..` runs synchronous
|
||||
rusqlite calls inside the same `async fn` frame; no `.await` points
|
||||
exist inside the bodies that we ship today. The `Send` bound on the
|
||||
generated `MemoryStore` variant is therefore satisfied automatically.
|
||||
If a future change adds `.await` inside an impl method, the new
|
||||
trait_variant surface will surface that as a compile error at the call
|
||||
site, which is the correct outcome.
|
||||
- **`async-trait` crate is left declared after this sub-plan.**
|
||||
This is intentional: the embedder impl still depends on it. The
|
||||
`0001c-async-trait-sunset.md` sub-plan removes the crate after the
|
||||
embedder side is rewritten. Grep on the whole workspace returns only
|
||||
the storage and embedder files; no downstream crate pulls `async-trait`.
|
||||
|
||||
---
|
||||
|
||||
## Out-of-Band Notes
|
||||
|
||||
- This sub-plan amends `feat/storage-trait-phase1` (tip 790c0c8). The
|
||||
branch has not been opened upstream yet, so amending in place is safe;
|
||||
no force-push to a public PR.
|
||||
- The companion sub-plan `0001b-sqlite-split.md` lands after this one on
|
||||
the same branch. The trait-rewrite landing first is intentional: the
|
||||
SQLite split moves the impl block into `storage/sqlite/trait_impl.rs`,
|
||||
and it is cleaner to move a small attribute-free impl than a
|
||||
macro-decorated one.
|
||||
- The companion sub-plan `0001c-async-trait-sunset.md` lands after this
|
||||
one (order with `0001b` is independent) and finishes the
|
||||
async_trait -> trait_variant transition for the Embedder trait, then
|
||||
removes the `async-trait` crate dependency.
|
||||
- After the Phase 1 amendment sub-plans (`0001a`, `0001b`, `0001c`) land,
|
||||
the branch is reviewed and merged before Phase 2 sub-plans (`0002a-`
|
||||
through `0002i-`) begin implementation.
|
||||
732
docs/plans/0001b-sqlite-split.md
Normal file
732
docs/plans/0001b-sqlite-split.md
Normal file
|
|
@ -0,0 +1,732 @@
|
|||
# Sub-Plan 0001b: Split sqlite.rs into a sqlite/ directory
|
||||
|
||||
**Status**: Draft
|
||||
**Branch**: `feat/storage-trait-phase1` (Phase 1 amendment, PR A)
|
||||
**Depends on**: `0001a-trait-rewrite.md` (must land first; it carries the
|
||||
`trait_variant`-generated trait declaration that `trait_impl.rs` matches)
|
||||
**Related**: `docs/adr/0002-phase-2-execution.md` (D3, D6)
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`crates/vestige-core/src/storage/sqlite.rs` is the single SQLite backend file
|
||||
that Phase 1 inherited from pre-trait Vestige and then appended the
|
||||
`LocalMemoryStore` trait impl block to. The file is 8713 lines as of
|
||||
commit 790c0c8 on `feat/storage-trait-phase1`. ADR 0002 D3 decides to split
|
||||
it into a `sqlite/` directory before Phase 2 lands `postgres/` as a peer.
|
||||
Reasoning, in one paragraph:
|
||||
|
||||
The Postgres backend is going in as a directory of seven small files
|
||||
(`postgres/{mod,pool,migrations,registry,search,migrate_cli,reembed}.rs`).
|
||||
If SQLite stays as one 8K-line file alongside that, the repo says "backends
|
||||
look like one big file or seven small ones, pick a side", which forces
|
||||
every future maintainer to re-litigate the layout. Splitting now -- as
|
||||
**pure code motion**, no public-API change, no behavioural change, no
|
||||
migration -- lets both backends look the same, keeps each surface mappable
|
||||
in a single editor tab, and shrinks the diffs Phase 2 has to review.
|
||||
This sub-plan is sized as one focused implementation session.
|
||||
|
||||
The split is **private** to `storage/sqlite/`. Cognitive modules continue
|
||||
to `use crate::storage::SqliteMemoryStore`; the existing re-exports in
|
||||
`crates/vestige-core/src/storage/mod.rs` keep working without touching
|
||||
any caller. Tests stay green commit-by-commit.
|
||||
|
||||
This sub-plan depends on `0001a-trait-rewrite.md` landing first because
|
||||
`sqlite/trait_impl.rs` is the file that picks up the new trait_variant
|
||||
attribute. Doing the split first would force a second rewrite of
|
||||
`trait_impl.rs` when the trait rewrite arrives. Order matters; this is
|
||||
the cheap-to-respect ordering.
|
||||
|
||||
---
|
||||
|
||||
## Target Layout
|
||||
|
||||
Final directory after this sub-plan:
|
||||
|
||||
```
|
||||
crates/vestige-core/src/storage/sqlite/
|
||||
mod.rs -- module root: SqliteMemoryStore struct, new(),
|
||||
reader/writer locks, error types, shared helpers,
|
||||
portable-sync-related types, record types
|
||||
crud.rs -- ingest/smart_ingest/get/update/delete/purge/search-by-id
|
||||
search.rs -- fts, semantic, hybrid, time-based queries
|
||||
scheduling.rs -- FSRS state, decay, consolidation, review, promote/demote,
|
||||
suppression, gc, retention, waking tags
|
||||
graph.rs -- memory_connections (edges), subgraph, neighbors
|
||||
domain.rs -- domains/domain_scores column readers, classify stub
|
||||
(Phase 4 will expand this file)
|
||||
registry.rs -- embedding_model table, enforce_model, register_model body
|
||||
portable_sync.rs -- portable export/import/sync + merge helpers
|
||||
trait_impl.rs -- impl LocalMemoryStore for SqliteMemoryStore
|
||||
```
|
||||
|
||||
`storage/mod.rs` is unchanged in spirit: it still does `mod sqlite;` and
|
||||
`pub use sqlite::{...};` -- the only difference is that `sqlite` is now a
|
||||
directory module instead of a leaf file. The re-export list does not
|
||||
change.
|
||||
|
||||
---
|
||||
|
||||
## Current File Map (line numbers from commit 790c0c8)
|
||||
|
||||
The current `sqlite.rs` is structurally:
|
||||
|
||||
| Region | Lines | Contents |
|
||||
|--------|-------|----------|
|
||||
| Header | 1-43 | Imports, feature-gated imports |
|
||||
| Error types | 45-89 | `StorageError`, `Result`, `SmartIngestResult`, `MergeWrite` |
|
||||
| Portable sync types | 97-211 | `PortableSyncBackend` trait, `FilePortableSyncBackend` struct, `PortableSyncReport`, `PurgeReport` |
|
||||
| Constants | 233-273 | `PORTABLE_TABLES`, `PORTABLE_USER_DATA_TABLES`, `PortableMergeState`, env constants |
|
||||
| Struct decl | 287-301 | `SqliteMemoryStore` struct fields |
|
||||
| Impl block 1 | 303-3740 | Constructor + bulk of native API |
|
||||
| Record structs | 3747-3866 | `IntentionRecord`, `InsightRecord`, `ConnectionRecord`, `MemoryStateRecord`, `StateTransitionRecord`, `ConsolidationHistoryRecord`, `DreamHistoryRecord`, `Default for InsightRecord` |
|
||||
| Impl block 2 | 3868-6133 | Intentions / Insights / Connections / States / History / Backup / Portable / GC / Subgraph |
|
||||
| Impl block 3 | 6139-6272 | Trait-helper methods (`node_to_record`, `read_domain_columns`, `enforce_model`) |
|
||||
| Trait impl | 6274-7110 | `impl LocalMemoryStore for SqliteMemoryStore` |
|
||||
| Tests | 7112-8713 | `#[cfg(test)] mod tests`: native API tests + trait round-trip tests |
|
||||
|
||||
---
|
||||
|
||||
## Mapping Table
|
||||
|
||||
Every public method, every private helper, every struct, every test module
|
||||
in the current file -- with the destination file in the new layout. Line
|
||||
ranges cite the current `sqlite.rs` (commit 790c0c8 on
|
||||
`feat/storage-trait-phase1`, viewed through the
|
||||
`/home/delandtj/prppl/vestige-phase2` worktree).
|
||||
|
||||
### Header and shared types (-> `sqlite/mod.rs`)
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| Module-level `use` imports | 1-43 | `sqlite/mod.rs` | Trimmed per-file in destination; what does not fit in `mod.rs` moves with its consumers |
|
||||
| `StorageError` enum + `Result` alias | 49-71 | `sqlite/mod.rs` | Re-exported through `pub use` chain; called from every sub-module |
|
||||
| `SmartIngestResult` struct | 73-89 | `sqlite/mod.rs` | Returned by `crud::smart_ingest`; defined here because other code depends on the type |
|
||||
| `MergeWrite` enum | 91-95 | `sqlite/portable_sync.rs` | Only used by merge helpers |
|
||||
| `PortableSyncBackend` trait | 97-109 | `sqlite/portable_sync.rs` | Public trait; re-exported through `mod.rs` |
|
||||
| `FilePortableSyncBackend` struct + `impl` | 111-194 | `sqlite/portable_sync.rs` | |
|
||||
| `PortableSyncReport` struct | 196-211 | `sqlite/portable_sync.rs` | |
|
||||
| `PurgeReport` struct | 213-231 | `sqlite/crud.rs` | Returned by `purge_node` |
|
||||
| `PORTABLE_TABLES` constant | 237-254 | `sqlite/portable_sync.rs` | |
|
||||
| `PORTABLE_USER_DATA_TABLES` constant | 256-272 | `sqlite/portable_sync.rs` | |
|
||||
| `PortableMergeState` struct | 274-277 | `sqlite/portable_sync.rs` | |
|
||||
| `DATA_DIR_ENV` constant | 279 | `sqlite/mod.rs` | Read by constructor |
|
||||
| `DATABASE_FILE` constant | 280 | `sqlite/mod.rs` | Read by constructor |
|
||||
| `SqliteMemoryStore` struct decl | 282-301 | `sqlite/mod.rs` | All fields stay public-to-crate within the directory |
|
||||
|
||||
### Constructor and config (-> `sqlite/mod.rs`)
|
||||
|
||||
These are foundational; they live in `mod.rs` because every sub-module
|
||||
calls them or operates on the struct they build.
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| `fn data_dir_from_env` | 304-313 | `sqlite/mod.rs` | private helper |
|
||||
| `fn expand_tilde` | 314-332 | `sqlite/mod.rs` | private helper |
|
||||
| `fn prepare_data_dir` | 333-346 | `sqlite/mod.rs` | private helper |
|
||||
| `pub fn db_path_for_data_dir` | 347-355 | `sqlite/mod.rs` | |
|
||||
| `pub fn default_db_path` | 356-368 | `sqlite/mod.rs` | |
|
||||
| `fn configure_connection` | 369-396 | `sqlite/mod.rs` | |
|
||||
| `pub fn new` | 397-457 | `sqlite/mod.rs` | The constructor |
|
||||
| `pub fn db_path` | 458-462 | `sqlite/mod.rs` | |
|
||||
| `pub fn data_dir` | 463-467 | `sqlite/mod.rs` | |
|
||||
| `pub fn sidecar_dir` | 468-473 | `sqlite/mod.rs` | |
|
||||
| `fn load_embeddings_into_index` | 474-552 | `sqlite/mod.rs` | Called by `new`; touches vector index |
|
||||
|
||||
### CRUD: ingest, get, update, delete, purge (-> `sqlite/crud.rs`)
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| `pub fn ingest` | 553-643 | `sqlite/crud.rs` | |
|
||||
| `pub fn smart_ingest` | 644-864 | `sqlite/crud.rs` | Calls vector search via `self.semantic_search`; cross-module call resolved by impl block being on the same struct |
|
||||
| `pub fn get_node_embedding` (vector-search) | 865-887 | `sqlite/crud.rs` | embedding read for one node |
|
||||
| `pub fn get_all_embeddings` (vector-search) | 888-914 | `sqlite/crud.rs` | |
|
||||
| `pub fn get_node_embedding` (no vector-search stub) | 915-919 | `sqlite/crud.rs` | feature-gated alternative |
|
||||
| `pub fn update_node_content` | 920-951 | `sqlite/crud.rs` | |
|
||||
| `fn generate_embedding_for_node` | 952-999 | `sqlite/crud.rs` | private helper; only called by ingest and update_node_content |
|
||||
| `pub fn get_node` | 1000-1011 | `sqlite/crud.rs` | |
|
||||
| `fn parse_timestamp` | 1012-1027 | `sqlite/mod.rs` | **Shared helper**: row_to_node uses it, intention/insight rows also parse timestamps. Bump to `pub(super) fn` |
|
||||
| `fn row_to_node` | 1028-1119 | `sqlite/mod.rs` | **Shared helper**: crud reads single nodes; search.rs builds node lists from rows; scheduling.rs returns nodes for review queue. Bump to `pub(super) fn`. Static method (no `&self`) so a free function in `mod.rs` is fine |
|
||||
| `pub fn delete_node` | 1842-1869 | `sqlite/crud.rs` | |
|
||||
| `pub fn purge_node` | 1870-1987 | `sqlite/crud.rs` | |
|
||||
| `fn node_exists` | 1988-1996 | `sqlite/crud.rs` | static helper, called only by purge |
|
||||
| `fn record_sync_tombstone` | 1997-2014 | `sqlite/crud.rs` | static helper, called by delete and purge |
|
||||
| `pub fn get_all_nodes` | 2268-2291 | `sqlite/crud.rs` | bulk read |
|
||||
| `pub fn get_nodes_by_type_and_tag` | 2292-2342 | `sqlite/crud.rs` | bulk read |
|
||||
|
||||
### Search: fts, semantic, hybrid, temporal (-> `sqlite/search.rs`)
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| `pub fn recall` | 1120-1147 | `sqlite/search.rs` | top-level recall path |
|
||||
| `fn keyword_search` | 1148-1180 | `sqlite/search.rs` | private |
|
||||
| `pub fn search` | 2015-2043 | `sqlite/search.rs` | |
|
||||
| `pub fn search_terms` | 2044-2075 | `sqlite/search.rs` | |
|
||||
| `pub fn concrete_search_filtered` | 2076-2172 | `sqlite/search.rs` | |
|
||||
| `fn upsert_concrete_result` | 2173-2197 | `sqlite/search.rs` | static helper |
|
||||
| `fn normalize_literal_query` | 2198-2210 | `sqlite/search.rs` | static helper |
|
||||
| `fn escape_like` | 2211-2224 | `sqlite/search.rs` | static helper |
|
||||
| `fn literal_match_score` | 2225-2248 | `sqlite/search.rs` | static helper |
|
||||
| `fn node_matches_type_filters` | 2249-2267 | `sqlite/search.rs` | static helper |
|
||||
| `pub fn is_embedding_ready` (both feature variants) | 2343-2354 | `sqlite/search.rs` | both versions move together |
|
||||
| `pub fn init_embeddings` (both feature variants) | 2355-2367 | `sqlite/search.rs` | both versions move together |
|
||||
| `fn get_query_embedding` | 2368-2400 | `sqlite/search.rs` | private; uses `query_cache` field |
|
||||
| `pub fn semantic_search` | 2401-2434 | `sqlite/search.rs` | |
|
||||
| `pub fn hybrid_search` (feature on) | 2435-2452 | `sqlite/search.rs` | |
|
||||
| `pub fn hybrid_search_filtered` (feature on) | 2453-2581 | `sqlite/search.rs` | |
|
||||
| `pub fn hybrid_search` (feature off) | 2582-2593 | `sqlite/search.rs` | feature-gated stub |
|
||||
| `pub fn hybrid_search_filtered` (feature off) | 2594-2635 | `sqlite/search.rs` | feature-gated stub |
|
||||
| `fn keyword_search_with_scores` | 2636-2726 | `sqlite/search.rs` | |
|
||||
| `fn semantic_search_raw` | 2727-2765 | `sqlite/search.rs` | |
|
||||
| `pub fn generate_embeddings` | 2766-2819 | `sqlite/search.rs` | populates embeddings post hoc |
|
||||
| `fn embedding_regeneration_candidates` | 2820-2891 | `sqlite/search.rs` | called by generate_embeddings |
|
||||
| `pub fn query_at_time` | 2892-2933 | `sqlite/search.rs` | temporal query |
|
||||
| `pub fn query_time_range` | 2934-3005 | `sqlite/search.rs` | temporal query |
|
||||
| `fn embedding_model_matches_active` (associated fn) | 5652-5671 | `sqlite/search.rs` | static helper for hybrid_search; promoted to `pub(super)` (test references it) |
|
||||
| `fn embedding_model_supports_matryoshka` | 5672-5677 | `sqlite/search.rs` | static helper |
|
||||
| `fn embedding_vector_for_active_model` | 5678-5697 | `sqlite/search.rs` | static helper |
|
||||
| `fn active_embedding_model_like_pattern` | 5698-5713 | `sqlite/search.rs` | static helper |
|
||||
|
||||
### Scheduling: FSRS, decay, consolidation, review, promote/demote, suppression, gc, retention (-> `sqlite/scheduling.rs`)
|
||||
|
||||
This is the busiest destination file. The grouping rule is: anything that
|
||||
touches FSRS scheduling fields (`stability`, `difficulty`, `retrievability`,
|
||||
`reps`, `lapses`, `retention_strength`, `retrieval_strength`) or the
|
||||
review/decay/consolidation/gc lifecycle lives here.
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| `pub fn mark_reviewed` | 1181-1275 | `sqlite/scheduling.rs` | FSRS state mutation |
|
||||
| `pub fn strengthen_on_access` | 1276-1344 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn strengthen_batch_on_access` | 1345-1357 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn mark_memory_useful` | 1358-1377 | `sqlite/scheduling.rs` | |
|
||||
| `fn log_access` | 1378-1393 | `sqlite/scheduling.rs` | private |
|
||||
| `pub fn promote_memory` | 1394-1425 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn demote_memory` | 1426-1472 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn suppress_memory` | 1473-1504 | `sqlite/scheduling.rs` | active forgetting |
|
||||
| `pub fn reverse_suppression` | 1505-1552 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn count_suppressed` | 1553-1567 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn get_recently_suppressed` | 1568-1594 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn apply_rac1_cascade` | 1595-1641 | `sqlite/scheduling.rs` | active forgetting cascade |
|
||||
| `pub fn run_rac1_cascade_sweep` | 1642-1657 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn get_review_queue` | 1658-1681 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn preview_review` | 1682-1712 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn get_stats` | 1713-1841 | `sqlite/scheduling.rs` | reports retention/lapses/review counts; lives here for symmetry with the FSRS reporters next door |
|
||||
| `pub fn apply_decay` | 3006-3095 | `sqlite/scheduling.rs` | core decay loop |
|
||||
| `fn get_fsrs_w20` | 3096-3119 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn run_consolidation` | 3120-3407 | `sqlite/scheduling.rs` | |
|
||||
| `fn auto_dedup_consolidation` | 3408-3538 | `sqlite/scheduling.rs` | called by run_consolidation |
|
||||
| `fn compute_act_r_activations` | 3539-3605 | `sqlite/scheduling.rs` | called by run_consolidation |
|
||||
| `fn prune_access_log` | 3606-3620 | `sqlite/scheduling.rs` | called by run_consolidation |
|
||||
| `fn optimize_w20_if_ready` | 3621-3720 | `sqlite/scheduling.rs` | called by run_consolidation |
|
||||
| `fn generate_missing_embeddings` | 3721-3740 | `sqlite/scheduling.rs` | called by run_consolidation |
|
||||
| `pub fn get_state_transitions` | 5714-5748 | `sqlite/scheduling.rs` | audit trail tied to scheduling state |
|
||||
| `pub fn get_avg_retention` | 5780-5792 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn get_retention_distribution` | 5794-5825 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn get_retention_trend` | 5826-5858 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn save_retention_snapshot` | 5859-5878 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn count_memories_below_retention` | 5879-5892 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn gc_below_retention` | 5893-5936 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn auto_promote_frequent_access` | 5937-5985 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn set_waking_tag` | 5986-5998 | `sqlite/scheduling.rs` | waking SWR tagging |
|
||||
| `pub fn clear_waking_tags` | 5999-6011 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn get_waking_tagged_memories` | 6012-6028 | `sqlite/scheduling.rs` | |
|
||||
| `pub fn get_recent_state_transitions` | 6105-6132 | `sqlite/scheduling.rs` | |
|
||||
|
||||
### Graph: edges (memory_connections), neighbors, subgraph (-> `sqlite/graph.rs`)
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| `pub fn save_connection` | 4180-4202 | `sqlite/graph.rs` | |
|
||||
| `pub fn get_connections_for_memory` | 4203-4220 | `sqlite/graph.rs` | |
|
||||
| `pub fn get_all_connections` | 4221-4236 | `sqlite/graph.rs` | |
|
||||
| `pub fn strengthen_connection` | 4237-4259 | `sqlite/graph.rs` | |
|
||||
| `pub fn apply_connection_decay` | 4260-4272 | `sqlite/graph.rs` | |
|
||||
| `pub fn prune_weak_connections` | 4273-4284 | `sqlite/graph.rs` | |
|
||||
| `fn row_to_connection` | 4285-4305 | `sqlite/graph.rs` | private |
|
||||
| `pub fn get_most_connected_memory` | 6029-6046 | `sqlite/graph.rs` | |
|
||||
| `pub fn get_memory_subgraph` | 6048-6103 | `sqlite/graph.rs` | calls `get_connections_for_memory`, `get_node`, `get_all_connections` -- all resolvable through `self` |
|
||||
|
||||
### Domain (-> `sqlite/domain.rs`)
|
||||
|
||||
Phase 1 keeps domain logic to JSON-column reads + classify stub. Phase 4
|
||||
expands this file. Keeping the file in the split so Phase 4 has an
|
||||
obvious place to add to.
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| `fn read_domain_columns` | 6167-6196 | `sqlite/domain.rs` | private helper used by trait `get`. Bump to `pub(super)` |
|
||||
|
||||
The trait methods `list_domains` / `get_domain` / `upsert_domain` /
|
||||
`delete_domain` / `classify` live in `sqlite/trait_impl.rs`; they
|
||||
delegate to thin helpers that, in Phase 1, are essentially noops or
|
||||
JSON reads. Phase 4 will move the substance of those methods into
|
||||
`sqlite/domain.rs` as real functions.
|
||||
|
||||
### Registry: embedding_model table (-> `sqlite/registry.rs`)
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| `fn enforce_model` | 6203-6272 | `sqlite/registry.rs` | private helper used by trait `insert` and `update`. Bump to `pub(super)` |
|
||||
|
||||
The trait methods `registered_model` and `register_model` themselves
|
||||
live in `sqlite/trait_impl.rs`. Phase 2's `postgres/registry.rs` will
|
||||
mirror this layout.
|
||||
|
||||
### Intentions, Insights, Memory States, Consolidation History, Dream History, Backup (-> `sqlite/mod.rs`)
|
||||
|
||||
These were tacked onto `SqliteMemoryStore` over time as the cognitive
|
||||
modules needed somewhere to persist their state. They are not part of the
|
||||
trait surface, they are not naturally grouped with crud/search/scheduling,
|
||||
and they are each fairly small and self-contained. They live in `mod.rs`
|
||||
under labelled sections (one big impl block can carry them) rather than
|
||||
inventing extra files like `intentions.rs` and `insights.rs`. Postgres
|
||||
will mirror this once Phase 5 picks up the work; for now they have no
|
||||
peer.
|
||||
|
||||
Rationale: every one of these methods writes to a single table, the
|
||||
bodies are short, and grouping them next to the constructor preserves
|
||||
"open `mod.rs` to see the whole struct" as the navigation default.
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| `IntentionRecord` struct | 3747-3766 | `sqlite/mod.rs` | re-exported through `storage/mod.rs` |
|
||||
| `InsightRecord` struct + `Default` | 3767-3797 | `sqlite/mod.rs` | re-exported |
|
||||
| `ConnectionRecord` struct | 3799-3809 | `sqlite/mod.rs` | re-exported; consumed by `graph.rs` |
|
||||
| `MemoryStateRecord` struct | 3811-3821 | `sqlite/mod.rs` | |
|
||||
| `StateTransitionRecord` struct | 3823-3833 | `sqlite/mod.rs` | re-exported |
|
||||
| `ConsolidationHistoryRecord` struct | 3835-3846 | `sqlite/mod.rs` | |
|
||||
| `DreamHistoryRecord` struct | 3848-3866 | `sqlite/mod.rs` | re-exported |
|
||||
| `pub fn save_intention` etc. (intention block) | 3874-4058 | `sqlite/mod.rs` | one impl block, in-section labelled |
|
||||
| `fn row_to_intention` | 4023-4058 | `sqlite/mod.rs` | private |
|
||||
| insights block (`save_insight`, `get_insights`, etc.) | 4065-4174 | `sqlite/mod.rs` | |
|
||||
| `fn row_to_insight` | 4153-4173 | `sqlite/mod.rs` | private |
|
||||
| memory-state block | 4306-4459 | `sqlite/mod.rs` | |
|
||||
| `fn row_to_memory_state` | 4431-4459 | `sqlite/mod.rs` | private |
|
||||
| consolidation-history block | 4465-4540 | `sqlite/mod.rs` | |
|
||||
| dream-history block | 4546-4638 | `sqlite/mod.rs` | |
|
||||
| `pub fn count_memories_since` | 4639-4651 | `sqlite/mod.rs` | |
|
||||
| `fn scan_last_backup_timestamp` | 4652-4682 | `sqlite/mod.rs` | |
|
||||
| `pub fn last_backup_timestamp` | 4683-4688 | `sqlite/mod.rs` | |
|
||||
| `pub fn get_last_backup_timestamp` (associated) | 4689-4696 | `sqlite/mod.rs` | |
|
||||
| `pub fn backup_to` | 5749-5774 | `sqlite/mod.rs` | sqlite VACUUM INTO; called by backup tool |
|
||||
|
||||
### Portable export/import/sync (-> `sqlite/portable_sync.rs`)
|
||||
|
||||
This is the second-largest destination after `scheduling.rs` and the most
|
||||
self-contained.
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| `pub fn export_portable_archive` | 4699-4755 | `sqlite/portable_sync.rs` | |
|
||||
| `pub fn export_portable_archive_to_path` | 4756-4806 | `sqlite/portable_sync.rs` | |
|
||||
| `pub fn import_portable_archive` | 4807-4978 | `sqlite/portable_sync.rs` | |
|
||||
| `pub fn import_portable_archive_from_path` | 4979-4996 | `sqlite/portable_sync.rs` | |
|
||||
| `pub fn sync_portable_archive` (generic over backend) | 4997-5025 | `sqlite/portable_sync.rs` | |
|
||||
| `pub fn sync_portable_archive_file` | 5026-5030 | `sqlite/portable_sync.rs` | |
|
||||
| `fn merge_portable_table` | 5031-5073 | `sqlite/portable_sync.rs` | |
|
||||
| `fn merge_knowledge_nodes` | 5074-5126 | `sqlite/portable_sync.rs` | |
|
||||
| `fn merge_sync_tombstones` | 5127-5204 | `sqlite/portable_sync.rs` | |
|
||||
| `fn merge_deletion_tombstones` | 5205-5245 | `sqlite/portable_sync.rs` | |
|
||||
| `fn merge_keyed_table` | 5246-5281 | `sqlite/portable_sync.rs` | |
|
||||
| `fn row_references_locally_newer_node` | 5282-5302 | `sqlite/portable_sync.rs` | |
|
||||
| `fn merge_append_only_table` | 5303-5338 | `sqlite/portable_sync.rs` | |
|
||||
| `fn parent_rows_exist` | 5339-5370 | `sqlite/portable_sync.rs` | |
|
||||
| `fn insert_or_replace_row` | 5371-5386 | `sqlite/portable_sync.rs` | |
|
||||
| `fn merge_key_columns` | 5387-5398 | `sqlite/portable_sync.rs` | |
|
||||
| `fn upsert_row_with_columns` | 5399-5446 | `sqlite/portable_sync.rs` | |
|
||||
| `fn insert_row_with_columns` | 5447-5469 | `sqlite/portable_sync.rs` | |
|
||||
| `fn merge_row_exists` | 5470-5487 | `sqlite/portable_sync.rs` | |
|
||||
| `fn row_exists_by_values` | 5488-5507 | `sqlite/portable_sync.rs` | |
|
||||
| `fn row_values_for_columns` | 5508-5528 | `sqlite/portable_sync.rs` | |
|
||||
| `fn portable_value` | 5529-5540 | `sqlite/portable_sync.rs` | |
|
||||
| `fn portable_text` | 5541-5551 | `sqlite/portable_sync.rs` | |
|
||||
| `fn portable_timestamp` | 5552-5559 | `sqlite/portable_sync.rs` | |
|
||||
| `fn parse_rfc3339_opt` | 5560-5565 | `sqlite/portable_sync.rs` | |
|
||||
| `fn tombstone_timestamp` | 5566-5580 | `sqlite/portable_sync.rs` | |
|
||||
| `fn current_schema_version` | 5581-5589 | `sqlite/portable_sync.rs` | static helper |
|
||||
| `fn ensure_portable_import_target_empty` | 5590-5604 | `sqlite/portable_sync.rs` | static helper |
|
||||
| `fn table_exists` | 5605-5613 | `sqlite/portable_sync.rs` | static helper |
|
||||
| `fn table_row_count` | 5614-5618 | `sqlite/portable_sync.rs` | static helper |
|
||||
| `fn table_columns` | 5619-5630 | `sqlite/portable_sync.rs` | static helper |
|
||||
| `fn portable_value_from_ref` | 5631-5646 | `sqlite/portable_sync.rs` | static helper |
|
||||
| `fn quote_ident` | 5647-5651 | `sqlite/portable_sync.rs` | static helper |
|
||||
|
||||
### Trait helpers and trait impl (-> `sqlite/trait_impl.rs`)
|
||||
|
||||
| Item | Lines | Destination | Notes |
|
||||
|------|-------|-------------|-------|
|
||||
| `fn node_to_record` | 6142-6164 | `sqlite/trait_impl.rs` | associated fn used only by trait body; co-locate |
|
||||
| `impl LocalMemoryStore for SqliteMemoryStore` block | 6274-7110 | `sqlite/trait_impl.rs` | full trait impl; attribute changes from `#[async_trait::async_trait]` to whatever 0001a settles on (`#[trait_variant::make(...)]` is on the trait declaration; the impl block carries no attribute under trait_variant) |
|
||||
|
||||
### Tests
|
||||
|
||||
The current `#[cfg(test)] mod tests` block at lines 7112-8713 contains
|
||||
**two** distinct test families:
|
||||
|
||||
1. **Native API tests** (7120-8198): unit tests against the legacy
|
||||
`pub fn` surface (`test_ingest_and_get`, `test_search`, `test_review`,
|
||||
`test_delete`, `test_dream_history_save_and_get_last`,
|
||||
`test_portable_archive_exact_round_trip`, `test_keyword_search_*`,
|
||||
`test_concrete_search_*`, `test_purge_*`, etc.).
|
||||
2. **Trait round-trip tests** (8200-8712, after the
|
||||
`// ===== Phase 1: LocalMemoryStore trait round-trip tests =====`
|
||||
banner): `trait_init_is_idempotent`, `trait_register_model_*`,
|
||||
`trait_insert_*`, `trait_get_*`, `trait_update_*`, `trait_delete_*`,
|
||||
`trait_fts_search_*`, `trait_hybrid_search_*`,
|
||||
`trait_scheduling_*`, `trait_add_edge_*`, `trait_get_edges_*`,
|
||||
`trait_remove_edge_*`, `trait_get_neighbors_*`, `trait_list_domains_*`,
|
||||
`trait_upsert_*`, `trait_classify_*`, `trait_count_*`,
|
||||
`trait_get_stats_*`, `trait_vacuum_*`,
|
||||
`trait_insert_refuses_dimension_mismatch`.
|
||||
|
||||
See the Test Relocation section below for the resolution.
|
||||
|
||||
---
|
||||
|
||||
## Visibility Changes
|
||||
|
||||
The split moves items into sibling files inside one module. Helpers that
|
||||
were `fn ...` (i.e. crate-private but file-private under the current
|
||||
layout, since the file *is* the module) need their visibility lifted
|
||||
just enough that sibling files can call them. The principle is: smallest
|
||||
bump that makes the call site compile.
|
||||
|
||||
`pub(super)` is sufficient for everything below; nothing needs
|
||||
`pub(crate)`. The trait `LocalMemoryStore` exposure does not change --
|
||||
sub-modules call `self.method(...)` on `SqliteMemoryStore`, which
|
||||
resolves through the impl blocks defined in their own files; visibility
|
||||
is automatic at impl-block scope.
|
||||
|
||||
Items that need a visibility bump (currently private fn, become
|
||||
`pub(super) fn`):
|
||||
|
||||
- `parse_timestamp` (1012): called by `row_to_node` and by intention /
|
||||
insight row mappers.
|
||||
- `row_to_node` (1028): called by `crud.rs`, `search.rs`,
|
||||
`scheduling.rs`. Static associated fn.
|
||||
- `read_domain_columns` (6167): called by `trait_impl.rs`.
|
||||
- `enforce_model` (6203): called by `trait_impl.rs`.
|
||||
- `embedding_model_matches_active` (5652): currently called by
|
||||
`hybrid_search_filtered`; tests also reference it. Has to remain
|
||||
`pub(super) fn` and be `pub` only if the existing test names reach it
|
||||
through a re-export. (See Test Relocation.)
|
||||
- `embedding_model_supports_matryoshka` (5672): private; only callers in
|
||||
`search.rs` after the move; stays `fn` (no bump needed).
|
||||
- `embedding_vector_for_active_model` (5678): same as the matches
|
||||
function -- a test references it. Bump to `pub(super)`.
|
||||
- `active_embedding_model_like_pattern` (5698): private; only used by
|
||||
search; stays `fn`.
|
||||
- `generate_embedding_for_node` (952): currently called by `ingest` and
|
||||
`update_node_content`. Both move to `crud.rs`; stays `fn`.
|
||||
- `get_query_embedding` (2368): only used inside `search.rs`; stays `fn`.
|
||||
- `keyword_search_with_scores` (2636): only used inside `search.rs`;
|
||||
stays `fn`.
|
||||
- `semantic_search_raw` (2727): only used inside `search.rs`; stays `fn`.
|
||||
- `embedding_regeneration_candidates` (2820): used by
|
||||
`generate_embeddings`; both move to `search.rs`; stays `fn`. The
|
||||
existing test (line 7167) references it through `storage.method()`,
|
||||
which will continue to work because the test file can move with it.
|
||||
- `log_access` (1378): private to `scheduling.rs`; stays `fn`.
|
||||
- All the `auto_dedup_consolidation` / `compute_act_r_activations` /
|
||||
`prune_access_log` / `optimize_w20_if_ready` /
|
||||
`generate_missing_embeddings` helpers (3408-3740): private to
|
||||
`scheduling.rs`; stays `fn`.
|
||||
- `row_to_intention` / `row_to_insight` / `row_to_memory_state` /
|
||||
`row_to_connection`: all stay private in their destination file (only
|
||||
one caller each).
|
||||
- All `merge_*` / `portable_*` / `parse_rfc3339_opt` / `quote_ident`:
|
||||
private to `portable_sync.rs`; stays `fn`.
|
||||
- `node_exists` (1988): private to `crud.rs`; stays `fn`.
|
||||
- `record_sync_tombstone` (1997): private to `crud.rs`; stays `fn`.
|
||||
- `get_fsrs_w20` (3096): private to `scheduling.rs`; stays `fn`.
|
||||
|
||||
Items already `pub fn` (or `pub(crate) fn`) stay as they are -- no
|
||||
visibility regression.
|
||||
|
||||
Field visibility on `SqliteMemoryStore` itself: currently all fields are
|
||||
private. The sub-modules access them via `self.field`. Because impl
|
||||
blocks for `SqliteMemoryStore` are written in sibling files of the same
|
||||
module, `self.field` reaches private fields without a visibility bump.
|
||||
**No field visibility changes are required.** Confirm this during the
|
||||
first motion commit; if Rust disagrees, mark the relevant fields
|
||||
`pub(super)` and document in the commit message.
|
||||
|
||||
---
|
||||
|
||||
## Public Re-exports
|
||||
|
||||
`crates/vestige-core/src/storage/mod.rs` currently exports:
|
||||
|
||||
```rust
|
||||
mod memory_store;
|
||||
mod migrations;
|
||||
mod portable;
|
||||
mod sqlite;
|
||||
|
||||
pub use memory_store::{...};
|
||||
pub use migrations::MIGRATIONS;
|
||||
pub use portable::{...};
|
||||
pub use sqlite::{
|
||||
ConnectionRecord, ConsolidationHistoryRecord, DreamHistoryRecord, FilePortableSyncBackend,
|
||||
InsightRecord, IntentionRecord, PortableSyncBackend, PortableSyncReport, Result,
|
||||
SmartIngestResult, SqliteMemoryStore, StateTransitionRecord, StorageError,
|
||||
};
|
||||
|
||||
pub type Storage = SqliteMemoryStore;
|
||||
```
|
||||
|
||||
After the split, `mod sqlite;` resolves to the new directory module
|
||||
(`storage/sqlite/mod.rs`). The `pub use sqlite::{...}` block resolves
|
||||
against the items re-exported by `storage/sqlite/mod.rs`.
|
||||
|
||||
`storage/sqlite/mod.rs` therefore needs the same names visible at its
|
||||
top level. Add at the end of `mod.rs`:
|
||||
|
||||
```rust
|
||||
mod crud;
|
||||
mod search;
|
||||
mod scheduling;
|
||||
mod graph;
|
||||
mod domain;
|
||||
mod registry;
|
||||
mod portable_sync;
|
||||
mod trait_impl;
|
||||
|
||||
pub use portable_sync::{FilePortableSyncBackend, PortableSyncBackend, PortableSyncReport};
|
||||
// SqliteMemoryStore, StorageError, Result, SmartIngestResult, IntentionRecord,
|
||||
// InsightRecord, ConnectionRecord, StateTransitionRecord,
|
||||
// ConsolidationHistoryRecord, DreamHistoryRecord are defined in mod.rs itself,
|
||||
// so they are already in scope and do not need a re-export.
|
||||
```
|
||||
|
||||
The `crates/vestige-core/src/storage/mod.rs` file does not change. The
|
||||
`pub type Storage = SqliteMemoryStore;` alias keeps working.
|
||||
|
||||
If `cargo build` complains that `storage/mod.rs` cannot resolve a name
|
||||
in its `pub use sqlite::{...}` block, the fix is to add the missing name
|
||||
to `sqlite/mod.rs`'s re-export tail; no change to `storage/mod.rs`.
|
||||
|
||||
---
|
||||
|
||||
## Test Relocation
|
||||
|
||||
Two test families, two destinations.
|
||||
|
||||
**Native API tests** (current lines 7120-8198) cover the legacy `pub fn`
|
||||
surface. They live close to their subject:
|
||||
|
||||
- Tests that touch the constructor, common helpers, and shared setup
|
||||
(`create_test_storage`, `create_test_storage_at`,
|
||||
`test_storage_creation`, `test_get_last_backup_timestamp_no_panic`)
|
||||
move to `sqlite/mod.rs` in a `#[cfg(test)] mod tests` block.
|
||||
- `test_ingest_and_get`, `test_delete`,
|
||||
`test_purge_scrubs_insight_json_orphans_children_and_writes_tombstone`
|
||||
go to `sqlite/crud.rs` as a `#[cfg(test)] mod tests` block.
|
||||
- `test_search`, `test_keyword_search_with_include_types`,
|
||||
`test_keyword_search_with_exclude_types`,
|
||||
`test_include_types_takes_precedence_over_exclude`,
|
||||
`test_type_filter_with_no_matches_returns_empty`,
|
||||
`test_hybrid_search_backward_compat`,
|
||||
`test_concrete_search_literal_identifier_lands_first`,
|
||||
`test_embedding_model_family_matching`,
|
||||
`test_embedding_regeneration_candidates_include_entire_mismatched_corpus`
|
||||
go to `sqlite/search.rs`.
|
||||
- `test_review` goes to `sqlite/scheduling.rs`.
|
||||
- `test_dream_history_save_and_get_last`, `test_dream_history_empty`,
|
||||
`test_count_memories_since` go to `sqlite/mod.rs` (history tables live
|
||||
there).
|
||||
- All `test_portable_*` go to `sqlite/portable_sync.rs`.
|
||||
- `test_file_portable_sync_round_trips_between_devices` goes to
|
||||
`sqlite/portable_sync.rs`.
|
||||
|
||||
**Trait round-trip tests** (current lines 8200-8712) test the
|
||||
`LocalMemoryStore` trait impl. Two viable layouts:
|
||||
|
||||
A. Co-locate with the impl in `sqlite/trait_impl.rs` (one big
|
||||
`#[cfg(test)] mod trait_tests`).
|
||||
B. Keep them as a single `tests.rs` file in the sqlite directory.
|
||||
|
||||
**Decision: A.** Co-locate. The trait round-trip tests are explicitly
|
||||
testing the `impl LocalMemoryStore for SqliteMemoryStore` block;
|
||||
co-location means a reader who edits the trait impl sees its tests in
|
||||
the same file. Option B would mean two places to look every time a
|
||||
trait method changes shape. For an 8K-line collapse the tradeoff
|
||||
favours co-location.
|
||||
|
||||
Concretely: `sqlite/trait_impl.rs` ends with a
|
||||
`#[cfg(test)] mod trait_tests { ... }` block that contains all 30+
|
||||
`trait_*` tests, plus the shared `make_record`, `rt`, and small helpers
|
||||
defined inside the current test mod for trait tests (lines 8208-8226).
|
||||
|
||||
---
|
||||
|
||||
## Commit Sequence
|
||||
|
||||
Each commit moves one logical group. After each commit:
|
||||
|
||||
```
|
||||
cargo build -p vestige-core
|
||||
cargo test -p vestige-core
|
||||
cargo clippy -p vestige-core -- -D warnings
|
||||
```
|
||||
|
||||
must pass. Order is chosen so that each move is small, the next move
|
||||
does not depend on the previous having grown surprising visibility, and
|
||||
the largest / riskiest move (the trait impl, with the new
|
||||
trait_variant attribute) lands last.
|
||||
|
||||
| # | Commit | What moves | Tests touched |
|
||||
|---|--------|-----------|----------------|
|
||||
| 1 | `refactor(sqlite): scaffold sqlite/ directory` | Convert `sqlite.rs` -> `sqlite/mod.rs` verbatim (rename + create empty sibling files `crud.rs`, `search.rs`, `scheduling.rs`, `graph.rs`, `domain.rs`, `registry.rs`, `portable_sync.rs`, `trait_impl.rs` each with `use super::*;`). At this point `mod.rs` declares the new modules but they are empty. | None move. Build proves the rename works. |
|
||||
| 2 | `refactor(sqlite): split out portable sync` | Move all `merge_*`, `portable_*`, `export_*`, `import_*`, `sync_*` items + `MergeWrite`, `PortableSyncBackend`, `FilePortableSyncBackend`, `PortableSyncReport`, `PortableMergeState`, `PORTABLE_TABLES`, `PORTABLE_USER_DATA_TABLES`, helper statics into `sqlite/portable_sync.rs`. Add `pub use portable_sync::{...}` in `mod.rs` for the public types. | `test_portable_*` and `test_file_portable_sync_round_trips_between_devices` move too. |
|
||||
| 3 | `refactor(sqlite): split out graph / connections` | Move `save_connection`, `get_connections_for_memory`, `get_all_connections`, `strengthen_connection`, `apply_connection_decay`, `prune_weak_connections`, `row_to_connection`, `get_most_connected_memory`, `get_memory_subgraph` to `sqlite/graph.rs`. | None move (no native graph tests; trait edge tests still in trait_tests). |
|
||||
| 4 | `refactor(sqlite): split out scheduling / fsrs / consolidation` | Move all items listed in the Scheduling row to `sqlite/scheduling.rs`. | `test_review` moves. |
|
||||
| 5 | `refactor(sqlite): split out search / fts / semantic / hybrid` | Move all items listed in the Search row to `sqlite/search.rs`. Add `pub(super)` to the four `embedding_model_*` helpers that tests reference. | `test_search`, `test_keyword_search_*`, `test_include_types_*`, `test_type_filter_*`, `test_hybrid_search_*`, `test_concrete_search_*`, `test_embedding_model_family_matching`, `test_embedding_regeneration_candidates_include_entire_mismatched_corpus` move. |
|
||||
| 6 | `refactor(sqlite): split out crud / ingest / get / update / delete / purge` | Move `ingest`, `smart_ingest`, `get_node`, `update_node_content`, `delete_node`, `purge_node`, `get_all_nodes`, `get_nodes_by_type_and_tag`, `node_exists`, `record_sync_tombstone`, `generate_embedding_for_node`, `get_node_embedding`, `get_all_embeddings`, `PurgeReport` to `sqlite/crud.rs`. Bump `row_to_node` and `parse_timestamp` to `pub(super) fn` in `mod.rs`. | `test_ingest_and_get`, `test_delete`, `test_purge_scrubs_insight_json_orphans_children_and_writes_tombstone` move. |
|
||||
| 7 | `refactor(sqlite): split out registry helper` | Move `enforce_model` to `sqlite/registry.rs`, bumped to `pub(super)`. | None move. |
|
||||
| 8 | `refactor(sqlite): split out domain helper` | Move `read_domain_columns` to `sqlite/domain.rs`, bumped to `pub(super)`. | None move. |
|
||||
| 9 | `refactor(sqlite): split out trait impl + tests` | Move `node_to_record` and the full `impl LocalMemoryStore for SqliteMemoryStore` block to `sqlite/trait_impl.rs`. Move the entire trait round-trip test module (lines 8200-8712, including `make_record` and `rt` helpers) to a `#[cfg(test)] mod trait_tests` block at the bottom of `trait_impl.rs`. This is the commit where the trait_variant attribute (from sub-plan 0001a) is observed: the impl block on `SqliteMemoryStore` uses whatever syntax the rewritten trait expects (no `#[async_trait::async_trait]`). | All `trait_*` tests move. |
|
||||
|
||||
Commit 1 is the only commit that creates new files; the rest move
|
||||
existing code into them. Reviewers can bisect through this list to
|
||||
find any silent-semantic change.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Run after every commit. All three must pass before pushing:
|
||||
|
||||
```
|
||||
cargo build -p vestige-core
|
||||
cargo test -p vestige-core
|
||||
cargo clippy -p vestige-core -- -D warnings
|
||||
```
|
||||
|
||||
The Phase 1 amendment branch must also build with the no-default-features
|
||||
configuration that the release binary uses for the alternative feature
|
||||
set:
|
||||
|
||||
```
|
||||
cargo build -p vestige-core --no-default-features
|
||||
cargo test -p vestige-core --no-default-features
|
||||
```
|
||||
|
||||
Some of the methods being moved (`get_node_embedding`, `is_embedding_ready`,
|
||||
`init_embeddings`, the feature-on/feature-off `hybrid_search` pair) have
|
||||
two definitions guarded by feature flags. The split must preserve both
|
||||
copies in the same destination file with their existing `#[cfg(...)]`
|
||||
attributes; the no-default-features build confirms.
|
||||
|
||||
After the last commit, run the workspace-wide check that Phase 1 promised:
|
||||
|
||||
```
|
||||
cargo build --workspace
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
This catches downstream consumers (`vestige-mcp`, `vestige`,
|
||||
`vestige-restore`) that might depend on a specific module path (they
|
||||
should not -- they import from `crate::storage::SqliteMemoryStore` and
|
||||
the re-exports in `storage/mod.rs` -- but the workspace build is the
|
||||
authoritative confirmation).
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `crates/vestige-core/src/storage/sqlite.rs` no longer exists. In its
|
||||
place is `crates/vestige-core/src/storage/sqlite/` with the eight
|
||||
files listed in the Target Layout section, each below 2000 lines.
|
||||
2. `crates/vestige-core/src/storage/mod.rs` is unchanged (or
|
||||
functionally unchanged -- the `pub use sqlite::{...}` block contains
|
||||
the same identifiers in the same order).
|
||||
3. Every cognitive module and binary in the workspace
|
||||
(`vestige-core`, `vestige-mcp`, `vestige`, `vestige-restore`)
|
||||
compiles with no source edits other than the ones in
|
||||
`crates/vestige-core/src/storage/sqlite/`.
|
||||
4. `cargo build -p vestige-core`,
|
||||
`cargo test -p vestige-core`,
|
||||
`cargo clippy -p vestige-core -- -D warnings`,
|
||||
`cargo build -p vestige-core --no-default-features`, and
|
||||
`cargo test -p vestige-core --no-default-features` all pass at the
|
||||
end of every commit in the sequence (bisectability).
|
||||
5. `cargo test --workspace` matches the Phase 1 baseline test count
|
||||
(758 tests, of which 352 are in `vestige-core`). No new tests are
|
||||
added by this sub-plan; no existing test is renamed or deleted.
|
||||
6. The on-disk SQLite schema is unchanged. A live database created on
|
||||
the pre-split build opens cleanly on the post-split build and round
|
||||
trips a memory.
|
||||
7. `git log --follow` works for at least one method in each destination
|
||||
file (i.e. `git mv` was used where the line range constitutes most
|
||||
of the file content of the destination, otherwise a `git log -p`
|
||||
on the new file shows the history before the rename through the
|
||||
block-move detection that recent `git log` versions support).
|
||||
8. No public symbol disappears from `crate::storage::*`. A reviewer can
|
||||
verify with:
|
||||
```
|
||||
cargo doc -p vestige-core --no-deps
|
||||
```
|
||||
before and after the split, and `diff` the generated
|
||||
`target/doc/vestige_core/storage/index.html` lists.
|
||||
|
||||
---
|
||||
|
||||
## Non-Goals (explicit)
|
||||
|
||||
- No public API change. The trait surface (`LocalMemoryStore`,
|
||||
`MemoryStore`), the legacy `pub fn` surface on `SqliteMemoryStore`,
|
||||
the re-exports through `storage/mod.rs`, and the `pub type Storage =
|
||||
SqliteMemoryStore;` alias are all preserved.
|
||||
- No behavioural change. No SQL is rewritten, no FSRS parameter is
|
||||
retuned, no embedding model is touched, no migration is added.
|
||||
- No new tests. Tests move with their subject; no new tests are
|
||||
written.
|
||||
- No clippy fix-ups that pre-date this sub-plan. If `cargo clippy
|
||||
-- -D warnings` was passing before the split, it must continue to
|
||||
pass; if it was not passing, the failures stay where they are and
|
||||
are addressed in a separate commit (out of scope here).
|
||||
- No removal of the `pub type Storage = SqliteMemoryStore;` BC alias.
|
||||
That happens at the end of Phase 4 per ADR 0001.
|
||||
- No reorganisation of `storage/memory_store.rs`,
|
||||
`storage/migrations.rs`, or `storage/portable.rs`. Those files are
|
||||
out of scope; the split is private to `storage/sqlite/`.
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Silent semantic change introduced by a motion commit | Per-commit `cargo test -p vestige-core` keeps the bisect window to a single commit. Reviewer bisects with `cargo test -p vestige-core` as the witness. |
|
||||
| Sibling-file `self.field` accesses fail because Rust enforces module visibility on tuple-struct or named fields | Confirmed: associated impl blocks in sibling files of the same `mod sqlite` reach private fields. If the compiler disagrees, bump the affected fields to `pub(super)` in `mod.rs` and note the bump in the commit message. |
|
||||
| Test-only helpers (`create_test_storage`, `make_record`, `rt`) get duplicated across test modules | Lift them once into a `#[cfg(test)] mod test_support { ... }` sub-module in `sqlite/mod.rs` and re-export with `pub(super) use`. Do this in commit 1 (scaffold), not later. |
|
||||
| `#[cfg(all(feature = "embeddings", feature = "vector-search"))]` items end up in `mod.rs` where they pollute the shared layer | Audit during commit 5 (search split); items behind both feature flags belong in `search.rs`. The `query_cache` field stays in `mod.rs` because the struct definition is there; the field declaration is feature-gated and that gate moves with the struct as-is. |
|
||||
| `git log --follow` blame chains break on the moved methods | Use `git mv sqlite.rs sqlite/mod.rs` in commit 1 so commit 1 looks like a rename (`git log --follow` keeps working). Subsequent commits are content moves inside the module; modern `git log --follow -M -C` heuristics still trace the lines. Reviewers who need pristine blame should bisect to before commit 1. |
|
||||
| Sub-plan 0001a (trait rewrite) has not landed when this work starts | Block: do not start commits 1-9 until 0001a is on the same branch (`feat/storage-trait-phase1`) and tests pass. `trait_impl.rs` lands the new attribute in commit 9; if 0001a is not in, commit 9 fails. |
|
||||
|
||||
---
|
||||
|
||||
## Self-Contained Brief (for /goal)
|
||||
|
||||
A fresh Claude Code session can execute this sub-plan by:
|
||||
|
||||
1. Reading this file end to end.
|
||||
2. Reading `crates/vestige-core/src/storage/sqlite.rs` (the file to be
|
||||
split) in full, using line ranges from the Mapping Table to confirm
|
||||
the current shape matches the brief.
|
||||
3. Reading `crates/vestige-core/src/storage/mod.rs` (the re-export
|
||||
surface that must continue to work).
|
||||
4. Reading `crates/vestige-core/src/storage/memory_store.rs` (the
|
||||
trait surface that `trait_impl.rs` implements).
|
||||
5. Confirming sub-plan 0001a has landed on the current branch by
|
||||
checking that `memory_store.rs` no longer carries
|
||||
`#[async_trait::async_trait]` on the trait declaration.
|
||||
6. Working through the Commit Sequence in order, running the
|
||||
Verification commands after each commit.
|
||||
|
||||
The session does not need to read ADR 0002 or the master Phase 2 plan
|
||||
to do this work. The split is purely mechanical relative to the
|
||||
mapping table above.
|
||||
847
docs/plans/0001c-async-trait-sunset.md
Normal file
847
docs/plans/0001c-async-trait-sunset.md
Normal file
|
|
@ -0,0 +1,847 @@
|
|||
# Sub-Plan 0001c: Sunset the `async-trait` crate dependency
|
||||
|
||||
**Status**: Draft
|
||||
**Branch**: `feat/storage-trait-phase1` (Phase 1 amendment, PR A)
|
||||
**Depends on**:
|
||||
- `0001a-trait-rewrite.md` (rewrites `MemoryStore` / `LocalMemoryStore` and
|
||||
the SQLite impl; lands first)
|
||||
- `0001b-sqlite-split.md` (moves `sqlite.rs` into `sqlite/`; lands second)
|
||||
|
||||
**Related**: `docs/adr/0002-phase-2-execution.md` (decision D1 closing line:
|
||||
"async-trait dependency stays in Cargo.toml only if other code uses it;
|
||||
otherwise removed"). This sub-plan operationalises the removal.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
This is the third and final Phase 1 amendment sub-plan. Sub-plan `0001a`
|
||||
rewrote `MemoryStore` / `LocalMemoryStore` using
|
||||
`#[trait_variant::make(MemoryStore: Send)]` and dropped the
|
||||
`#[async_trait::async_trait]` attribute from the SQLite impl block.
|
||||
Sub-plan `0001b` then split `sqlite.rs` into a `sqlite/` directory; the
|
||||
trait impl now lives in `sqlite/trait_impl.rs`. After `0001a` lands, the
|
||||
only remaining `async_trait` usage in the workspace is the embedder pair
|
||||
(`embedder/mod.rs` declares the trait; `embedder/fastembed.rs` implements
|
||||
it). This sub-plan rewrites those two files following the exact pattern
|
||||
from `0001a`, then removes `async-trait = "0.1"` from
|
||||
`crates/vestige-core/Cargo.toml`. End state: zero `async_trait` references
|
||||
anywhere under `crates/`, zero direct dependency on the `async-trait`
|
||||
crate, workspace tests and clippy green.
|
||||
|
||||
The rewrite is mechanically tiny -- one trait declaration, one impl block,
|
||||
one Cargo.toml line -- but it is gated behind `0001a` and `0001b` so the
|
||||
trait-rewrite pattern is already settled and so the SQLite trait impl
|
||||
attribute has already been dropped. Doing the embedder rewrite without
|
||||
that pre-work would leave the `async-trait` dep behind for the SQLite
|
||||
side and force the Cargo.toml deletion into a separate commit later.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Rewrite `LocalEmbedder` declaration in
|
||||
`crates/vestige-core/src/embedder/mod.rs` to use
|
||||
`#[trait_variant::make(Embedder: Send)] pub trait LocalEmbedder`.
|
||||
- Delete the `pub use LocalEmbedder as Embedder;` alias from the same file.
|
||||
The `Embedder` symbol becomes the trait that `trait_variant::make` emits
|
||||
at the same module path, so the existing
|
||||
`pub use embedder::{Embedder, ..., LocalEmbedder};` line in
|
||||
`crates/vestige-core/src/lib.rs:167` keeps working untouched.
|
||||
- Drop the `#[async_trait::async_trait]` attribute from the
|
||||
`FastembedEmbedder` impl block in
|
||||
`crates/vestige-core/src/embedder/fastembed.rs`.
|
||||
- Update doc comments on the trait declaration to describe
|
||||
`trait_variant` rather than `async_trait`.
|
||||
- Remove `async-trait = "0.1"` from
|
||||
`crates/vestige-core/Cargo.toml` (line 119 area). Use
|
||||
`cargo rm async-trait` from inside the crate directory.
|
||||
- Verify with `grep -rn "async_trait" crates/` returning zero hits.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Any change to the `MemoryStore` trait or `SqliteMemoryStore` impl;
|
||||
those were handled by `0001a`.
|
||||
- Any file moves in `embedder/` (no parallel of `0001b` is required;
|
||||
`embedder/` already has the `mod.rs` + `fastembed.rs` shape).
|
||||
- Touching `crates/vestige-mcp/` or any cognitive module. None of them
|
||||
hold `Arc<dyn Embedder>` or `Box<dyn Embedder>` in production.
|
||||
- Renaming the `Embedder` / `LocalEmbedder` symbols or changing the
|
||||
re-exports in `crates/vestige-core/src/lib.rs`.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### State assumed at start
|
||||
|
||||
- `0001a` is merged onto the branch. After `0001a`:
|
||||
- `crates/vestige-core/src/storage/memory_store.rs` declares
|
||||
`#[trait_variant::make(MemoryStore: Send)] pub trait LocalMemoryStore`.
|
||||
- The SQLite impl block has no `#[async_trait::async_trait]` attribute.
|
||||
- `grep -rn async_trait crates/` returns exactly three hits, all in
|
||||
`crates/vestige-core/src/embedder/` (two in `mod.rs`, one in
|
||||
`fastembed.rs`), and one Cargo.toml hit.
|
||||
- `0001b` is merged onto the branch. After `0001b`:
|
||||
- `crates/vestige-core/src/storage/sqlite.rs` no longer exists as a
|
||||
single file; the impl lives in `crates/vestige-core/src/storage/sqlite/trait_impl.rs`.
|
||||
- The embedder files are untouched.
|
||||
|
||||
### Required crates
|
||||
|
||||
| Crate | Version | Action |
|
||||
|----------------|---------|-----------------------------------------------------------------|
|
||||
| `trait-variant`| `0.1` | Already declared (line 117 of Cargo.toml). Verify present. |
|
||||
| `async-trait` | `0.1` | Remove. Only the two embedder files still use it after `0001a`. |
|
||||
|
||||
### Workspace-wide audit before starting
|
||||
|
||||
Run from `/home/delandtj/prppl/vestige-phase2/` (or the equivalent
|
||||
worktree where this sub-plan executes):
|
||||
|
||||
```bash
|
||||
grep -rn "async_trait\|async-trait" crates/ tests/
|
||||
```
|
||||
|
||||
Expected hits before this sub-plan starts (after `0001a` + `0001b`):
|
||||
|
||||
```
|
||||
crates/vestige-core/Cargo.toml:119:async-trait = "0.1"
|
||||
crates/vestige-core/src/embedder/mod.rs:24:/// `#[async_trait::async_trait]` makes every `async fn` return a
|
||||
crates/vestige-core/src/embedder/mod.rs:27:#[async_trait::async_trait]
|
||||
crates/vestige-core/src/embedder/mod.rs:56:/// Both names refer to the same `async_trait`-annotated trait.
|
||||
crates/vestige-core/src/embedder/fastembed.rs:44:#[async_trait::async_trait]
|
||||
```
|
||||
|
||||
Five hits across two source files and one Cargo.toml. After this sub-plan,
|
||||
the same grep must return zero hits.
|
||||
|
||||
```bash
|
||||
grep -rn "async-trait\|async_trait" --include="Cargo.toml" crates/
|
||||
```
|
||||
|
||||
Expected: exactly one hit (`crates/vestige-core/Cargo.toml:119`). No other
|
||||
workspace crate declares `async-trait` as a direct dependency. This is
|
||||
the precondition that lets us delete the line cleanly.
|
||||
|
||||
---
|
||||
|
||||
## Files Touched
|
||||
|
||||
### Trait declaration (vestige-core)
|
||||
|
||||
| File | Lines (approx) | Change |
|
||||
|-------------------------------------------------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `crates/vestige-core/src/embedder/mod.rs` | 21-57 | Replace `#[async_trait::async_trait] pub trait LocalEmbedder: Send + Sync + 'static` with `#[trait_variant::make(Embedder: Send)] pub trait LocalEmbedder: Sync + 'static`. Delete the `pub use LocalEmbedder as Embedder;` alias on line 57. Update doc comments (lines 21-26, 55-56). |
|
||||
|
||||
### Impl block (vestige-core)
|
||||
|
||||
| File | Lines (approx) | Change |
|
||||
|-------------------------------------------------|----------------|--------------------------------------------------------------------------------------------------------------|
|
||||
| `crates/vestige-core/src/embedder/fastembed.rs` | 44 | Delete the `#[async_trait::async_trait]` attribute. Keep the `impl LocalEmbedder for FastembedEmbedder { ... }` body verbatim. No `Box::pin`, no `'async_trait` lifetimes, no manual `Pin<Box<dyn Future>>`. |
|
||||
|
||||
### Other Embedder impls
|
||||
|
||||
None. `grep -rn "impl.*LocalEmbedder\|impl.*Embedder for" crates/ tests/`
|
||||
returns exactly one production hit:
|
||||
`crates/vestige-core/src/embedder/fastembed.rs:45: impl LocalEmbedder for FastembedEmbedder`.
|
||||
There is no test mock implementing `Embedder` in the test tree (the only
|
||||
test that touches the trait, `tests/phase_1/embedder_trait.rs`, uses the
|
||||
concrete `FastembedEmbedder` boxed as `Box<dyn Embedder>`).
|
||||
|
||||
### Call sites (production)
|
||||
|
||||
Verified by:
|
||||
|
||||
```bash
|
||||
grep -rn "dyn Embedder\|dyn LocalEmbedder" crates/ tests/ --include="*.rs"
|
||||
grep -rn "Box<dyn Embedder>\|Arc<dyn Embedder>" crates/ tests/ --include="*.rs"
|
||||
grep -rn "use.*Embedder" crates/ tests/ --include="*.rs"
|
||||
```
|
||||
|
||||
Production call sites that may need verification (and the expected change
|
||||
for each, even though we have already verified that none need an edit):
|
||||
|
||||
| File | Use | Required change |
|
||||
|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|-----------------|
|
||||
| `crates/vestige-core/src/lib.rs:167` | `pub use embedder::{Embedder, EmbedderError, EmbedderResult, FastembedEmbedder, LocalEmbedder};` | None. Both names still exist at `crate::embedder::*` after the rewrite; `Embedder` is now the `trait_variant`-generated trait, `LocalEmbedder` is the source-of-truth trait. The re-export keeps resolving. |
|
||||
| `crates/vestige-core/src/embedder/fastembed.rs:7` | `use super::{EmbedderError, EmbedderResult, LocalEmbedder};` | None. `LocalEmbedder` is still the source-of-truth trait name. |
|
||||
| `crates/vestige-core/src/embedder/mod.rs:5` | `pub use fastembed::FastembedEmbedder;` | None. Concrete type, untouched. |
|
||||
| `crates/vestige-mcp/src/**` | No file imports `Embedder` or `LocalEmbedder` by name; none hold `Arc<dyn Embedder>` or `Box<dyn Embedder>`. | None. Verified by grep returning empty for `dyn Embedder` and `dyn LocalEmbedder` under `crates/vestige-mcp/`. |
|
||||
| Cognitive modules under `crates/vestige-core/src/advanced/` and `crates/vestige-core/src/neuroscience/` | No file imports `Embedder` or `LocalEmbedder` by name. `advanced/adaptive_embedding.rs` defines its own unrelated `AdaptiveEmbedder` struct. | None. The name collision is purely surface-level; the two types live in different modules and never resolve to each other. |
|
||||
| `crates/vestige-core/src/embeddings/**` | No file imports `Embedder` or `LocalEmbedder`. The `EmbeddingService` struct is what `FastembedEmbedder` wraps internally. | None. |
|
||||
|
||||
The production audit returns zero files that need editing.
|
||||
|
||||
### Call sites (tests)
|
||||
|
||||
| File | Lines | Use | Required change |
|
||||
|------------------------------------------------------------|-------|--------------------------------------------------------------------|-----------------|
|
||||
| `tests/phase_1/embedder_trait.rs` | 3, 19 | `use vestige_core::embedder::{Embedder, FastembedEmbedder};`<br>`let e: Box<dyn Embedder> = Box::new(FastembedEmbedder::new());` | None. `Embedder` is the `trait_variant`-generated Send variant; `Box<dyn Embedder>` keeps compiling. `FastembedEmbedder` implements `LocalEmbedder`; the blanket `impl<T: LocalEmbedder + Send> Embedder for T` that `trait_variant::make` emits gives the boxing for free. |
|
||||
|
||||
The test audit returns zero files that need editing.
|
||||
|
||||
### Cargo dependency cleanup
|
||||
|
||||
| File | Lines | Change |
|
||||
|-------------------------------------|-----------|-----------------------------------------------------------------------------------------------------|
|
||||
| `crates/vestige-core/Cargo.toml` | 119 | Remove `async-trait = "0.1"`. Run `cargo rm async-trait` from inside `crates/vestige-core/` so `Cargo.lock` updates atomically with the manifest. |
|
||||
|
||||
### Documentation
|
||||
|
||||
| File | Change |
|
||||
|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `crates/vestige-core/src/embedder/mod.rs` | Rewrite the trait-level doc comment (lines 21-26) and the `pub use` doc comment (lines 55-56) to describe `trait_variant`, not `async_trait`. See "Trait declaration rewrite" below for the exact new text. |
|
||||
| `CLAUDE.md` | No change. The repo-level architecture notes do not name the trait attribute. |
|
||||
|
||||
---
|
||||
|
||||
## Trait Declaration Rewrite
|
||||
|
||||
### Before (state after `0001a` and `0001b` land)
|
||||
|
||||
`crates/vestige-core/src/embedder/mod.rs:1-58`:
|
||||
|
||||
```rust
|
||||
//! Text-to-vector encoding trait. Pluggable per-install.
|
||||
|
||||
mod fastembed;
|
||||
|
||||
pub use fastembed::FastembedEmbedder;
|
||||
|
||||
/// Error returned by every `Embedder` method.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EmbedderError {
|
||||
#[error("embedder initialization failed: {0}")]
|
||||
Init(String),
|
||||
#[error("embedding generation failed: {0}")]
|
||||
EmbedFailed(String),
|
||||
#[error("invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
}
|
||||
|
||||
pub type EmbedderResult<T> = std::result::Result<T, EmbedderError>;
|
||||
|
||||
/// Pluggable embedder. The storage layer NEVER calls fastembed directly;
|
||||
/// callers compute vectors via this trait and pass them into `MemoryStore`.
|
||||
///
|
||||
/// `#[async_trait::async_trait]` makes every `async fn` return a
|
||||
/// `Pin<Box<dyn Future + Send>>`, which is required for `Box<dyn Embedder>`
|
||||
/// and `Arc<dyn Embedder>` to be dyn-compatible.
|
||||
#[async_trait::async_trait]
|
||||
pub trait LocalEmbedder: Send + Sync + 'static {
|
||||
async fn embed(&self, text: &str) -> EmbedderResult<Vec<f32>>;
|
||||
|
||||
fn model_name(&self) -> &str;
|
||||
|
||||
fn dimension(&self) -> usize;
|
||||
|
||||
/// Stable blake3 hash of (model_name || dimension || vestige-core crate version).
|
||||
/// Lowercase hex, 64 chars.
|
||||
///
|
||||
/// Used by `MemoryStore::register_model` to detect silent model drift
|
||||
/// (e.g. a fastembed minor upgrade that changes vector output).
|
||||
fn model_hash(&self) -> String;
|
||||
|
||||
async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult<Vec<Vec<f32>>>;
|
||||
|
||||
/// Returns the `ModelSignature` describing this embedder. Convenience
|
||||
/// wrapper over the three accessors above.
|
||||
fn signature(&self) -> crate::storage::ModelSignature {
|
||||
crate::storage::ModelSignature {
|
||||
name: self.model_name().to_string(),
|
||||
dimension: self.dimension(),
|
||||
hash: self.model_hash(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias: `Embedder` is the dyn-compatible, Send+Sync variant.
|
||||
/// Both names refer to the same `async_trait`-annotated trait.
|
||||
pub use LocalEmbedder as Embedder;
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
`crates/vestige-core/src/embedder/mod.rs:1-55` (approximately):
|
||||
|
||||
```rust
|
||||
//! Text-to-vector encoding trait. Pluggable per-install.
|
||||
|
||||
mod fastembed;
|
||||
|
||||
pub use fastembed::FastembedEmbedder;
|
||||
|
||||
/// Error returned by every `Embedder` method.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EmbedderError {
|
||||
#[error("embedder initialization failed: {0}")]
|
||||
Init(String),
|
||||
#[error("embedding generation failed: {0}")]
|
||||
EmbedFailed(String),
|
||||
#[error("invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
}
|
||||
|
||||
pub type EmbedderResult<T> = std::result::Result<T, EmbedderError>;
|
||||
|
||||
/// Pluggable embedder. The storage layer NEVER calls fastembed directly;
|
||||
/// callers compute vectors via this trait and pass them into `MemoryStore`.
|
||||
///
|
||||
/// `LocalEmbedder` is the source-of-truth trait. The
|
||||
/// `#[trait_variant::make(Embedder: Send)]` attribute auto-generates an
|
||||
/// `Embedder` variant whose returned futures are `Send`, so
|
||||
/// `Box<dyn Embedder>` and `Arc<dyn Embedder>` are usable on tokio/axum
|
||||
/// runtimes, while `Box<dyn LocalEmbedder>` remains usable on single-
|
||||
/// threaded executors and thread-local backends.
|
||||
///
|
||||
/// Every method is native async-fn-in-trait (stable on MSRV 1.91); no
|
||||
/// per-call heap allocation, no boxed futures at the static-dispatch
|
||||
/// boundary.
|
||||
#[trait_variant::make(Embedder: Send)]
|
||||
pub trait LocalEmbedder: Sync + 'static {
|
||||
async fn embed(&self, text: &str) -> EmbedderResult<Vec<f32>>;
|
||||
|
||||
fn model_name(&self) -> &str;
|
||||
|
||||
fn dimension(&self) -> usize;
|
||||
|
||||
/// Stable blake3 hash of (model_name || dimension || vestige-core crate version).
|
||||
/// Lowercase hex, 64 chars.
|
||||
///
|
||||
/// Used by `MemoryStore::register_model` to detect silent model drift
|
||||
/// (e.g. a fastembed minor upgrade that changes vector output).
|
||||
fn model_hash(&self) -> String;
|
||||
|
||||
async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult<Vec<Vec<f32>>>;
|
||||
|
||||
/// Returns the `ModelSignature` describing this embedder. Convenience
|
||||
/// wrapper over the three accessors above.
|
||||
fn signature(&self) -> crate::storage::ModelSignature {
|
||||
crate::storage::ModelSignature {
|
||||
name: self.model_name().to_string(),
|
||||
dimension: self.dimension(),
|
||||
hash: self.model_hash(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Both halves of the macro-generated output (for reviewer clarity)
|
||||
|
||||
`trait_variant::make(Embedder: Send)` expands the source-of-truth
|
||||
`LocalEmbedder` declaration above into the equivalent of:
|
||||
|
||||
```rust
|
||||
// 1. The source-of-truth trait, exactly as written.
|
||||
pub trait LocalEmbedder: Sync + 'static {
|
||||
fn embed(&self, text: &str) -> impl Future<Output = EmbedderResult<Vec<f32>>>;
|
||||
fn model_name(&self) -> &str;
|
||||
fn dimension(&self) -> usize;
|
||||
fn model_hash(&self) -> String;
|
||||
fn embed_batch(&self, texts: &[&str]) -> impl Future<Output = EmbedderResult<Vec<Vec<f32>>>>;
|
||||
fn signature(&self) -> crate::storage::ModelSignature { /* default impl unchanged */ }
|
||||
}
|
||||
|
||||
// 2. The generated Send variant.
|
||||
pub trait Embedder: Sync + 'static {
|
||||
fn embed(&self, text: &str) -> impl Future<Output = EmbedderResult<Vec<f32>>> + Send;
|
||||
fn model_name(&self) -> &str;
|
||||
fn dimension(&self) -> usize;
|
||||
fn model_hash(&self) -> String;
|
||||
fn embed_batch(&self, texts: &[&str]) -> impl Future<Output = EmbedderResult<Vec<Vec<f32>>>> + Send;
|
||||
fn signature(&self) -> crate::storage::ModelSignature { /* default impl unchanged */ }
|
||||
}
|
||||
|
||||
// 3. The blanket impl that wires any LocalEmbedder + Send through to Embedder.
|
||||
impl<T> Embedder for T
|
||||
where
|
||||
T: LocalEmbedder + Send,
|
||||
// (all returned futures of LocalEmbedder's async fns are required to be Send,
|
||||
// which is satisfied for FastembedEmbedder -- see "Risks" below)
|
||||
{ /* forwarders */ }
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The `pub use LocalEmbedder as Embedder;` line on the current
|
||||
`embedder/mod.rs:57` is **deleted** entirely. `Embedder` is now the
|
||||
trait that `trait_variant::make` emits at the same module path; the
|
||||
re-export in `crates/vestige-core/src/lib.rs:167`
|
||||
(`pub use embedder::{Embedder, ..., LocalEmbedder};`) keeps resolving
|
||||
unchanged.
|
||||
- `Sync + 'static` on `LocalEmbedder` (and no `Send` bound on the trait
|
||||
itself) mirrors the `0001a` pattern for `LocalMemoryStore`. The current
|
||||
trait carries `Send + Sync + 'static`; the rewrite drops the `Send`
|
||||
bound from the local variant. `Box<dyn LocalEmbedder>` is `Sync` but
|
||||
not `Send`; `Box<dyn Embedder>` (the generated variant) is `Send + Sync`.
|
||||
- `trait_variant` 0.1 does **not** require any attribute on the impl
|
||||
block. The attribute lives only on the trait declaration. See next
|
||||
section.
|
||||
|
||||
---
|
||||
|
||||
## Impl Block Migration
|
||||
|
||||
`trait_variant` 0.1 attaches the attribute only to the trait declaration.
|
||||
The impl side is plain `impl LocalEmbedder for FastembedEmbedder`; no
|
||||
attribute on the impl, no `#[trait_variant::make(Embedder: Send)]` on the
|
||||
impl block. The macro auto-generates the blanket
|
||||
`impl<T: LocalEmbedder + Send> Embedder for T`, so any concrete type that
|
||||
implements `LocalEmbedder` automatically also implements `Embedder`
|
||||
provided it is `Send`.
|
||||
|
||||
`FastembedEmbedder` is `Send + Sync` because:
|
||||
|
||||
- `inner: EmbeddingService` is `Send + Sync` (it wraps fastembed's
|
||||
`TextEmbedding` which is `Send + Sync` after fastembed 4.x; verified
|
||||
in `crates/vestige-core/src/embeddings/mod.rs`).
|
||||
- `cached_hash: std::sync::OnceLock<String>` is `Send + Sync` for `T: Send + Sync`.
|
||||
- The `#[cfg(not(feature = "embeddings"))]` branch carries only
|
||||
`cached_hash`, which is unconditionally `Send + Sync`.
|
||||
|
||||
No new bound is needed.
|
||||
|
||||
### Before
|
||||
|
||||
`crates/vestige-core/src/embedder/fastembed.rs:38-100` (relevant header):
|
||||
|
||||
```rust
|
||||
impl Default for FastembedEmbedder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl LocalEmbedder for FastembedEmbedder {
|
||||
async fn embed(&self, text: &str) -> EmbedderResult<Vec<f32>> {
|
||||
// ... body unchanged ...
|
||||
}
|
||||
|
||||
fn model_name(&self) -> &str { /* ... */ }
|
||||
fn dimension(&self) -> usize { /* ... */ }
|
||||
fn model_hash(&self) -> String { /* ... */ }
|
||||
|
||||
async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult<Vec<Vec<f32>>> {
|
||||
// ... body unchanged ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
`crates/vestige-core/src/embedder/fastembed.rs:38-99` (one fewer line):
|
||||
|
||||
```rust
|
||||
impl Default for FastembedEmbedder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalEmbedder for FastembedEmbedder {
|
||||
async fn embed(&self, text: &str) -> EmbedderResult<Vec<f32>> {
|
||||
// ... body unchanged ...
|
||||
}
|
||||
|
||||
fn model_name(&self) -> &str { /* ... */ }
|
||||
fn dimension(&self) -> usize { /* ... */ }
|
||||
fn model_hash(&self) -> String { /* ... */ }
|
||||
|
||||
async fn embed_batch(&self, texts: &[&str]) -> EmbedderResult<Vec<Vec<f32>>> {
|
||||
// ... body unchanged ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Diff is exactly one removed line (the `#[async_trait::async_trait]`
|
||||
attribute on line 44). Every method body, every `async fn` signature,
|
||||
every `use` statement inside the impl block stays verbatim. No
|
||||
`Box::pin(async move { ... })`, no manual `Pin<Box<dyn Future>>`, no
|
||||
`'async_trait` lifetime markers; native async-fn-in-trait does this
|
||||
directly.
|
||||
|
||||
### Why the impl block does not need an attribute
|
||||
|
||||
`trait_variant::make` generates two things from the source trait
|
||||
(see the "macro-generated output" subsection above):
|
||||
|
||||
1. The source trait itself (`LocalEmbedder`), with native async fns.
|
||||
2. A second trait (`Embedder`) whose method signatures return
|
||||
`impl Future<Output = ...> + Send` instead of `impl Future<Output = ...>`,
|
||||
plus a blanket impl wiring concrete types through.
|
||||
|
||||
Both are emitted at the macro-call site. `FastembedEmbedder` writes one
|
||||
impl block (against `LocalEmbedder`); the macro-generated blanket
|
||||
guarantees `FastembedEmbedder: Embedder` for free. The
|
||||
`Box<dyn Embedder>` cast on `tests/phase_1/embedder_trait.rs:19` keeps
|
||||
type-checking unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Call Site Audit
|
||||
|
||||
Verified via, from the phase2 worktree root:
|
||||
|
||||
```bash
|
||||
grep -rn "async_trait\|LocalEmbedder\|dyn Embedder" crates/
|
||||
grep -rn "use.*Embedder" crates/ tests/ --include="*.rs"
|
||||
grep -rn "Box<dyn Embedder>\|Arc<dyn Embedder>\|&dyn Embedder" crates/ tests/ --include="*.rs"
|
||||
grep -rn "Box<dyn LocalEmbedder>\|Arc<dyn LocalEmbedder>\|&dyn LocalEmbedder" crates/ tests/ --include="*.rs"
|
||||
grep -rn "impl LocalEmbedder for\|impl Embedder for" crates/ tests/ --include="*.rs"
|
||||
```
|
||||
|
||||
### Files that reference the trait object form
|
||||
|
||||
Exactly one, test-only:
|
||||
|
||||
| File | Line | Use | Required change |
|
||||
|--------------------------------------|------|------------------------------------------------------------------------------------------|-----------------|
|
||||
| `tests/phase_1/embedder_trait.rs` | 3 | `use vestige_core::embedder::{Embedder, FastembedEmbedder};` | None. `Embedder` is the generated Send variant at the same path. |
|
||||
| `tests/phase_1/embedder_trait.rs` | 19 | `let e: Box<dyn Embedder> = Box::new(FastembedEmbedder::new());` | None. `FastembedEmbedder: LocalEmbedder + Send` -> blanket gives `: Embedder` -> `Box<dyn Embedder>` is well-formed. |
|
||||
|
||||
### Files that import `Embedder` or `LocalEmbedder` by name
|
||||
|
||||
| File | Line | Use | Required change |
|
||||
|-----------------------------------------------------|------|----------------------------------------------------------------------------------------------------------------|-----------------|
|
||||
| `crates/vestige-core/src/lib.rs` | 167 | `pub use embedder::{Embedder, EmbedderError, EmbedderResult, FastembedEmbedder, LocalEmbedder};` | None. Both names still resolve. |
|
||||
| `crates/vestige-core/src/embedder/mod.rs` | 5 | `pub use fastembed::FastembedEmbedder;` | None. |
|
||||
| `crates/vestige-core/src/embedder/fastembed.rs` | 7 | `use super::{EmbedderError, EmbedderResult, LocalEmbedder};` | None. |
|
||||
| `tests/phase_1/embedder_trait.rs` | 3 | `use vestige_core::embedder::{Embedder, FastembedEmbedder};` | None. |
|
||||
|
||||
### Files that implement the trait
|
||||
|
||||
| File | Line | Impl | Required change |
|
||||
|-----------------------------------------------------|------|-----------------------------------------------------------------------|----------------------------------------------|
|
||||
| `crates/vestige-core/src/embedder/fastembed.rs` | 45 | `impl LocalEmbedder for FastembedEmbedder` (currently `#[async_trait]`) | Drop the `#[async_trait::async_trait]` attr. |
|
||||
|
||||
No other impls exist. There is no test mock implementing `Embedder` or
|
||||
`LocalEmbedder` anywhere in the workspace.
|
||||
|
||||
### Files that import `async_trait` directly
|
||||
|
||||
After `0001a` lands, only the embedder pair:
|
||||
|
||||
```
|
||||
crates/vestige-core/src/embedder/mod.rs:24 (doc comment)
|
||||
crates/vestige-core/src/embedder/mod.rs:27 (attribute)
|
||||
crates/vestige-core/src/embedder/mod.rs:56 (doc comment)
|
||||
crates/vestige-core/src/embedder/fastembed.rs:44 (attribute)
|
||||
```
|
||||
|
||||
Plus the Cargo manifest:
|
||||
|
||||
```
|
||||
crates/vestige-core/Cargo.toml:119:async-trait = "0.1"
|
||||
```
|
||||
|
||||
### Production files that hold a concrete embedder
|
||||
|
||||
`FastembedEmbedder` is constructed and used by concrete name (not via
|
||||
trait object) in: the dashboard / MCP layer if it needs to embed query
|
||||
strings ad-hoc. None of those call sites need an edit because the
|
||||
concrete type is what they hold, and the concrete type is untouched by
|
||||
this sub-plan.
|
||||
|
||||
### Conclusion
|
||||
|
||||
| Category | Count |
|
||||
|--------------------------------------------------|-------|
|
||||
| Production source files modified | 2 |
|
||||
| Test source files modified | 0 |
|
||||
| Cargo manifests modified | 1 |
|
||||
| Production source files importing `Embedder` / `LocalEmbedder` (verified unchanged) | 3 |
|
||||
| Test source files importing `Embedder` (verified unchanged) | 1 |
|
||||
| Direct `async_trait` uses outside the embedder module after `0001a` | 0 |
|
||||
|
||||
---
|
||||
|
||||
## Cargo.toml Change
|
||||
|
||||
From inside `crates/vestige-core/`:
|
||||
|
||||
```bash
|
||||
cargo rm async-trait
|
||||
```
|
||||
|
||||
This removes line 119 of `Cargo.toml` and updates `Cargo.lock` in one
|
||||
step. Manual editing is acceptable as a fallback if `cargo rm` is
|
||||
unavailable in the agent environment; in that case, follow up with
|
||||
`cargo check -p vestige-core` to refresh the lockfile.
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
# Direct dependency must be gone.
|
||||
grep -rn "async-trait\|async_trait" --include="Cargo.toml" crates/
|
||||
# Expected: empty.
|
||||
|
||||
# Transitive presence is permitted (e.g. via a third-party crate).
|
||||
cargo tree -p vestige-core --depth 2 | grep async-trait
|
||||
# Expected: empty for the direct edges; if a sub-dependency still pulls
|
||||
# async-trait transitively, the output may contain it deeper than depth 2,
|
||||
# which is fine. We only care about removing the DIRECT edge.
|
||||
```
|
||||
|
||||
If `cargo tree --depth 2` returns any `async-trait` line, inspect with
|
||||
`cargo tree -p vestige-core -i async-trait` to see what is pulling it.
|
||||
Acceptable parents: any third-party crate. Unacceptable parent: anything
|
||||
under `vestige-*`, which would mean a missed file.
|
||||
|
||||
---
|
||||
|
||||
## Commit Sequence
|
||||
|
||||
Three commits, each green on
|
||||
`cargo test -p vestige-core --features embeddings,vector-search` and
|
||||
`cargo test -p vestige-core --no-default-features`.
|
||||
|
||||
### Commit 1: rewrite LocalEmbedder trait declaration
|
||||
|
||||
- Touches: `crates/vestige-core/src/embedder/mod.rs` only.
|
||||
- Action: replace lines 21-57 per the "Trait Declaration Rewrite"
|
||||
section above. Delete the `pub use LocalEmbedder as Embedder;` line.
|
||||
- Green after: `cargo check -p vestige-core` (the impl block in
|
||||
`fastembed.rs` still has its `#[async_trait::async_trait]` attribute;
|
||||
the macro is harmless when applied to a trait that the impl block
|
||||
targets by path, because async_trait rewrites the impl's async fns
|
||||
into boxed-future fns whose signatures still match the native-async
|
||||
declarations after trait_variant lowering, just as it did for the
|
||||
SQLite intermediate state in `0001a`'s commit 1).
|
||||
|
||||
**Mitigation if check fails between commits 1 and 2:** combine the
|
||||
two into a single commit. The split is offered for review convenience;
|
||||
the build must be green after every commit lands.
|
||||
|
||||
### Commit 2: drop `#[async_trait::async_trait]` from FastembedEmbedder impl
|
||||
|
||||
- Touches: `crates/vestige-core/src/embedder/fastembed.rs` only.
|
||||
- Action: delete line 44 (`#[async_trait::async_trait]`).
|
||||
- Green after:
|
||||
- `cargo test -p vestige-core --features embeddings,vector-search`.
|
||||
- `cargo test -p vestige-core --no-default-features` (the
|
||||
`#[cfg(not(feature = "embeddings"))]` branches inside the impl now
|
||||
stand on their own).
|
||||
- Phase 1 integration test: `cargo test --test embedder_trait
|
||||
--features embeddings,vector-search`.
|
||||
|
||||
### Commit 3: drop the async-trait dependency
|
||||
|
||||
- Touches: `crates/vestige-core/Cargo.toml` (plus `Cargo.lock` as a
|
||||
side effect).
|
||||
- Action: from inside `crates/vestige-core/`, run `cargo rm async-trait`.
|
||||
- Green after: `cargo build --workspace --all-targets` and
|
||||
`cargo test --workspace`.
|
||||
- Final hard ASCII gate: `! grep -rn "async_trait" crates/` must exit
|
||||
with status 0 (i.e. the inverted grep finds nothing).
|
||||
|
||||
### Combined alternative
|
||||
|
||||
Commits 1 and 2 may fold into a single commit if the per-step split
|
||||
feels artificial (the patterns are identical to `0001a`'s commits 3
|
||||
and 4). Commit 3 (the Cargo.toml removal) should stay separate so the
|
||||
dependency-removal diff is visible in isolation.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Every command runs from the repo root unless noted otherwise.
|
||||
|
||||
```bash
|
||||
# 1. Vestige-core, default features (embeddings + vector-search).
|
||||
cargo test -p vestige-core --features embeddings,vector-search
|
||||
|
||||
# 2. Vestige-core, minimal features (no embeddings, no vector-search).
|
||||
cargo test -p vestige-core --no-default-features
|
||||
|
||||
# 3. Workspace build, all targets (catches any feature-gated regression
|
||||
# in the vestige-mcp tools tree).
|
||||
cargo build --workspace --all-targets
|
||||
|
||||
# 4. Whole-workspace test (vestige-mcp 406 tests + vestige-core 352
|
||||
# tests per the CLAUDE.md baseline).
|
||||
cargo test --workspace
|
||||
|
||||
# 5. Phase 1 embedder integration test (the trait-shape contract).
|
||||
cargo test --test embedder_trait --features embeddings,vector-search
|
||||
|
||||
# 6. Clippy gate, deny warnings (matches Phase 1 PR policy).
|
||||
cargo clippy --workspace --all-targets --features embeddings,vector-search -- -D warnings
|
||||
|
||||
# 7. Hard ASCII gate -- async_trait must be gone from source.
|
||||
! grep -rn "async_trait" crates/
|
||||
# Inverted grep: exit 0 iff grep found nothing.
|
||||
|
||||
# 8. Hard ASCII gate -- async-trait must be gone from manifests.
|
||||
! grep -rn "async-trait" --include="Cargo.toml" crates/
|
||||
|
||||
# 9. Confirm trait_variant attribute is in place at the embedder.
|
||||
grep -rn "trait_variant::make" crates/vestige-core/src/embedder/
|
||||
# Expected: exactly one hit, in embedder/mod.rs.
|
||||
|
||||
# 10. Workspace-wide trait_variant audit (should match the count after
|
||||
# 0001a -- two hits total, one for storage, one for embedder).
|
||||
grep -rn "trait_variant::make" crates/vestige-core/src/
|
||||
# Expected: two hits.
|
||||
```
|
||||
|
||||
Expected outcomes:
|
||||
|
||||
- Command 1: 352 vestige-core tests pass (matches baseline).
|
||||
- Command 2: smaller test count, all pass.
|
||||
- Command 3: workspace builds in dev mode for all targets.
|
||||
- Command 4: 758 total tests pass (matches CLAUDE.md baseline).
|
||||
- Command 5: `embedder_trait` integration test passes. The
|
||||
`fastembed_implements_embedder_trait` assertion (`let e: Box<dyn
|
||||
Embedder> = ...`) is the canary; if `trait_variant::make` failed to
|
||||
emit the `Embedder` Send variant, this fails to compile.
|
||||
- Command 6: zero clippy warnings.
|
||||
- Command 7: empty output. `async_trait` is fully gone from source.
|
||||
- Command 8: empty output. `async-trait` is fully gone from manifests.
|
||||
- Command 9: one hit.
|
||||
- Command 10: two hits.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
A reviewer should be able to check every box:
|
||||
|
||||
- [ ] `crates/vestige-core/src/embedder/mod.rs` declares the embedder
|
||||
trait with `#[trait_variant::make(Embedder: Send)] pub trait
|
||||
LocalEmbedder: Sync + 'static`, no `async_trait` attribute, no
|
||||
`Send` bound on `LocalEmbedder` itself.
|
||||
- [ ] `crates/vestige-core/src/embedder/mod.rs` no longer contains
|
||||
`pub use LocalEmbedder as Embedder;`.
|
||||
- [ ] `crates/vestige-core/src/embedder/fastembed.rs` declares
|
||||
`impl LocalEmbedder for FastembedEmbedder` with no attribute on
|
||||
the impl block.
|
||||
- [ ] `crates/vestige-core/Cargo.toml` does not declare `async-trait`
|
||||
as a direct dependency.
|
||||
- [ ] `grep -rn "async_trait" crates/` returns zero hits.
|
||||
- [ ] `grep -rn "async-trait" --include="Cargo.toml" crates/` returns
|
||||
zero hits.
|
||||
- [ ] `grep -rn "trait_variant::make" crates/vestige-core/src/` returns
|
||||
exactly two hits (storage trait + embedder trait).
|
||||
- [ ] All 758 workspace tests pass (`cargo test --workspace`).
|
||||
- [ ] `tests/phase_1/embedder_trait.rs` compiles and passes with the
|
||||
`Box<dyn Embedder>` cast intact.
|
||||
- [ ] `cargo clippy --workspace --all-targets --features
|
||||
embeddings,vector-search -- -D warnings` is clean.
|
||||
- [ ] No file under `crates/vestige-mcp/` or under
|
||||
`crates/vestige-core/src/{neuroscience,advanced,consolidation,
|
||||
codebase,memory,embeddings}/` was modified by this sub-plan.
|
||||
- [ ] `Cargo.lock` was updated as a side effect of `cargo rm async-trait`
|
||||
(it must no longer reference `async-trait`).
|
||||
- [ ] Doc comments on the embedder trait declaration describe
|
||||
`trait_variant`, not `async_trait`.
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- **`trait_variant::make` requires returned futures to be `Send` for the
|
||||
blanket `impl<T: LocalEmbedder + Send> Embedder for T`. If any
|
||||
`async fn embed`/`embed_batch` body inside `FastembedEmbedder` captures
|
||||
a non-Send local, the blanket impl fails to type-check.**
|
||||
Mitigation: the existing impl bodies call `self.inner.embed(text)` /
|
||||
`self.inner.embed_batch(texts)`, where `inner: EmbeddingService` is
|
||||
`Send + Sync` (verified in `crates/vestige-core/src/embeddings/mod.rs`).
|
||||
No `.await` points exist inside the bodies in either feature branch;
|
||||
the `EmbeddingService::embed` calls are synchronous. The futures are
|
||||
trivially `Send`. If a future change introduces a non-Send local
|
||||
(e.g. an `Rc` or a non-Send guard), the blanket impl will surface that
|
||||
as a compile error at the dyn cast in `tests/phase_1/embedder_trait.rs`,
|
||||
which is the correct outcome.
|
||||
- **The macro's blanket impl interacts oddly with the default `signature`
|
||||
method.**
|
||||
Mitigation: `signature` is a synchronous method returning
|
||||
`crate::storage::ModelSignature`, with no `Send` or `async` concerns.
|
||||
`trait_variant::make` emits it on both variants as-is. The existing
|
||||
Phase 1 test `signature_matches_memory_store_registry` exercises this
|
||||
path and is part of the verification step.
|
||||
- **`Box<dyn Embedder>` cast in `tests/phase_1/embedder_trait.rs` fails
|
||||
to resolve after the rewrite.**
|
||||
Mitigation: the rewrite preserves the `Embedder` symbol at the same
|
||||
module path; only its provenance changes (now generated by
|
||||
`trait_variant::make` instead of by `pub use LocalEmbedder as
|
||||
Embedder;`). The macro is specifically designed so that the generated
|
||||
trait is dyn-compatible at the Send-bound boundary. Verified by the
|
||||
identical pattern already working for `MemoryStore` after `0001a`.
|
||||
- **`cargo rm async-trait` updates `Cargo.lock` but accidentally bumps
|
||||
other crates.**
|
||||
Mitigation: run `cargo rm async-trait` and then immediately inspect
|
||||
the resulting `Cargo.lock` diff. The expected diff is the removal of
|
||||
the `[[package]] name = "async-trait"` block and its hash. Anything
|
||||
else is a red flag and should be reverted before committing
|
||||
(`git checkout -- Cargo.lock` then `cargo update -p async-trait
|
||||
--precise=remove` -- or fall back to manual edit + `cargo check`).
|
||||
- **A new workspace crate added in parallel with this work declares
|
||||
`async-trait` and the dependency removal silently re-introduces it
|
||||
later.**
|
||||
Mitigation: the verification step `grep -rn "async-trait"
|
||||
--include="Cargo.toml" crates/` is part of the acceptance criteria; a
|
||||
rebase that reintroduces the line will fail this gate.
|
||||
- **MCP server uses `Embedder` somewhere we missed.**
|
||||
Mitigation: full workspace grep (`grep -rn "Embedder" crates/`)
|
||||
returns no hits inside `crates/vestige-mcp/` for the trait names; the
|
||||
MCP layer uses the concrete `EmbeddingService` from
|
||||
`crates/vestige-core/src/embeddings/` for ad-hoc embedding calls. The
|
||||
trait surface is purely internal to `vestige-core`.
|
||||
|
||||
---
|
||||
|
||||
## Out-of-Band Notes
|
||||
|
||||
- **No other workspace crate declares `async-trait` as a direct
|
||||
dependency.** Verified by
|
||||
`grep -rn "async-trait" --include="Cargo.toml" crates/` returning
|
||||
exactly one hit at `crates/vestige-core/Cargo.toml:119`. There is
|
||||
nothing to clean up in `crates/vestige-mcp/Cargo.toml` or elsewhere.
|
||||
- **Order matters across the three Phase 1 amendment sub-plans:**
|
||||
`0001a` (trait rewrite) -> `0001b` (sqlite split) -> `0001c` (this
|
||||
one, async-trait sunset). Reversing the order is possible in
|
||||
principle but would force re-editing the embedder rewrite twice and
|
||||
leaves the `async-trait` dep behind until very late.
|
||||
- **This sub-plan amends `feat/storage-trait-phase1` (tip 790c0c8 plus
|
||||
whatever commits `0001a` and `0001b` added).** The branch has not
|
||||
been opened upstream yet, so amending in place is safe; no force-push
|
||||
to a public PR.
|
||||
- **After this sub-plan lands, the branch is reviewed and merged before
|
||||
Phase 2 sub-plans (`0002a-` through `0002i-`) begin implementation.**
|
||||
Phase 2 introduces no async-trait usage; the Postgres backend follows
|
||||
the same `trait_variant::make` pattern (see ADR 0002 D1).
|
||||
- **`trait-variant` 0.1 stays in `Cargo.toml`.** It is the only crate
|
||||
this sub-plan keeps; `async-trait` is the only one it removes.
|
||||
|
||||
---
|
||||
|
||||
## Self-Contained `/goal` Brief
|
||||
|
||||
For a fresh Claude Code session executing this sub-plan without prior
|
||||
conversation context:
|
||||
|
||||
1. Check out branch `feat/storage-trait-phase1` (or a worktree off
|
||||
of it after `0001a` and `0001b` are merged into it).
|
||||
2. Read this file (`docs/plans/0001c-async-trait-sunset.md`) in full.
|
||||
3. Read `docs/plans/0001a-trait-rewrite.md` sections "Trait declaration
|
||||
rewrite" and "Impl block migration" -- they document the exact
|
||||
pattern this sub-plan mirrors for the embedder.
|
||||
4. Run the prerequisite audit grep listed under "Prerequisites". If it
|
||||
returns more than the five hits documented there, stop and report;
|
||||
the upstream state does not match what this sub-plan assumes.
|
||||
5. Execute Commit 1 (rewrite `embedder/mod.rs`), then Commit 2 (drop
|
||||
the attribute on the FastembedEmbedder impl), then Commit 3
|
||||
(`cargo rm async-trait`). Run the verification commands listed
|
||||
above after each commit; do not proceed if any test or clippy gate
|
||||
fails.
|
||||
6. Verify every box in "Acceptance Criteria" is ticked.
|
||||
7. Report file paths touched, test counts, and the final two grep
|
||||
results (commands 7 and 8 from "Verification") in the closing
|
||||
message.
|
||||
Loading…
Add table
Add a link
Reference in a new issue