Skip to content

Commit d209d03

Browse files
Merge pull request #42 from YellowSnnowmann/feat/18-a3-bind-core-store-as-a-driver
Bind this crate's own store as a driver (#18 §A3)
2 parents 5f9052e + 8f0c818 commit d209d03

7 files changed

Lines changed: 188 additions & 14 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ walkdir = "2"
7070
# `TestHostConfig` — the concrete `MemoryHostConfig` the extracted test suites
7171
# build, since `Config` is a trait object and cannot be `Default`ed.
7272
tinymemory-api = { path = "../api", features = ["test-support"] }
73+
# The driver conformance suite (#18 §E1). A dev-dependency only: it exists to
74+
# hold this crate's own store to the same contract the adapters are held to.
75+
# No cycle — `tinymemory-conformance` depends on `tinymemory-api` alone.
76+
tinymemory-conformance = { path = "../conformance" }
7377
tempfile = "3"
7478
tokio = { version = "1", features = ["test-util"] }
7579

core/src/store/factories.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,71 @@ pub fn create_memory(
353353
create_memory_full(config, &[], None, None, "", workspace_dir)
354354
}
355355

356+
/// Bind this crate's own store as a [`MemoryProvider`], the way every other
357+
/// engine in the workspace is bound.
358+
///
359+
/// Issue #18 §A3/§A5. Until this existed, `tinymemory-core`'s
360+
/// [`UnifiedMemory`] was the one storage
361+
/// implementation in the workspace with no route through the contract: the
362+
/// TinyCortex adapter binds the bundled engine, `adapters/remote` binds the
363+
/// three hosted services, and core's own SQLite store was reachable only by
364+
/// naming its concrete type. A host could therefore not treat "the store
365+
/// tinymemory ships with" as a driver, which is the whole premise of the
366+
/// registry — and `create_memory` returning `Box<dyn Memory>` is exactly the
367+
/// bypass §A3 names.
368+
///
369+
/// Nothing structural was missing, which is worth recording because it was
370+
/// mis-diagnosed at one point as needing the crate split: `UnifiedMemory`
371+
/// already implements [`Memory`], and
372+
/// [`MemoryTraitProvider`] already wraps any `Memory` into a provider. This is
373+
/// the one-line composition the adapters have been doing all along.
374+
///
375+
/// # Capabilities
376+
///
377+
/// The returned provider advertises the mandatory three — Core, Recall,
378+
/// Portability — and nothing else, because that is what [`Memory`] can express.
379+
/// `UnifiedMemory` implements more than that internally (trees, chunks,
380+
/// entities), but those reach the caller through their own concrete APIs rather
381+
/// than through an optional family accessor, so advertising them here would be
382+
/// a claim `audit_provider` correctly rejects. Widening that is §C3's shape of
383+
/// work, not this function's.
384+
///
385+
/// # Errors
386+
///
387+
/// Propagates whatever [`create_memory`] fails with — a store that cannot open
388+
/// cannot be bound.
389+
///
390+
/// [`MemoryProvider`]: tinymemory_api::provider::MemoryProvider
391+
/// [`Memory`]: tinymemory_api::traits::Memory
392+
/// [`MemoryTraitProvider`]: tinymemory::mandatory::MemoryTraitProvider
393+
pub fn create_memory_provider(
394+
config: &MemoryConfig,
395+
workspace_dir: &Path,
396+
) -> anyhow::Result<Arc<dyn tinymemory_api::provider::MemoryProvider>> {
397+
let memory = create_memory(config, workspace_dir)?;
398+
Ok(bind_as_provider(memory))
399+
}
400+
401+
/// Wraps an already-built store as a driver under [`NAMESPACE_DRIVER_ID`].
402+
///
403+
/// Split out from [`create_memory_provider`] so a caller that already holds a
404+
/// store — the migration path, tests, a host that built one through
405+
/// [`create_memory_with_local_ai`] — can bind it without constructing a second
406+
/// one. Constructing twice against one directory is the hazard the host-side
407+
/// bypass allowlists exist to refuse, so the seam that avoids it belongs here
408+
/// rather than at each call site.
409+
///
410+
/// [`NAMESPACE_DRIVER_ID`]: tinymemory::registry::NAMESPACE_DRIVER_ID
411+
#[must_use]
412+
pub fn bind_as_provider(
413+
memory: Box<dyn Memory>,
414+
) -> Arc<dyn tinymemory_api::provider::MemoryProvider> {
415+
Arc::new(tinymemory::mandatory::MemoryTraitProvider::new(
416+
Arc::from(memory),
417+
tinymemory::registry::NAMESPACE_DRIVER_ID,
418+
))
419+
}
420+
356421
/// Create a memory instance honouring the unified per-workload embedding
357422
/// provider.
358423
///
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//! `tinymemory-core`'s own store, held to the driver contract (#18 §A3/§E1).
2+
//!
3+
//! The TinyCortex adapter and the three hosted adapters each have a
4+
//! `conformance_test.rs` asserting they uphold `MemoryProvider`. This crate's
5+
//! own store had no such file, because until `create_memory_provider` there was
6+
//! no way to express it as a driver at all — which is precisely the gap §A3
7+
//! describes. These are the missing equivalents.
8+
9+
// `expect` is the assertion mechanism here, and its message is the failure
10+
// diagnostic — `expect_used` is `warn` workspace-wide (Cargo.toml) and CI runs
11+
// clippy with `-D warnings`. Scoped to that one lint: unlike the sibling
12+
// conformance tests this file has no explicit `panic!`, so it does not need
13+
// `clippy::panic` too.
14+
#![allow(clippy::expect_used)]
15+
16+
use std::sync::Arc;
17+
18+
use tinymemory_api::host::MemoryConfig;
19+
use tinymemory_api::provider::{audit_provider, MemoryProvider};
20+
21+
use super::factories::create_memory_provider;
22+
23+
/// Builds a provider over a real store in a throwaway directory.
24+
///
25+
/// A temp dir rather than an in-memory backend on purpose: `UnifiedMemory` is a
26+
/// SQLite store, and a driver that only ever answered from memory would not be
27+
/// the thing hosts actually bind.
28+
fn provider(dir: &std::path::Path) -> Arc<dyn MemoryProvider> {
29+
// The store resolves its embedder through the process-global `EmbeddingHost`
30+
// and refuses to open without one. `init` is the crate's idempotent stub
31+
// installer, so this is the same seam every other core test uses rather
32+
// than a second setup path.
33+
crate::test_seams::init();
34+
create_memory_provider(&MemoryConfig::default(), dir).expect("the bundled store opens")
35+
}
36+
37+
#[tokio::test]
38+
async fn the_core_store_upholds_the_contract() {
39+
let dir = tempfile::tempdir().expect("temp dir");
40+
tinymemory_conformance::assert_provider(provider(dir.path())).await;
41+
}
42+
43+
#[tokio::test]
44+
async fn the_core_store_actually_retains() {
45+
// The conformance suite tolerates a driver that refuses a write; without
46+
// this probe a store that silently retained nothing could pass it
47+
// vacuously. That is not hypothetical — it is how a broken double slipped
48+
// through review once already.
49+
let dir = tempfile::tempdir().expect("temp dir");
50+
assert!(
51+
tinymemory_conformance::retains_writes(provider(dir.path()).as_ref()).await,
52+
"the bundled store reported success and kept nothing"
53+
);
54+
}
55+
56+
#[tokio::test]
57+
async fn it_binds_under_the_reserved_namespace_id() {
58+
let dir = tempfile::tempdir().expect("temp dir");
59+
assert_eq!(
60+
provider(dir.path()).driver_id(),
61+
tinymemory::registry::NAMESPACE_DRIVER_ID,
62+
"the bundled store must not bind under another engine's id"
63+
);
64+
}
65+
66+
#[tokio::test]
67+
async fn its_advertised_capabilities_match_what_it_exposes() {
68+
// `audit_provider` is the honesty check: advertised families must equal
69+
// reachable accessors. Wrapping through `MemoryTraitProvider` derives the
70+
// advertisement from the accessors, so this should hold by construction —
71+
// it runs because that construction lives in another crate.
72+
let dir = tempfile::tempdir().expect("temp dir");
73+
audit_provider(provider(dir.path()).as_ref()).expect("the bundled store is honest");
74+
}
75+
76+
#[tokio::test]
77+
async fn the_registry_admits_it_as_an_embedded_driver() {
78+
use tinymemory::registry::{DriverClass, DriverRegistry, NAMESPACE_DRIVER_ID};
79+
80+
// A reserved id with no admission path would be a driver nothing can bind.
81+
let admitted = DriverRegistry::builtin()
82+
.admit(NAMESPACE_DRIVER_ID, None, Default::default())
83+
.expect("the bundled store is admissible");
84+
assert_eq!(admitted.class, DriverClass::Embedded);
85+
}

core/src/store/memory_trait.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,9 +370,14 @@ impl Memory for UnifiedMemory {
370370
let ns = UnifiedMemory::sanitize_namespace(namespace);
371371
let key = crate::store::safety::canonical_document_key(key);
372372
let conn = self.conn.lock();
373-
let row: Option<(String, String, String, f64, String, String)> = conn
373+
// `session_id` is selected here for the same reason `list` selects it:
374+
// it is a column on this row, and a `get` that dropped it made the two
375+
// readers disagree about one record. The contract's round-trip
376+
// assertion catches exactly that (`tinymemory_conformance`), and it was
377+
// invisible until #18 §A3 let this store be bound as a driver at all.
378+
let row: Option<(String, String, String, f64, String, String, Option<String>)> = conn
374379
.query_row(
375-
"SELECT document_id, key, content, updated_at, category, taint
380+
"SELECT document_id, key, content, updated_at, category, taint, session_id
376381
FROM memory_docs WHERE namespace = ?1 AND key = ?2 LIMIT 1",
377382
params![ns, key],
378383
|row| {
@@ -383,19 +388,20 @@ impl Memory for UnifiedMemory {
383388
row.get(3)?,
384389
row.get(4)?,
385390
row.get(5)?,
391+
row.get(6)?,
386392
))
387393
},
388394
)
389395
.optional()?;
390396
Ok(row.map(
391-
|(id, key, content, updated_at, category, taint_str)| MemoryEntry {
397+
|(id, key, content, updated_at, category, taint_str, session_id)| MemoryEntry {
392398
id,
393399
key,
394400
content,
395401
namespace: Some(ns.clone()),
396402
category: memory_category_from_stored(&category),
397403
timestamp: timestamp_to_rfc3339(updated_at),
398-
session_id: None,
404+
session_id,
399405
score: None,
400406
taint: crate::MemoryTaint::from_db_str(&taint_str),
401407
},

core/src/store/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub mod types;
3939

4040
mod client;
4141
pub mod factories;
42+
#[cfg(test)]
43+
mod factories_provider_test;
4244
/// Golden-workspace fixture seeding / read-back / schema-manifest capture.
4345
///
4446
/// Public only so `tests/memory_golden_fixture_e2e.rs` can drive it; it needs

src/registry/mod.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ pub use tinymemory_api::null::NULL_DRIVER_ID;
6161
/// still refuse to bind something *else* under this name.
6262
pub const TINYCORTEX_DRIVER_ID: &str = "tinycortex";
6363

64+
/// The driver id of `tinymemory-core`'s own in-process store.
65+
///
66+
/// Distinct from [`TINYCORTEX_DRIVER_ID`], and the distinction is the point:
67+
/// that one names the bundled TinyCortex engine, this one names
68+
/// `tinymemory_core::store::UnifiedMemory` — a separate SQLite store this
69+
/// workspace implements itself and which, until now, no host could reach
70+
/// through [`MemoryProvider`]. Both are `Embedded`; they are not the same
71+
/// engine, and a host that binds one has not bound the other.
72+
///
73+
/// Named for what `create_memory` has always called this backend
74+
/// (`effective_memory_backend_name` returns `"namespace"`), so the id an
75+
/// operator sees in status matches the name already in the logs rather than
76+
/// introducing a third vocabulary for one store.
77+
///
78+
/// [`MemoryProvider`]: tinymemory_api::provider::MemoryProvider
79+
pub const NAMESPACE_DRIVER_ID: &str = "namespace";
80+
6481
/// Driver id of the native Supermemory HTTP adapter.
6582
pub const SUPERMEMORY_DRIVER_ID: &str = "supermemory";
6683

@@ -169,13 +186,15 @@ impl Default for DriverRegistry {
169186
}
170187

171188
impl DriverRegistry {
172-
/// The registry every host starts from: the null placeholder, TinyCortex,
173-
/// and the three supported native HTTP engines.
189+
/// The registry every host starts from: the null placeholder, the two
190+
/// embedded engines — TinyCortex and this workspace's own `namespace`
191+
/// store — and the three supported native HTTP engines.
174192
#[must_use]
175193
pub fn builtin() -> Self {
176194
let mut reserved = BTreeMap::new();
177195
reserved.insert(NULL_DRIVER_ID.to_string(), DriverClass::Null);
178196
reserved.insert(TINYCORTEX_DRIVER_ID.to_string(), DriverClass::Embedded);
197+
reserved.insert(NAMESPACE_DRIVER_ID.to_string(), DriverClass::Embedded);
179198
reserved.insert(SUPERMEMORY_DRIVER_ID.to_string(), DriverClass::External);
180199
reserved.insert(MEM0_DRIVER_ID.to_string(), DriverClass::External);
181200
reserved.insert(COGNEE_DRIVER_ID.to_string(), DriverClass::External);

0 commit comments

Comments
 (0)