Merge M4 — kei-crossdomain migration

This commit is contained in:
Parfii-bot 2026-04-23 05:55:35 +08:00
commit 91f9f050e1
4 changed files with 90 additions and 17 deletions

View file

@ -1978,6 +1978,7 @@ dependencies = [
"anyhow",
"chrono",
"clap",
"kei-entity-store",
"rusqlite",
"serde",
"serde_json",

View file

@ -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"] }

View file

@ -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(())
}

View file

@ -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<Self> {
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<Self> {
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()
}
}