From 9843d5f1abefc89f2d23253b3fe05a8da8ed9d31 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 23 Oct 2026 16:57:06 -0400 Subject: [PATCH] Audit: export names, linked-file ids and MCP prompt arguments in one pass --- crates/engine/src/cmd/fileio/mod.rs | 12 +++++++----- crates/engine/src/cmd/links.rs | 4 +++- crates/mcp/src/prompts.rs | 21 +++++++++++++++++++-- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index c819b9ae6..8c224aa12 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -1366,15 +1366,17 @@ pub(crate) fn unique_file_names<'a>(names: impl IntoIterator) -> Vec { let mut taken = std::collections::HashSet::new(); + // The next number to try for each base: a name repeated many times doesn't search the numbers + // taken before it. + let mut next: std::collections::HashMap = std::collections::HashMap::new(); names .into_iter() .map(|base| { let mut name = base.clone(); - for i in 2.. { - if taken.insert(name.to_lowercase()) { - break; - } - name = format!("{base}-{i}"); + while !taken.insert(name.to_lowercase()) { + let n = next.entry(base.to_lowercase()).or_insert(1); + *n += 1; + name = format!("{base}-{n}"); } name }) diff --git a/crates/engine/src/cmd/links.rs b/crates/engine/src/cmd/links.rs index f74c344e7..e8f122e96 100644 --- a/crates/engine/src/cmd/links.rs +++ b/crates/engine/src/cmd/links.rs @@ -182,6 +182,8 @@ pub(crate) fn linked_groups(d: &Document) -> Vec<(String, Vec)> { fn groups(d: &Document, ids: Option<&[NodeId]>) -> Vec { let mut out: Vec = vec![]; let mut index: BTreeMap<(String, String, bool), usize> = BTreeMap::new(); + // Looked up for every linked node: a set, not the list (which may repeat an id many times). + let wanted: Option> = ids.map(|ids| ids.iter().copied().collect()); let mut add = |link: &LinkInfo, key: &str, document: Option, id: Option| { let i = *index.entry((link.path.clone(), key.to_string(), document.is_some())).or_insert_with(|| { out.push(Group { link: link.clone(), key: key.to_string(), ids: vec![], document }); @@ -193,7 +195,7 @@ fn groups(d: &Document, ids: Option<&[NodeId]>) -> Vec { }; d.walk(|n| { if let Some((l, key, document)) = linked(n) - && ids.is_none_or(|ids| ids.contains(&n.id)) + && wanted.as_ref().is_none_or(|ids| ids.contains(&n.id)) { add(l, key, document, Some(n.id)); } diff --git a/crates/mcp/src/prompts.rs b/crates/mcp/src/prompts.rs index 2dff4486e..0c0a43926 100644 --- a/crates/mcp/src/prompts.rs +++ b/crates/mcp/src/prompts.rs @@ -405,7 +405,10 @@ pub fn get(name: &str, args: &Value) -> Result { let ids: Vec<&str> = PROMPTS.iter().map(|p| p.name).collect(); return Err(format!("unknown prompt `{name}` ({})", ids.join("|"))); }; - let mut text = flow(p.text); + let template = flow(p.text); + // (placeholder, value): filled in one pass over the template, so a value that looks like a + // placeholder stays as it is. + let mut values: Vec<(String, String)> = vec![]; for a in p.arguments { let given = args.get(a.name).filter(|v| !v.is_null()).map(|v| match v { Value::String(s) => s.clone(), @@ -416,7 +419,21 @@ pub fn get(name: &str, args: &Value) -> Result { _ if a.required => return Err(format!("prompt `{name}` needs the `{a_name}` argument", a_name = a.name)), _ => a.default.to_string(), }; - text = text.replace(&format!("{{{}}}", a.name), &value); + values.push((format!("{{{}}}", a.name), value)); + } + let mut text = String::with_capacity(template.len()); + let mut rest = template.as_str(); + while let Some(c) = rest.chars().next() { + match values.iter().find(|(placeholder, _)| rest.starts_with(placeholder.as_str())) { + Some((placeholder, value)) => { + text.push_str(value); + rest = rest.get(placeholder.len()..).unwrap_or_default(); + } + None => { + text.push(c); + rest = rest.get(c.len_utf8()..).unwrap_or_default(); + } + } } Ok(json!({ "description": p.description,