use std::collections::HashMap;
use memstead_schema::ConstraintDef;
use super::Engine;
use crate::entity::MetadataValue;
impl Engine {
pub fn render_gates_brief(&self, mem_filter: Option<&str>) -> String {
let checks = self.check_state_provider();
let mut sections: Vec<String> = Vec::new();
let mut declaring_mems: Vec<String> = Vec::new();
let mut mems: Vec<&str> = self
.mounts
.iter()
.map(|m| m.mount.mem.as_str())
.filter(|m| mem_filter.is_none_or(|f| f == *m))
.collect();
mems.sort_unstable();
for mem in mems {
let Some(schema) = self.schemas.get(mem) else {
continue;
};
let mut gated: Vec<(&str, &ConstraintDef)> = Vec::new();
for td in schema.types.values() {
for c in &td.constraints {
if matches!(c, ConstraintDef::TransitionRequiresChecks { .. }) {
gated.push((td.name.as_str(), c));
}
}
}
if gated.is_empty() {
continue;
}
gated.sort_by_key(|(name, _)| *name);
declaring_mems.push(mem.to_string());
let mut lines: Vec<String> = Vec::new();
lines.push(format!("## {mem}"));
lines.push(String::new());
for (type_name, c) in &gated {
let ConstraintDef::TransitionRequiresChecks {
field,
to_value,
relationships,
direction,
..
} = c
else {
continue;
};
lines.push(format!(
"Gate: `{type_name}` — `{field}: {to_value}` requires a fresh confirming \
check record on every entity related via [{}] ({}).",
relationships.join(", "),
match direction {
memstead_schema::PropagationDirection::Incoming => "incoming",
memstead_schema::PropagationDirection::Outgoing => "outgoing",
},
));
lines.push(String::new());
let entities: Vec<_> = self
.store
.all_entities()
.filter(|e| e.mem == mem && !e.stub && e.entity_type == *type_name)
.collect();
let mut closed: Vec<String> = Vec::new();
struct OpenRow {
id: String,
value: String,
total: usize,
unchecked: Vec<crate::ops::health::UncheckedRelated>,
}
let mut open: Vec<OpenRow> = Vec::new();
for e in &entities {
let value = match e.metadata.get(field.as_str()) {
Some(MetadataValue::String(s)) => s.clone(),
Some(v) => v.to_frontmatter_string(),
None => String::new(),
};
if value == *to_value {
closed.push(e.id.0.clone());
} else {
let (total, unchecked) = crate::ops::health::transition_gate_standing(
&self.store,
e,
relationships,
*direction,
None,
Some(&checks),
);
open.push(OpenRow {
id: e.id.0.clone(),
value,
total,
unchecked,
});
}
}
closed.sort();
let acyclic: Vec<&str> = schema
.manifest
.relationships
.definitions
.iter()
.filter(|r| r.acyclic)
.map(|r| r.name.as_str())
.collect();
let open_ids: Vec<String> = open.iter().map(|r| r.id.clone()).collect();
let mut prereqs: HashMap<String, Vec<String>> = HashMap::new();
for e in &entities {
if !open_ids.contains(&e.id.0) {
continue;
}
for rel in &e.relationships {
if acyclic.contains(&rel.rel_type.as_str())
&& open_ids.contains(&rel.target.0)
&& rel.target.0 != e.id.0
{
prereqs
.entry(e.id.0.clone())
.or_default()
.push(rel.target.0.clone());
}
}
}
let mut ordered: Vec<&OpenRow> = Vec::new();
let mut placed: Vec<&str> = Vec::new();
let mut remaining: Vec<&OpenRow> = open.iter().collect();
remaining.sort_by(|a, b| a.id.cmp(&b.id));
while !remaining.is_empty() {
let idx = remaining.iter().position(|r| {
prereqs
.get(&r.id)
.is_none_or(|p| p.iter().all(|d| placed.contains(&d.as_str())))
});
let idx = idx.unwrap_or(0);
let row = remaining.remove(idx);
placed.push(row.id.as_str());
ordered.push(row);
}
lines.push(format!(
"Closed (at `{to_value}`): {}",
if closed.is_empty() {
"none".to_string()
} else {
closed
.iter()
.map(|id| format!("`{id}`"))
.collect::<Vec<_>>()
.join(", ")
}
));
lines.push(String::new());
if ordered.is_empty() {
lines.push("Open: none — every gated entity stands closed.".to_string());
} else {
lines.push("Open, in dependency order (prerequisites first):".to_string());
for row in &ordered {
let coverage = if row.total == 0 {
"no related entities — the gate is vacuously satisfiable".to_string()
} else if row.unchecked.is_empty() {
format!(
"{}/{} related confirmed — gate satisfiable",
row.total, row.total
)
} else {
format!(
"{}/{} related confirmed — unconfirmed: {}",
row.total - row.unchecked.len(),
row.total,
row.unchecked
.iter()
.map(|u| format!("`{}` ({})", u.id, u.state))
.collect::<Vec<_>>()
.join(", ")
)
};
lines.push(format!(
"- `{}` — {field}: {} — {coverage}",
row.id,
if row.value.is_empty() {
"(unset)"
} else {
row.value.as_str()
},
));
}
}
lines.push(String::new());
}
sections.push(lines.join("\n"));
}
let mut out = String::from("# Gates brief\n\n");
if declaring_mems.is_empty() {
out.push_str(match mem_filter {
Some(f) => {
sections.push(format!(
"No `transition_requires_checks` gate declared by the schema of `{f}` \
(or the mem is not mounted)."
));
""
}
None => "No mounted mem's schema declares a `transition_requires_checks` gate.",
});
}
out.push_str(§ions.join("\n"));
if !out.ends_with('\n') {
out.push('\n');
}
out
}
}
#[cfg(test)]
mod tests {
#[test]
fn brief_names_the_no_gates_case() {
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path().join("plain");
std::fs::create_dir_all(&dir).unwrap();
let engine = crate::Engine::from_mounts(vec![(
crate::engine::test_helpers::folder_mount("plain", dir.clone()),
Box::new(crate::storage::FilesystemMemWriter::new(dir))
as Box<dyn crate::backend::MemBackend>,
)])
.unwrap();
let brief = engine.render_gates_brief(None);
assert!(
brief.contains("No mounted mem's schema declares"),
"{brief}"
);
let filtered = engine.render_gates_brief(Some("plain"));
assert!(
filtered.contains("No `transition_requires_checks` gate declared"),
"{filtered}"
);
}
}