use std::path::Path; use serde_json::Value; use tracing::{info, warn}; use crate::cache::NclCache; /// Counts of records upserted per table during a seed pass. pub struct SeedReport { pub nodes: usize, pub edges: usize, pub dimensions: usize, pub membranes: usize, } /// Seed ontology tables from local NCL files into SurrealDB. /// /// All record IDs are prefixed with `slug` (`{slug}--{id}`) to ensure /// multi-project isolation within shared tables. Records from different /// projects never collide even when they share the same ontology ID. #[cfg(feature = "db")] pub async fn seed_ontology( db: &stratum_db::StratumDb, slug: &str, project_root: &Path, cache: &NclCache, import_path: Option<&str>, ) -> SeedReport { let mut report = SeedReport { nodes: 0, edges: 0, dimensions: 0, membranes: 0, }; let core_path = project_root.join(".ontology").join("core.ncl"); if core_path.exists() { match cache.export(&core_path, import_path).await { Ok((json, _)) => { report.nodes = seed_table_by_id(db, slug, "node", &json, "nodes").await; report.edges = seed_edges(db, slug, &json).await; } Err(e) => warn!(error = %e, "seed: core.ncl export failed"), } } let state_path = project_root.join(".ontology").join("state.ncl"); if state_path.exists() { match cache.export(&state_path, import_path).await { Ok((json, _)) => { report.dimensions = seed_table_by_id(db, slug, "dimension", &json, "dimensions").await; } Err(e) => warn!(error = %e, "seed: state.ncl export failed"), } } let gate_path = project_root.join(".ontology").join("gate.ncl"); if gate_path.exists() { match cache.export(&gate_path, import_path).await { Ok((json, _)) => { report.membranes = seed_table_by_id(db, slug, "membrane", &json, "membranes").await; } Err(e) => warn!(error = %e, "seed: gate.ncl export failed"), } } info!( slug, nodes = report.nodes, edges = report.edges, dimensions = report.dimensions, membranes = report.membranes, "ontology seeded from local files" ); report } /// Seed ontology tables from a pre-exported JSON payload (push-based sync). /// /// `slug` scopes all record IDs — see `seed_ontology` for the isolation model. #[cfg(feature = "db")] pub async fn seed_from_payload( db: &stratum_db::StratumDb, slug: &str, payload: &serde_json::Value, ) -> SeedReport { let mut report = SeedReport { nodes: 0, edges: 0, dimensions: 0, membranes: 0, }; if let Some(core) = payload.get("core") { report.nodes = seed_table_by_id(db, slug, "node", core, "nodes").await; report.edges = seed_edges(db, slug, core).await; } if let Some(state) = payload.get("state") { report.dimensions = seed_table_by_id(db, slug, "dimension", state, "dimensions").await; } if let Some(gate) = payload.get("gate") { report.membranes = seed_table_by_id(db, slug, "membrane", gate, "membranes").await; } info!( slug, nodes = report.nodes, edges = report.edges, dimensions = report.dimensions, membranes = report.membranes, "ontology seeded from push payload" ); report } /// Generic upsert: extract `json[array_key]`, iterate, use each item's `id` /// field as record key prefixed with `{slug}--` for multi-project isolation. #[cfg(feature = "db")] async fn seed_table_by_id( db: &stratum_db::StratumDb, slug: &str, table: &str, json: &Value, array_key: &str, ) -> usize { let items = match json.get(array_key).and_then(|a| a.as_array()) { Some(arr) => arr, None => return 0, }; let mut count = 0; for item in items { let raw_id = match item.get("id").and_then(|i| i.as_str()) { Some(id) => id, None => continue, }; let id = format!("{slug}--{raw_id}"); // Rewrite the `id` field to the prefixed value so the content is // consistent with the record specifier (`table:id`). SurrealDB rejects // upserts where the `id` field in the content differs from the // targeted record ID. let mut record = item.clone(); record["id"] = Value::String(id.clone()); if let Err(e) = db.upsert(table, &id, record).await { warn!(table, id, error = %e, "seed: upsert failed"); continue; } count += 1; } count } /// Edges use a deterministic compound key: `{slug}--{from}--{kind}--{to}`. #[cfg(feature = "db")] async fn seed_edges(db: &stratum_db::StratumDb, slug: &str, core_json: &Value) -> usize { let edges = match core_json.get("edges").and_then(|e| e.as_array()) { Some(arr) => arr, None => return 0, }; let mut count = 0; for edge in edges { let from = edge.get("from").and_then(|f| f.as_str()).unwrap_or(""); let to = edge.get("to").and_then(|t| t.as_str()).unwrap_or(""); let kind = edge .get("kind") .and_then(|k| k.as_str()) .unwrap_or("unknown"); let edge_id = format!("{slug}--{from}--{kind}--{to}"); if let Err(e) = db.upsert("edge", &edge_id, edge.clone()).await { warn!(edge_id, error = %e, "seed: edge upsert failed"); continue; } count += 1; } count }