diff --git a/_primitives/_rust/Cargo.lock b/_primitives/_rust/Cargo.lock index bf44e3d..1a6b205 100644 --- a/_primitives/_rust/Cargo.lock +++ b/_primitives/_rust/Cargo.lock @@ -1978,6 +1978,7 @@ dependencies = [ "anyhow", "chrono", "clap", + "kei-entity-store", "rusqlite", "serde", "serde_json", diff --git a/_primitives/_rust/kei-crossdomain/Cargo.toml b/_primitives/_rust/kei-crossdomain/Cargo.toml index 88e5194..56b1c67 100644 --- a/_primitives/_rust/kei-crossdomain/Cargo.toml +++ b/_primitives/_rust/kei-crossdomain/Cargo.toml @@ -14,6 +14,7 @@ name = "kei_crossdomain" path = "src/lib.rs" [dependencies] +kei-entity-store = { path = "../kei-entity-store" } rusqlite = { version = "0.31", features = ["bundled"] } clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } diff --git a/_primitives/_rust/kei-crossdomain/src/schema.rs b/_primitives/_rust/kei-crossdomain/src/schema.rs index 796874f..056e939 100644 --- a/_primitives/_rust/kei-crossdomain/src/schema.rs +++ b/_primitives/_rust/kei-crossdomain/src/schema.rs @@ -1,7 +1,42 @@ +//! kei-crossdomain EntitySchema — declarative spec consumed by +//! `kei_entity_store::Store` for migrations + user_version pragma. +//! +//! **Architectural note (2026-04-23 migration to Layer-A engine):** +//! kei-crossdomain is an edges-only graph store — URIs (`domain://path`) +//! are the only identifiers; there is no primary "node" entity row. The +//! engine's `EntitySchema` contract requires exactly one `IntegerPk` +//! field, so we declare a minimal synthetic `cross_nodes` table purely +//! to satisfy the DDL contract. No code writes to this table; every +//! query still runs against `cross_edges`. +//! +//! **Why `edge_table: None` instead of `Some("cross_edges")` + TextPair:** +//! the engine's TextPair edge DDL is `(src_path, dst_path, edge_type)` +//! with `PRIMARY KEY(src_path, dst_path, edge_type)` — **incompatible** +//! with the existing `cross_edges` schema, which carries five extra +//! columns (`id INTEGER PRIMARY KEY`, `weight`, `evidence`, `metadata`, +//! `created_at`) and uses column names `from_uri` / `to_uri`. Adopting +//! engine's TextPair would destroy the `CrossEdge` type, the +//! `link()` rowid return, and backward compatibility with existing +//! on-disk DBs. Instead we follow the kei-task pattern: ride the engine +//! for connection lifecycle + `PRAGMA user_version` + migration +//! orchestration, keep the rich edge DDL in `custom_migrations`. +//! +//! Constructive path (not pursued here, would require destructive +//! rewrite): extend `kei-entity-store` with a richer TextPair variant +//! that preserves extra columns via schema fields, OR migrate +//! kei-crossdomain callers to drop the id/weight/evidence/metadata +//! fields. Both are multi-file changes outside this crate's scope. + +use kei_entity_store::schema::{EdgeKeyKind, EntitySchema, FieldDef}; use rusqlite::{Connection, Result}; -pub fn create_schema(conn: &Connection) -> Result<()> { - conn.execute_batch(r#" +/// Synthetic primary table — exists solely to satisfy the engine's +/// `IntegerPk` requirement. Not used by any verb or caller. +static FIELDS: &[FieldDef] = &[FieldDef::pk("id")]; + +/// Byte-identical to the pre-migration `cross_edges` DDL (plus the +/// three indexes that used to live in the same `execute_batch`). +const DDL_CROSS_EDGES: &str = r#" CREATE TABLE IF NOT EXISTS cross_edges ( id INTEGER PRIMARY KEY, from_uri TEXT NOT NULL, @@ -16,6 +51,32 @@ pub fn create_schema(conn: &Connection) -> Result<()> { CREATE INDEX IF NOT EXISTS idx_ce_from ON cross_edges(from_uri); CREATE INDEX IF NOT EXISTS idx_ce_to ON cross_edges(to_uri); CREATE INDEX IF NOT EXISTS idx_ce_type ON cross_edges(edge_type); - "#)?; +"#; + +pub static CROSSDOMAIN_SCHEMA: EntitySchema = EntitySchema { + name: "crossdomain", + table: "cross_nodes", + fields: FIELDS, + // Empty verb set: every kei-crossdomain op is bespoke (TextPair with + // extra columns — engine verbs can't dispatch them). Link/unlink/ + // query/BFS/auto-link/stats all live in `edges.rs`, `bfs.rs`, + // `auto_link.rs`. + enabled_verbs: &[], + fts_columns: None, + // `None`: engine skips edge DDL. `cross_edges` is created via + // `custom_migrations` with byte-identical legacy DDL. + edge_table: None, + // Documentation hint only (inert while `edge_table = None`). + edge_key_kind: EdgeKeyKind::TextPair, + archived_field: None, + custom_migrations: &[DDL_CROSS_EDGES], +}; + +/// Kept for backward compatibility with any external caller that +/// imported `schema::create_schema` directly. New code should open via +/// `Store::open` / `Store::open_memory`, which invokes the engine's +/// migration runner with `CROSSDOMAIN_SCHEMA`. +pub fn create_schema(conn: &Connection) -> Result<()> { + conn.execute_batch(DDL_CROSS_EDGES)?; Ok(()) } diff --git a/_primitives/_rust/kei-crossdomain/src/store.rs b/_primitives/_rust/kei-crossdomain/src/store.rs index af862a3..251e552 100644 --- a/_primitives/_rust/kei-crossdomain/src/store.rs +++ b/_primitives/_rust/kei-crossdomain/src/store.rs @@ -1,28 +1,38 @@ -use crate::schema::create_schema; -use anyhow::{Context, Result}; +//! Crossdomain store — thin shim over `kei_entity_store::Store`. +//! +//! Layer-A convergence (2026-04-23): connection lifecycle + migrations + +//! `PRAGMA user_version` stamping now ride the shared engine via +//! `CROSSDOMAIN_SCHEMA`. Public surface preserved byte-for-byte so +//! `edges.rs`, `bfs.rs`, `auto_link.rs`, and integration tests compile +//! unchanged. +//! +//! Generic CRUD verbs are NOT wired here — kei-crossdomain is an +//! edges-only store with bespoke TextPair+extras columns; see +//! `schema.rs` for the architectural note on why engine's `link`/`rank` +//! verbs cannot serve this crate without a destructive schema rewrite. + +use crate::schema::CROSSDOMAIN_SCHEMA; +use anyhow::Result; +use kei_entity_store::Store as EntityStore; use rusqlite::Connection; use std::path::Path; pub struct Store { - conn: Connection, + inner: EntityStore, } impl Store { pub fn open(path: &Path) -> Result { - if let Some(parent) = path.parent() { - let _ = std::fs::create_dir_all(parent); - } - let conn = Connection::open(path).context("open sqlite")?; - conn.pragma_update(None, "journal_mode", "WAL").ok(); - create_schema(&conn)?; - Ok(Self { conn }) + let inner = EntityStore::open(path, &CROSSDOMAIN_SCHEMA)?; + Ok(Self { inner }) } pub fn open_memory() -> Result { - let conn = Connection::open_in_memory()?; - create_schema(&conn)?; - Ok(Self { conn }) + let inner = EntityStore::open_memory(&CROSSDOMAIN_SCHEMA)?; + Ok(Self { inner }) } - pub fn conn(&self) -> &Connection { &self.conn } + pub fn conn(&self) -> &Connection { + self.inner.conn() + } }