182 lines
5.5 KiB
Rust
Raw Normal View History

2026-03-13 00:18:14 +00:00
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.
2026-03-13 00:18:14 +00:00
#[cfg(feature = "db")]
pub async fn seed_ontology(
db: &stratum_db::StratumDb,
slug: &str,
2026-03-13 00:18:14 +00:00
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;
2026-03-13 00:18:14 +00:00
}
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;
2026-03-13 00:18:14 +00:00
}
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;
2026-03-13 00:18:14 +00:00
}
Err(e) => warn!(error = %e, "seed: gate.ncl export failed"),
}
}
info!(
slug,
2026-03-13 00:18:14 +00:00
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
}
2026-03-13 00:18:14 +00:00
/// Generic upsert: extract `json[array_key]`, iterate, use each item's `id`
/// field as record key prefixed with `{slug}--` for multi-project isolation.
2026-03-13 00:18:14 +00:00
#[cfg(feature = "db")]
async fn seed_table_by_id(
db: &stratum_db::StratumDb,
slug: &str,
2026-03-13 00:18:14 +00:00
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()) {
2026-03-13 00:18:14 +00:00
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 {
2026-03-13 00:18:14 +00:00
warn!(table, id, error = %e, "seed: upsert failed");
continue;
}
count += 1;
}
count
}
/// Edges use a deterministic compound key: `{slug}--{from}--{kind}--{to}`.
2026-03-13 00:18:14 +00:00
#[cfg(feature = "db")]
async fn seed_edges(db: &stratum_db::StratumDb, slug: &str, core_json: &Value) -> usize {
2026-03-13 00:18:14 +00:00
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}");
2026-03-13 00:18:14 +00:00
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
}