use serde::Serialize;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct TopologyNode {
pub id: String,
pub title: String,
pub entity_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub community: Option<String>,
pub stub: bool,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct TopologyEdge {
pub source: String,
pub target: String,
pub rel_type: String,
pub target_in_mem: bool,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct TopologyCommunity {
pub id: String,
pub size_in_mem: usize,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct MemTopology {
pub mem: String,
pub nodes: Vec<TopologyNode>,
pub edges: Vec<TopologyEdge>,
pub communities: Vec<TopologyCommunity>,
}
impl crate::Engine {
pub fn mem_topology(&self, mem: &str) -> Result<MemTopology, crate::EngineError> {
if self.mount(mem).is_none() {
return Err(self.unknown_mem_error(mem));
}
let store = self.store();
let louvain = self.communities();
let mut nodes = Vec::new();
let mut edges = Vec::new();
let mut community_sizes: std::collections::BTreeMap<String, usize> =
std::collections::BTreeMap::new();
for entity in store.all_entities() {
if entity.mem != mem {
continue;
}
let id = entity.id.to_string();
let community = louvain.entity_cluster_map.get(&id).cloned();
if let Some(cluster) = &community {
*community_sizes.entry(cluster.clone()).or_insert(0) += 1;
}
nodes.push(TopologyNode {
id: id.clone(),
title: entity.title.clone(),
entity_type: entity.entity_type.clone(),
community,
stub: entity.stub,
});
for edge in store.outgoing(&entity.id) {
let target_in_mem = store
.get(&edge.target)
.map(|t| t.mem == mem)
.unwrap_or(false);
edges.push(TopologyEdge {
source: id.clone(),
target: edge.target.to_string(),
rel_type: edge.rel_type.clone(),
target_in_mem,
});
}
}
nodes.sort_by(|a, b| a.id.cmp(&b.id));
edges.sort_by(|a, b| {
(&a.source, &a.target, &a.rel_type).cmp(&(&b.source, &b.target, &b.rel_type))
});
let communities = community_sizes
.into_iter()
.map(|(id, size_in_mem)| TopologyCommunity { id, size_in_mem })
.collect();
Ok(MemTopology {
mem: mem.to_string(),
nodes,
edges,
communities,
})
}
}
#[cfg(test)]
mod tests {
use crate::storage::MemWriter;
fn two_mem_engine() -> (crate::Engine, tempfile::TempDir) {
let tmp = tempfile::TempDir::new().unwrap();
let seed = |dir: &std::path::Path, files: &[(&str, &str)]| {
std::fs::create_dir_all(dir).unwrap();
let writer = crate::storage::FilesystemMemWriter::new(dir.to_path_buf());
for (name, body) in files {
writer
.write_entity(std::path::Path::new(name), body.as_bytes())
.unwrap();
}
writer
.commit("seed", &crate::vcs::CommitContext::internal())
.unwrap();
};
let specs_dir = tmp.path().join("specs");
let vendor_dir = tmp.path().join("vendor");
seed(
&specs_dir,
&[
(
"alpha.md",
"---\ntype: spec\ncreated_date: 2026-01-01\nlast_modified: 2026-01-01\nlevel: M0\n---\n# Alpha\n\n## Identity\n\nA.\n\n## Relationships\n\n- **USES**: [[beta]]\n- **DEPENDS_ON**: [[vendor--gamma]]\n",
),
(
"beta.md",
"---\ntype: spec\ncreated_date: 2026-01-01\nlast_modified: 2026-01-01\nlevel: M0\n---\n# Beta\n\n## Identity\n\nB.\n",
),
],
);
seed(
&vendor_dir,
&[(
"gamma.md",
"---\ntype: spec\ncreated_date: 2026-01-01\nlast_modified: 2026-01-01\nlevel: M0\n---\n# Gamma\n\n## Identity\n\nC.\n",
)],
);
let mount = |mem: &str, path: std::path::PathBuf| {
(
crate::Mount {
mem: mem.to_string(),
schema: Some(memstead_schema::SchemaRef::new(
"default",
semver::Version::new(1, 0, 0),
)),
storage: crate::MountStorage::Folder { path: path.clone() },
capability: crate::MountCapability::Write,
lifecycle: crate::MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
},
Box::new(crate::storage::FilesystemMemWriter::new(path))
as Box<dyn crate::MemBackend>,
)
};
let engine = crate::Engine::from_mounts(vec![
mount("specs", specs_dir),
mount("vendor", vendor_dir),
])
.unwrap();
(engine, tmp)
}
#[test]
fn projection_is_faithful_composable_and_coordinate_free() {
let (engine, _tmp) = two_mem_engine();
let specs = engine.mem_topology("specs").unwrap();
let vendor = engine.mem_topology("vendor").unwrap();
let specs_ids: Vec<&str> = specs.nodes.iter().map(|n| n.id.as_str()).collect();
assert_eq!(specs_ids, vec!["specs--alpha", "specs--beta"]);
assert_eq!(vendor.nodes.len(), 1);
assert_eq!(vendor.nodes[0].id, "vendor--gamma");
let cross: Vec<_> = specs
.edges
.iter()
.filter(|e| e.rel_type == "DEPENDS_ON")
.collect();
assert_eq!(cross.len(), 1);
assert_eq!(cross[0].target, "vendor--gamma");
assert!(!cross[0].target_in_mem);
assert!(
vendor.edges.iter().all(|e| e.rel_type != "DEPENDS_ON"),
"cross-mem edge must not repeat at the target mem: {:?}",
vendor.edges
);
let uses: Vec<_> = specs
.edges
.iter()
.filter(|e| e.rel_type == "USES")
.collect();
assert_eq!(uses.len(), 1);
assert!(uses[0].target_in_mem);
let louvain = engine.communities();
for node in specs.nodes.iter().chain(vendor.nodes.iter()) {
assert_eq!(
node.community.as_ref(),
louvain.entity_cluster_map.get(&node.id),
"node {} must carry the global assignment",
node.id
);
}
let json = serde_json::to_string(&specs).unwrap();
for forbidden in ["\"x\":", "\"y\":", "\"z\":", "position", "layout"] {
assert!(
!json.contains(forbidden),
"layout field leaked: {forbidden}"
);
}
let err = engine.mem_topology("ghost").unwrap_err();
assert!(matches!(err, crate::EngineError::UnknownMem(m) if m == "ghost"));
}
}
#[cfg(test)]
mod scale_tests {
use crate::storage::MemWriter;
#[test]
fn design_target_mem_is_served_whole() {
const N: usize = 1_500;
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path().join("bulk");
std::fs::create_dir_all(&dir).unwrap();
let writer = crate::storage::FilesystemMemWriter::new(dir.clone());
for i in 0..N {
let rel = if i > 0 {
format!("\n## Relationships\n\n- **USES**: [[node-{}]]\n", i - 1)
} else {
String::new()
};
let body = format!(
"---\ntype: spec\ncreated_date: 2026-01-01\nlast_modified: 2026-01-01\nlevel: M0\n---\n# Node {i}\n\n## Identity\n\nBulk node {i}.\n{rel}"
);
writer
.write_entity(
std::path::Path::new(&format!("node-{i}.md")),
body.as_bytes(),
)
.unwrap();
}
writer
.commit("seed bulk", &crate::vcs::CommitContext::internal())
.unwrap();
let mount = crate::Mount {
mem: "bulk".to_string(),
schema: Some(memstead_schema::SchemaRef::new(
"default",
semver::Version::new(1, 0, 0),
)),
storage: crate::MountStorage::Folder { path: dir.clone() },
capability: crate::MountCapability::Write,
lifecycle: crate::MountLifecycle::Eager,
cross_linkable: false,
migration_target: None,
};
let backend =
Box::new(crate::storage::FilesystemMemWriter::new(dir)) as Box<dyn crate::MemBackend>;
let engine = crate::Engine::from_mounts(vec![(mount, backend)]).unwrap();
let topology = engine.mem_topology("bulk").unwrap();
assert_eq!(topology.nodes.len(), N, "every node, no cap");
assert_eq!(topology.edges.len(), N - 1, "every edge, no cap");
}
}