build: slots instead of placeholders, and the first worded run
Fallback words read like finished copy, and nemotron handed the front door straight back. The prompt now shows every string as a slot saying what belongs there; a slot echoed back counts as no answer and keeps the fallback. runs/contractor-build-2026-09-21: nemotron-3.5-lightning:30b-a3b, the model that produced no loadable site free-form, words a complete contractor site in 130 s. Verifier green by construction; held-out personas route 8/8 with descriptions and 8/8 on headings alone (qwen3:8b as the visitor). "What happens next" is still 0/2 on the front door: no heading or description says who reads a submission. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
8c286a8483
commit
aef0fe1396
+32
-3
@@ -291,11 +291,40 @@ pub fn merge_words(fallback: &Value, given: &Value) -> Value {
|
||||
(Value::Object(f), Value::Object(g)) => {
|
||||
Value::Object(f.iter().map(|(k, v)| (k.clone(), g.get(k).map_or_else(|| v.clone(), |gv| merge_words(v, gv)))).collect())
|
||||
}
|
||||
(Value::String(_), Value::String(s)) if !s.trim().is_empty() => Value::String(s.trim().to_string()),
|
||||
// An unfilled slot echoed back is no answer.
|
||||
(Value::String(_), Value::String(s)) if !s.trim().is_empty() && !s.contains(SLOT) => {
|
||||
Value::String(s.trim().to_string())
|
||||
}
|
||||
_ => fallback.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
const SLOT: &str = "<WRITE";
|
||||
|
||||
/// 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
|
||||
/// copy straight back (measured: nemotron returned the front door
|
||||
/// unchanged). A slot cannot be mistaken for an answer.
|
||||
fn slots(value: &Value, key: &str) -> Value {
|
||||
match value {
|
||||
Value::Object(map) => Value::Object(map.iter().map(|(k, v)| (k.clone(), slots(v, k))).collect()),
|
||||
Value::String(_) => Value::String(format!(
|
||||
"{SLOT}: {}>",
|
||||
match key {
|
||||
"question" => "the one question this page asks its reader".to_string(),
|
||||
"heading" | "next_heading" => "a heading, at most eight words, saying what the reader gets".to_string(),
|
||||
"description" | "page_description" | "next_description" => "one or two short sentences".to_string(),
|
||||
"button" | "back" | "confirm" => "a button, one to three words".to_string(),
|
||||
"form_title" => "a title for the form, two to four words".to_string(),
|
||||
other if other.contains("->") => format!("a button, two to four words, for a case going {other}"),
|
||||
other => format!("a form label for the field `{other}`"),
|
||||
}
|
||||
)),
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn s(v: &Value) -> &str {
|
||||
v.as_str().unwrap_or_default()
|
||||
}
|
||||
@@ -470,14 +499,14 @@ fn words_prompt(plan: &Plan, path: &str, fallback: &Value) -> String {
|
||||
.map(|n| format!("- {}: {} - wants: {}. Handled by {}. Stages: {}.", n.id, n.who, n.wants, n.handled_by, n.stages.join(" -> ")))
|
||||
.collect();
|
||||
format!(
|
||||
"You write the words for one page of a website for this business.\n\nBusiness: {}\nAbout: {}\nContact: {} <{}>\nLanguage to write in: {}\n\nEveryone the site serves:\n{}\n\nThe page: {path}\nIt is {purpose}\n\nBelow is the page's wording as JSON, filled with placeholder text. Return the same JSON with every string rewritten. Keep every key exactly as it is; change only the string values.\n\nRules for the words:\n- `question` is a question the reader could ask themselves.\n- A heading says what the reader gets, in the reader's own words, and must work without its description.\n- Lead with what a thing enables. No negations in headings. No trade jargon the reader would not use.\n- Descriptions are one or two short sentences. Labels and buttons are a few words.\n- Write the way people in this trade and country actually speak.\n\n{}\n\nReply with the JSON object only.",
|
||||
"You write the words for one page of a website for this business.\n\nBusiness: {}\nAbout: {}\nContact: {} <{}>\nLanguage to write in: {}\n\nEveryone the site serves:\n{}\n\nThe page: {path}\nIt is {purpose}\n\nBelow is the page as JSON. Every value is a slot that says what to write there. Return the same JSON with every slot replaced by your words. Keep every key exactly as it is. A slot left as it is counts as no answer.\n\nRules for the words:\n- `question` is a question the reader could ask themselves.\n- A heading says what the reader gets, in the reader's own words, and must work without its description.\n- Lead with what a thing enables. No negations in headings. No trade jargon the reader would not use.\n- Descriptions are one or two short sentences. Labels and buttons are a few words.\n- Write the way people in this trade and country actually speak.\n\n{}\n\nReply with the JSON object only.",
|
||||
plan.business,
|
||||
plan.about.trim(),
|
||||
plan.contact.name,
|
||||
plan.contact.email,
|
||||
plan.language,
|
||||
context.join("\n"),
|
||||
serde_json::to_string_pretty(fallback).unwrap_or_default(),
|
||||
serde_json::to_string_pretty(&slots(fallback, "")).unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user