mod parse;
mod render;
pub use parse::{parse_md, parse_md_reporting};
pub use render::render_md;
pub(crate) struct SectionShape {
pub list_field: Option<&'static str>,
pub key_field: &'static str,
}
pub(crate) fn nested_get<'a>(
m: &'a serde_yaml::Mapping,
path: &str,
) -> Option<&'a serde_yaml::Value> {
let mut parts = path.split('.');
let mut cur = m.get(serde_yaml::Value::from(parts.next()?))?;
for part in parts {
cur = cur.get(part)?;
}
Some(cur)
}
pub(crate) fn nested_insert(m: &mut serde_yaml::Mapping, path: &str, value: serde_yaml::Value) {
use serde_yaml::{Mapping, Value};
let mut parts = path.split('.').peekable();
let mut cursor = m;
loop {
let part = parts.next().expect("a path has at least one segment");
let key = Value::from(part);
if parts.peek().is_none() {
cursor.insert(key, value);
return;
}
let slot = cursor
.entry(key)
.or_insert_with(|| Value::Mapping(Mapping::new()));
let Value::Mapping(next) = slot else { return };
cursor = next;
}
}
pub(crate) fn nested_remove(m: &mut serde_yaml::Mapping, path: &str) {
use serde_yaml::Value;
let Some((head, rest)) = path.split_once('.') else {
m.remove(Value::from(path));
return;
};
let key = Value::from(head);
let Some(Value::Mapping(inner)) = m.get_mut(&key) else {
return;
};
nested_remove(inner, rest);
if inner.is_empty() {
m.remove(&key);
}
}
pub(crate) fn section_shape(section: &str) -> Option<SectionShape> {
let (list_field, key_field) = match section {
"intent.background" => (None, "key"),
"intent.prerequisites" => (None, "step"),
"intent.goals" => (None, "name"),
"intent.success" => (None, "name"),
"safety.checks" => (None, "name"),
"safety.alerts" => (None, "id"),
"execution.triggers" => (Some("triggers"), "on.type"),
"execution.phases" => (Some("items"), "name"),
"execution.default_skills" => (None, "name"),
"execution.graph" => (Some("nodes"), "id"),
"execution.providers" => (Some("providers"), "id"),
"information" => (None, "key"),
"pre_execution" => (None, "step"),
"validations" => (None, "name"),
"schedules" => (None, "type"),
"execution_guidelines" => (Some("items"), "name"),
_ => return None,
};
Some(SectionShape {
list_field,
key_field,
})
}
pub(crate) const SECTION_PATHS: &[(&str, &str, &str)] = &[
("background", "intent.background", "Background"),
("prerequisites", "intent.prerequisites", "Prerequisites"),
("goals", "intent.goals", "Goals"),
("success", "intent.success", "Success"),
("graph", "execution.graph", "Graph"),
("providers", "execution.providers", "Providers"),
("phases", "execution.phases", "Phases"),
("default_skills", "execution.default_skills", "Default skills"),
("skills", "execution.skills", "Skills"),
("memory", "execution.memory", "Memory"),
("triggers", "execution.triggers", "Triggers"),
("checks", "safety.checks", "Checks"),
("gates", "safety.gates", "Gates"),
("limits", "safety.limits", "Limits"),
("recovery", "safety.recovery", "Recovery"),
("protected", "safety.protected", "Protected"),
("alerts", "safety.alerts", "Alerts"),
("evolution", "evolution", "Evolution"),
];
pub(crate) const LEGACY_SECTION_KEYS: &[&str] = &[
"information",
"pre_execution",
"validations",
"stop_gates",
"constraints",
"execution_guidelines",
"schedules",
"context",
];
pub(crate) fn section_path(key: &str) -> Option<&'static str> {
if let Some((_, path, _)) = SECTION_PATHS.iter().find(|(k, _, _)| *k == key) {
return Some(path);
}
if let Some(legacy) = LEGACY_SECTION_KEYS.iter().find(|k| **k == key) {
return Some(legacy);
}
SECTION_PATHS
.iter()
.find(|(_, path, _)| path.replace('.', "_") == key)
.map(|(_, path, _)| *path)
}
pub(crate) fn heading_to_key(heading: &str) -> String {
let h = heading.trim();
let h = match h.find(['.', ')', '-']) {
Some(i) if i <= 2 && h[..i].chars().all(|c| c.is_ascii_alphanumeric()) => &h[i + 1..],
_ => h,
};
h.trim()
.to_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join("_")
.replace('-', "_")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn headings_normalise_to_config_keys() {
for (heading, key) in [
("A. Information", "information"),
("B. Pre-execution", "pre_execution"),
("F. Stop gates", "stop_gates"),
("I. Execution guidelines", "execution_guidelines"),
("J. Default skills", "default_skills"),
("Providers", "providers"),
("stop_gates", "stop_gates"),
(" Graph ", "graph"),
] {
assert_eq!(heading_to_key(heading), key, "for heading `{heading}`");
}
}
#[test]
fn a_hyphenated_word_is_not_mistaken_for_a_section_letter() {
assert_eq!(heading_to_key("Pre-execution"), "pre_execution");
}
}