From 0a02b4db1a65970ae4c033f52b76c2a50fb67499 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Thu, 24 Sep 2026 09:33:21 +0200 Subject: [PATCH] build: a bare {chain} in a mail body goes, on its own line or inline Co-Authored-By: Claude Fable 5.1 --- src/build.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/build.rs b/src/build.rs index ce685dc..6ed62ff 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1045,14 +1045,23 @@ fn keep_links(fallback: &Value, merged: &mut Value) { for (bucket, stages) in fb { let Some(ms) = mb.get_mut(bucket).and_then(Value::as_object_mut) else { continue }; for (stage, spec) in stages.as_object().into_iter().flatten() { - let Some(link) = spec["body"].as_str().and_then(|b| b.lines().find(|l| l.contains("{site}/decide/"))) else { continue }; + let link = spec["body"].as_str().and_then(|b| b.lines().find(|l| l.contains("{site}/decide/"))); let Some(body) = ms.get_mut(stage).and_then(|m| m.get_mut("body")) else { continue }; let Some(text) = body.as_str() else { continue }; - if text.contains("/decide/") { - continue; + // A bare `{chain}` outside a link is the record id pasted + // into prose, which a model does when told to keep + // placeholders; it goes, on its own line or inline. + let cleaned: Vec = text + .lines() + .map(|l| if l.contains("/decide/") { l.to_string() } else { l.replace("{chain}", "").split_whitespace().collect::>().join(" ") }) + .collect(); + let mut cleaned = cleaned.join("\n").trim_end().to_string(); + if let Some(link) = link { + if !cleaned.contains("/decide/") { + cleaned = format!("{cleaned}\n\n{link}"); + } } - let kept: Vec<&str> = text.lines().filter(|l| l.trim() != "{chain}").collect(); - *body = Value::String(format!("{}\n\n{link}", kept.join("\n").trim_end())); + *body = Value::String(cleaned); } } } @@ -1723,11 +1732,12 @@ mod tests { #[test] fn a_models_mail_keeps_the_submitters_link() { let fallback = json!({ "leaks": { "open": { "subject": "s", "body": "Hello,\n\nGot it.\n\nWhat you can do next: {site}/decide/leak?chain={chain}&email={email}\n\nI at i@b.example" } } }); - let mut merged = json!({ "leaks": { "open": { "subject": "Got it", "body": "Hi,\n\nWe have it.\n\n{chain}" } } }); + let mut merged = json!({ "leaks": { "open": { "subject": "Got it", "body": "Hi,\n\nWe have it. {chain}\n\n{chain}" } } }); keep_links(&fallback, &mut merged); let body = merged["leaks"]["open"]["body"].as_str().unwrap(); assert!(body.ends_with("What you can do next: {site}/decide/leak?chain={chain}&email={email}")); - assert!(!body.contains("\n{chain}")); + assert!(!body.contains("it. {chain}") && !body.contains("\n{chain}")); + assert!(body.starts_with("Hi,\n\nWe have it.")); // A body that kept its link is left alone. let mut fine = json!({ "leaks": { "open": { "subject": "s", "body": "Withdraw here: {site}/decide/leak?chain={chain}&email={email}" } } }); keep_links(&fallback, &mut fine);