Substantive changes across the crate tree (cargo check --workspace: clean): - ontoref-daemon: UI handlers/pages, MCP surface, api, domain query delegation - ontoref-ops: runtime - ontoref-ontology / ontoref-ontology-content / oplog / triples / commit: supporting - domains/rustelo: schema surface sync
274 lines
8.5 KiB
Rust
274 lines
8.5 KiB
Rust
//! Domain graph navigation — project ↔ domain bidirectional discovery.
|
|
//!
|
|
//! Detects domain implementations from:
|
|
//! (a) `config.ncl`: `domain_<id> = true`
|
|
//! (b) `manifest.ncl`: `domain_origin = { id = "...", path = "...", ... }`
|
|
//! (c) `.{domain}.ontoref/` directory presence
|
|
|
|
use std::path::Path;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::registry::ProjectRegistry;
|
|
|
|
/// How a domain was declared in a project.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum DomainDeclKind {
|
|
ConfigFlag,
|
|
DomainOrigin,
|
|
ImplDir,
|
|
}
|
|
|
|
/// A domain implementation declaration found in a project.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DomainDecl {
|
|
pub id: String,
|
|
pub kind: DomainDeclKind,
|
|
/// Source path from `domain_origin`, empty otherwise.
|
|
pub path: String,
|
|
}
|
|
|
|
/// A project that implements a given domain.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DomainConsumer {
|
|
pub slug: String,
|
|
pub root: String,
|
|
pub kind: DomainDeclKind,
|
|
}
|
|
|
|
/// Extract `domain_<id> = true` declarations from a file's text content.
|
|
fn extract_config_flags(content: &str) -> Vec<String> {
|
|
content
|
|
.lines()
|
|
.filter_map(|line| {
|
|
let trimmed = line.trim();
|
|
// Match: domain_<alphanum_> = true
|
|
let rest = trimmed.strip_prefix("domain_")?;
|
|
let eq_pos = rest.find('=')?;
|
|
let id = rest[..eq_pos].trim();
|
|
let val = rest[eq_pos + 1..].trim();
|
|
if val.starts_with("true")
|
|
&& id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|
|
&& !id.is_empty()
|
|
{
|
|
Some(id.to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Extract `domain_origin.id` and `domain_origin.path` from manifest text.
|
|
fn extract_domain_origin(content: &str) -> Option<(String, String)> {
|
|
if !content.contains("domain_origin") {
|
|
return None;
|
|
}
|
|
// Find the domain_origin block start then scan for id and path fields.
|
|
let block_start = content.find("domain_origin")?;
|
|
let block = &content[block_start..];
|
|
let open = block.find('{')? + 1;
|
|
// Find matching closing brace.
|
|
let mut depth = 1usize;
|
|
let mut close = open;
|
|
for (i, ch) in block[open..].char_indices() {
|
|
match ch {
|
|
'{' => depth += 1,
|
|
'}' => {
|
|
depth -= 1;
|
|
if depth == 0 {
|
|
close = open + i;
|
|
break;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
let inner = &block[open..close];
|
|
|
|
let id = inner.lines().find_map(|l| {
|
|
let t = l.trim();
|
|
let rest = t.strip_prefix("id")?.trim_start_matches([' ', '=']).trim();
|
|
rest.strip_prefix('"')?
|
|
.split('"')
|
|
.next()
|
|
.map(|s| s.replace("-framework", "").replace("-domain", ""))
|
|
})?;
|
|
|
|
let path = inner
|
|
.lines()
|
|
.find_map(|l| {
|
|
let t = l.trim();
|
|
let rest = t
|
|
.strip_prefix("path")?
|
|
.trim_start_matches([' ', '='])
|
|
.trim();
|
|
rest.strip_prefix('"')?.split('"').next().map(String::from)
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
Some((id, path))
|
|
}
|
|
|
|
/// Read domain declarations for a project root (offline, fast grep).
|
|
pub fn project_domains(root: &Path) -> Vec<DomainDecl> {
|
|
let mut domains: Vec<DomainDecl> = Vec::new();
|
|
|
|
// (a) config.ncl: domain_<id> = true
|
|
let config_path = root.join(".ontoref").join("config.ncl");
|
|
if let Ok(content) = std::fs::read_to_string(&config_path) {
|
|
for id in extract_config_flags(&content) {
|
|
if !domains.iter().any(|d| d.id == id) {
|
|
domains.push(DomainDecl {
|
|
id,
|
|
kind: DomainDeclKind::ConfigFlag,
|
|
path: String::new(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// (b) manifest.ncl: domain_origin block
|
|
let manifest_path = root.join(".ontoref").join("ontology").join("manifest.ncl");
|
|
if let Ok(content) = std::fs::read_to_string(&manifest_path) {
|
|
if let Some((id, path)) = extract_domain_origin(&content) {
|
|
if !domains.iter().any(|d| d.id == id) {
|
|
domains.push(DomainDecl {
|
|
id,
|
|
kind: DomainDeclKind::DomainOrigin,
|
|
path,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// (c) .{domain}.ontoref/ directory presence
|
|
if let Ok(entries) = std::fs::read_dir(root) {
|
|
for entry in entries.flatten() {
|
|
let name = entry.file_name().to_string_lossy().into_owned();
|
|
if name.starts_with('.')
|
|
&& name.ends_with(".ontoref")
|
|
&& name != ".ontoref"
|
|
&& entry.path().is_dir()
|
|
{
|
|
let id = name
|
|
.trim_start_matches('.')
|
|
.trim_end_matches(".ontoref")
|
|
.to_string();
|
|
if !domains.iter().any(|d| d.id == id) {
|
|
domains.push(DomainDecl {
|
|
id,
|
|
kind: DomainDeclKind::ImplDir,
|
|
path: String::new(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
domains
|
|
}
|
|
|
|
/// List all registered projects implementing a given domain.
|
|
pub fn consumers_of(registry: &ProjectRegistry, domain_id: &str) -> Vec<DomainConsumer> {
|
|
registry
|
|
.all()
|
|
.into_iter()
|
|
.filter_map(|ctx| {
|
|
let domains = project_domains(&ctx.root);
|
|
domains
|
|
.into_iter()
|
|
.find(|d| d.id == domain_id)
|
|
.map(|d| DomainConsumer {
|
|
slug: ctx.slug.clone(),
|
|
root: ctx.root.to_string_lossy().into_owned(),
|
|
kind: d.kind,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Registered domain ids (directories under $ONTOREF_ROOT/domains/ with
|
|
/// domain.ncl).
|
|
pub fn registered_domain_ids(ontoref_root: &Path) -> Vec<String> {
|
|
let domains_dir = ontoref_root.join("domains");
|
|
std::fs::read_dir(&domains_dir)
|
|
.into_iter()
|
|
.flatten()
|
|
.flatten()
|
|
.filter(|e| e.path().is_dir() && e.path().join("domain.ncl").exists())
|
|
.map(|e| e.file_name().to_string_lossy().into_owned())
|
|
.collect()
|
|
}
|
|
|
|
/// Light metadata from domain.ncl — fast grep, no nickel export.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct DomainMeta {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub schema_cmd: String,
|
|
}
|
|
|
|
fn extract_ncl_string_field(content: &str, field: &str) -> String {
|
|
content
|
|
.lines()
|
|
.find_map(|l| {
|
|
let t = l.trim();
|
|
let rest = t.strip_prefix(field)?.trim_start_matches([' ', '=']).trim();
|
|
rest.strip_prefix('"')?.split('"').next().map(String::from)
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn domain_meta(ontoref_root: &Path, domain_id: &str) -> Option<DomainMeta> {
|
|
let ncl = ontoref_root
|
|
.join("domains")
|
|
.join(domain_id)
|
|
.join("domain.ncl");
|
|
let content = std::fs::read_to_string(&ncl).ok()?;
|
|
Some(DomainMeta {
|
|
id: domain_id.to_string(),
|
|
name: extract_ncl_string_field(&content, "name"),
|
|
description: extract_ncl_string_field(&content, "description"),
|
|
schema_cmd: extract_ncl_string_field(&content, "schema_cmd"),
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn extract_config_flags_finds_domain_declarations() {
|
|
let content = "ops = { tier = 'Tier0 },\n domain_provisioning = true,\n domain_rustelo \
|
|
= true,\n other = false,";
|
|
let flags = extract_config_flags(content);
|
|
assert!(flags.contains(&"provisioning".to_string()));
|
|
assert!(flags.contains(&"rustelo".to_string()));
|
|
assert_eq!(flags.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn extract_domain_origin_parses_block() {
|
|
let content = r#"
|
|
domain_origin = {
|
|
id = "rustelo-framework",
|
|
path = "/dev/rustelo",
|
|
},
|
|
"#;
|
|
let result = extract_domain_origin(content);
|
|
assert!(result.is_some());
|
|
let (id, path) = result.unwrap();
|
|
assert_eq!(id, "rustelo");
|
|
assert_eq!(path, "/dev/rustelo");
|
|
}
|
|
|
|
#[test]
|
|
fn extract_domain_origin_strips_framework_suffix() {
|
|
let content = r#"domain_origin = { id = "librosys-domain", path = "/dev/lb" },"#;
|
|
let (id, _) = extract_domain_origin(content).unwrap();
|
|
assert_eq!(id, "librosys");
|
|
}
|
|
}
|