build: stages in the plan's own words

`stages_text` on a need gives each stage plain words in the plan's
language. The fallback mails, the road line and the desk buttons use
them, and needs.yaml carries them as `stage_words` so the desk score
describes an outcome the way the site would. The vel's plan has them;
its desks score 26 of 26 where the id-based outcome scored 24.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-24 05:17:18 +02:00
co-authored by Claude Fable 5.1
parent 8e6a93ebb7
commit be14e33603
5 changed files with 60 additions and 8 deletions
+5 -3
View File
@@ -187,8 +187,10 @@ fn s(v: &Value) -> String {
/// the button for it. The outcome is put in plain words from the
/// state's own name, never from the label, and the buttons are listed
/// in a seeded shuffle.
fn button_pick(cmd: &str, group: &str, bucket: &str, from: &str, to: &str, labels: &[&str], seed: usize) -> anyhow::Result<String> {
let plain = |id: &str| id.replace(['_', '-'], " ");
fn button_pick(cmd: &str, group: &str, bucket: &str, from: &str, to: &str, labels: &[&str], seed: usize, words: &Value) -> anyhow::Result<String> {
// The state in the plan's own words when the spec carries them,
// else the id made readable.
let plain = |id: &str| words[id].as_str().map(str::to_string).unwrap_or_else(|| id.replace(['_', '-'], " "));
let mut order: Vec<&str> = labels.to_vec();
// A fixed rotation per move: the right answer is not always first.
let by = seed % order.len().max(1);
@@ -319,7 +321,7 @@ pub fn rate(argv: &[String]) -> anyhow::Result<()> {
continue;
}
asked += 1;
let picked = button_pick(&cmd, group, bucket, from, to, &labels, i)?;
let picked = button_pick(&cmd, group, bucket, from, to, &labels, i, &model["buckets"][bucket]["words"])?;
if picked == *label {
right += 1;
} else {