build: a model may not lose the submitter's link; uhhm round 7, the mails

Sonnet wrote the ten uhhm mails and replaced the decide link with a
bare {chain}. keep_links puts the fallback's link line back into any
mail that lost it. The mails read easier than the fallbacks (LIX 9 to
33 against 18 to 28) and say something.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-23 23:02:48 +02:00
co-authored by Claude Fable 5.1
parent b5f5c46ff2
commit 72a081ed59
21 changed files with 939 additions and 1468 deletions
+42 -1
View File
@@ -934,6 +934,29 @@ pub fn merge_words(fallback: &Value, given: &Value) -> Value {
const SLOT: &str = "<WRITE";
/// A mail's link to the submitter's own move is structure, not
/// wording: a model told to keep it still drops it (Sonnet replaced
/// the whole line with a bare `{chain}`). Wherever the fallback body
/// had a `{site}/decide/…` line and the model's has none, the line is
/// put back at the end, and a `{chain}` left on a line of its own is
/// removed.
fn keep_links(fallback: &Value, merged: &mut Value) {
let (Some(fb), Some(mb)) = (fallback.as_object(), merged.as_object_mut()) else { return };
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 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;
}
let kept: Vec<&str> = text.lines().filter(|l| l.trim() != "{chain}").collect();
*body = Value::String(format!("{}\n\n{link}", kept.join("\n").trim_end()));
}
}
}
/// The page's wording with every string replaced by a slot that says
/// what belongs there. Shown to the model in place of the fallbacks:
/// fallbacks read like finished copy, and a small model hands finished
@@ -1487,7 +1510,11 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> {
std::fs::write(out.join(format!("llm/{:02}-{}.md", n + 1, path.replace('/', "_"))), format!("{prompt}\n\n=== REPLY ===\n{reply}"))?;
match first_object(&reply) {
Some(given) => {
words.insert(path.clone(), merge_words(fallback, &given));
let mut merged = merge_words(fallback, &given);
if path == "aggregates.yaml" {
keep_links(fallback, &mut merged);
}
words.insert(path.clone(), merged);
eprintln!("words: {path}");
}
None => eprintln!("words: {path} - no usable reply, keeping the fallback"),
@@ -1565,6 +1592,20 @@ mod tests {
assert!(serde_yaml::from_str::<Plan>(&no_from).unwrap().validate().unwrap_err().to_string().contains("mail: { from"));
}
#[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}" } } });
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}"));
// 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);
assert_eq!(fine["leaks"]["open"]["body"], "Withdraw here: {site}/decide/leak?chain={chain}&email={email}");
}
#[test]
fn a_live_sites_event_names_survive_a_rebuild() {
let mut n = need(&["open", "invited", "active", "departed", "declined"], &["departed", "declined"], &["departed"], &[]);