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. /// /// Local files are the source of truth. The DB is a queryable projection /// rebuilt from files on daemon startup and on watcher-detected changes. /// Uses UPSERT semantics — idempotent, safe to call on every change. #[cfg(feature = "db")] pub async fn seed_ontology( db: &stratum_db::StratumDb, 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, "node", &json, "nodes").await; report.edges = seed_edges(db, &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, "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, "membrane", &json, "membranes").await; } Err(e) => warn!(error = %e, "seed: gate.ncl export failed"), } } info!( nodes = report.nodes, edges = report.edges, dimensions = report.dimensions, membranes = report.membranes, "ontology seeded from local files" ); report } /// Generic upsert: extract `json[array_key]`, iterate, use each item's `id` /// field as record key. #[cfg(feature = "db")] async fn seed_table_by_id( db: &stratum_db::StratumDb, 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 id = match item.get("id").and_then(|i| i.as_str()) { Some(id) => id, None => continue, }; if let Err(e) = db.upsert(table, id, item.clone()).await { warn!(table, id, error = %e, "seed: upsert failed"); continue; } count += 1; } count } /// Edges use a deterministic compound key: `{from}--{kind}--{to}`. #[cfg(feature = "db")] async fn seed_edges(db: &stratum_db::StratumDb, 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!("{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 }