diff --git a/README.md b/README.md index b93184b..af146da 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,13 @@ default. `--words-from ` lifts the words a repo already has back into the words map by structure, so a rebuild keeps hand-edited wording and -adds only the new slots; `--words ` overrides slots by page; +adds only the new slots. The lift is by position, so it holds for a +rebuild of the same shape and not across a restructure: on uhhm.no it +put the old front door's three headings on three new cards. `--words +` overrides slots by page and may repeat, each file layered over +the last (the words a build used, then a round of hand changes); `words.json` in the output is the words as used. See -`runs/tomtervel-invites-2026-09-23`. +`runs/tomtervel-invites-2026-09-23` and `runs/uhhm-2026-09-23`. | Same contractor, same local model (nemotron 30B-A3B on ergo) | Free-form `generate` | `build` | |---|---|---| diff --git a/src/build.rs b/src/build.rs index 9cc5e65..5b6af8d 100644 --- a/src/build.rs +++ b/src/build.rs @@ -833,7 +833,11 @@ fn slots_in(value: &Value, key: &str, parent: &str) -> Value { (_, "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, saying what pressing it does".to_string(), + (_, "button" | "back") => "a button, one to three words, saying what pressing it does".to_string(), + // One slot serves every list on the desk, so it cannot + // name any one of them: a model once wrote "Send invite" + // under the cases list. + (_, "confirm") => "the one button under every list on this desk, confirming a move the person has chosen; one word, such as Confirm".to_string(), (_, "encouragement") => "one reassuring sentence: what happens with what they send, or how little it takes; or empty".to_string(), (_, "icon") => "an icon name from the list at the end, the one that fits this best; or empty".to_string(), (_, "for_whom") => "a few words naming who this is for, addressed to them; or empty".to_string(), @@ -1296,7 +1300,7 @@ fn words_prompt(plan: &Plan, path: &str, fallback: &Value) -> String { pub fn run(argv: &[String]) -> anyhow::Result<()> { let value = |flag: &str| argv.iter().position(|a| a == flag).and_then(|i| argv.get(i + 1)).cloned(); - let usage = "usage: portal-trainer build --plan --out [--words-from ] [--words ] [--llm 'cmd'] [--lint question_lint]"; + let usage = "usage: portal-trainer build --plan --out [--words-from ] [--words ]... [--llm 'cmd'] [--lint question_lint]"; let plan_path = value("--plan").ok_or_else(|| anyhow!(usage))?; let out = std::path::PathBuf::from(value("--out").ok_or_else(|| anyhow!(usage))?); let plan: Plan = serde_yaml::from_str(&std::fs::read_to_string(&plan_path).with_context(|| format!("reading {plan_path}"))?) @@ -1313,8 +1317,13 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> { Some(repo) => lift_words(&plan, Path::new(&repo))?, None => fallback_words(&plan), }; - if let Some(path) = value("--words") { - let given: BTreeMap = serde_json::from_str(&std::fs::read_to_string(&path).with_context(|| format!("reading {path}"))?) + // `--words` may repeat: the words a previous build used, then a + // round of hand overrides on top, in the order given. Each is + // merged key by key over what is there, so a file names only the + // slots it changes. + let words_files: Vec<&String> = argv.iter().enumerate().filter(|(_, a)| *a == "--words").filter_map(|(i, _)| argv.get(i + 1)).collect(); + for path in words_files { + let given: BTreeMap = serde_json::from_str(&std::fs::read_to_string(path).with_context(|| format!("reading {path}"))?) .with_context(|| format!("parsing {path}"))?; for (page, v) in given { match words.get(&page) {