ontoref-code/crates/ontoref-daemon/src/domain.rs
Jesús Pérez 234432856c
feat(daemon,ops): daemon surface + ontology-content implementation
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
2026-07-19 18:23:49 +01:00

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");
}
}