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:
co-authored by
Claude Fable 5.1
parent
8e6a93ebb7
commit
be14e33603
@@ -167,6 +167,12 @@ the business's own words and held-out personas.
|
||||
body (`aggregates.yaml` in the words map); the fallback says what
|
||||
happened and, where the submitter has a move from that stage, the
|
||||
link to make it.
|
||||
- `stages_text: { <stage>: <words> }` on a need: each stage in the
|
||||
plan's language, where the ASCII id will not do (`ikke_lost` reads
|
||||
as English). Used by the fallback mails, the "what happens next"
|
||||
line, the desk buttons' fallback labels, and written into needs.yaml
|
||||
as `stage_words` so the desk score describes an outcome in the
|
||||
site's own words. With them, the vel's desks score 26 of 26.
|
||||
- `attended_by:` on a need: who moves cases that no desk button moves,
|
||||
an automation listening on the bucket's events, written into
|
||||
aggregates.yaml as portal's own annotation for the reader.
|
||||
|
||||
@@ -70,6 +70,7 @@ needs:
|
||||
door: medlem
|
||||
handled_by: kasserer
|
||||
stages: [open, betalt, utmeldt]
|
||||
stages_text: { open: "mottatt", betalt: "betalt", utmeldt: "meldt ut" }
|
||||
done_when: [betalt, utmeldt]
|
||||
success: [betalt]
|
||||
# Medlemmet melder seg ut selv, gjennom sin egen lenke.
|
||||
@@ -89,6 +90,7 @@ needs:
|
||||
door: medlem
|
||||
handled_by: kasserer
|
||||
stages: [open, oppdatert]
|
||||
stages_text: { open: "mottatt", oppdatert: "oppdatert" }
|
||||
done_when: [oppdatert]
|
||||
success: [oppdatert]
|
||||
max_fields: 3
|
||||
@@ -104,6 +106,7 @@ needs:
|
||||
door: melde-fra
|
||||
handled_by: trafikk
|
||||
stages: [open, tatt_opp, ordnet, avvist]
|
||||
stages_text: { open: "mottatt", tatt_opp: "tatt opp med kommunen", ordnet: "ordnet", avvist: "avvist" }
|
||||
done_when: [ordnet, avvist]
|
||||
success: [ordnet]
|
||||
# Alle kan se hva som er meldt og hvor det står; e-posten vises ikke.
|
||||
@@ -121,6 +124,7 @@ needs:
|
||||
door: melde-fra
|
||||
handled_by: lekeplasser
|
||||
stages: [open, tatt_tak_i, utbedret]
|
||||
stages_text: { open: "mottatt", tatt_tak_i: "tatt tak i", utbedret: "utbedret" }
|
||||
done_when: [utbedret]
|
||||
success: [utbedret]
|
||||
watched_by: [public]
|
||||
@@ -137,6 +141,7 @@ needs:
|
||||
handled_by: horingsinstans
|
||||
watched_by: [styret]
|
||||
stages: [open, tatt_med, ikke_tatt_med]
|
||||
stages_text: { open: "mottatt", tatt_med: "tatt med i høringssvaret", ikke_tatt_med: "ikke tatt med" }
|
||||
done_when: [tatt_med, ikke_tatt_med]
|
||||
success: [tatt_med]
|
||||
max_fields: 3
|
||||
@@ -152,6 +157,7 @@ needs:
|
||||
door: bidra
|
||||
handled_by: arrangementer
|
||||
stages: [open, kontaktet, satt_opp, takket_nei]
|
||||
stages_text: { open: "mottatt", kontaktet: "kontaktet", satt_opp: "satt opp", takket_nei: "takket nei" }
|
||||
done_when: [satt_opp, takket_nei]
|
||||
success: [satt_opp]
|
||||
max_fields: 3
|
||||
@@ -167,6 +173,7 @@ needs:
|
||||
door: bidra
|
||||
handled_by: styret
|
||||
stages: [open, plassert, avslatt]
|
||||
stages_text: { open: "mottatt", plassert: "plassert i en komité", avslatt: "avslått" }
|
||||
done_when: [plassert, avslatt]
|
||||
success: [plassert]
|
||||
max_fields: 3
|
||||
@@ -185,6 +192,7 @@ needs:
|
||||
door: naboer
|
||||
handled_by: nabohjelp
|
||||
stages: [open, formidlet, ordnet, ikke_lost]
|
||||
stages_text: { open: "mottatt", formidlet: "formidlet til en nabo", ordnet: "ordnet", ikke_lost: "ikke løst" }
|
||||
done_when: [ordnet, ikke_lost]
|
||||
success: [ordnet]
|
||||
moves_by: { ordnet: [submitter, nabohjelp] }
|
||||
@@ -204,6 +212,7 @@ needs:
|
||||
door: naboer
|
||||
handled_by: nabohjelp
|
||||
stages: [open, registrert, trukket]
|
||||
stages_text: { open: "mottatt", registrert: "registrert som hjelper", trukket: "trukket" }
|
||||
done_when: [registrert, trukket]
|
||||
success: [registrert]
|
||||
moves_by: { trukket: [submitter, nabohjelp] }
|
||||
@@ -222,6 +231,7 @@ needs:
|
||||
door: naboer
|
||||
handled_by: styret
|
||||
stages: [open, lagt_ut, tatt_ned]
|
||||
stages_text: { open: "mottatt", lagt_ut: "lagt ut", tatt_ned: "tatt ned" }
|
||||
done_when: [lagt_ut, tatt_ned]
|
||||
success: [lagt_ut]
|
||||
# Den som la det ut kan ta det ned selv. Tavla er offentlig: det er
|
||||
@@ -243,6 +253,7 @@ needs:
|
||||
lands_in: utlegg
|
||||
handled_by: kasserer
|
||||
stages: [open, godkjent, utbetalt, avvist]
|
||||
stages_text: { open: "mottatt", godkjent: "godkjent", utbetalt: "utbetalt", avvist: "avvist" }
|
||||
done_when: [utbetalt, avvist]
|
||||
success: [utbetalt]
|
||||
max_fields: 3
|
||||
|
||||
+35
-5
@@ -190,6 +190,13 @@ pub struct PlanNeed {
|
||||
/// its desk.
|
||||
#[serde(default)]
|
||||
pub events: BTreeMap<String, String>,
|
||||
/// Each stage in plain words, in the plan's language, where the
|
||||
/// id will not do: ids are ASCII, so a Norwegian `ikke_lost` reads
|
||||
/// as English in a mail, a road line or a desk score. Used by the
|
||||
/// fallback mails, the "what happens next" line, the desk
|
||||
/// buttons, and written into needs.yaml for the scorer.
|
||||
#[serde(default)]
|
||||
pub stages_text: BTreeMap<String, String>,
|
||||
/// Who moves cases that no desk button moves - an automation
|
||||
/// listening on the bucket's events. Written into aggregates.yaml
|
||||
/// for the reader, as portal's `attended_by` annotation.
|
||||
@@ -364,6 +371,14 @@ impl Plan {
|
||||
bail!(at("has mails, so the plan needs `mail: { from: ... }` saying who they come from"));
|
||||
}
|
||||
}
|
||||
for (state, text) in &need.stages_text {
|
||||
if !need.stages.contains(state) {
|
||||
bail!(at(&format!("stages_text names {state:?}, which is not one of its stages")));
|
||||
}
|
||||
if text.trim().is_empty() {
|
||||
bail!(at(&format!("stages_text for {state:?} is empty")));
|
||||
}
|
||||
}
|
||||
for (state, event) in &need.events {
|
||||
if !need.stages.contains(state) {
|
||||
bail!(at(&format!("events names {state:?}, which is not one of its stages")));
|
||||
@@ -425,6 +440,7 @@ impl Plan {
|
||||
|| other.events != first.events
|
||||
|| other.attended_by != first.attended_by
|
||||
|| other.mails != first.mails
|
||||
|| other.stages_text != first.stages_text
|
||||
{
|
||||
bail!("needs {:?} and {:?} both land in {bucket:?} and must agree on stages, done_when, skippable, events, attended_by, mails and who moves and watches it", first.id, other.id);
|
||||
}
|
||||
@@ -521,9 +537,14 @@ pub fn event_for(need: &PlanNeed, stage: &str) -> String {
|
||||
|
||||
/// The stages a case goes through, as one sentence: "It is received,
|
||||
/// then surveyed, then quoted, and ends won or lost."
|
||||
/// A stage in words: what the plan says, else its id made readable.
|
||||
pub fn stage_text(need: &PlanNeed, stage: &str) -> String {
|
||||
need.stages_text.get(stage).cloned().unwrap_or_else(|| humanize(stage).to_lowercase())
|
||||
}
|
||||
|
||||
fn stages_sentence(need: &PlanNeed) -> String {
|
||||
let line: Vec<String> = need.stages.iter().filter(|s| !need.done_when.contains(s)).map(|s| humanize(s).to_lowercase()).collect();
|
||||
let ends: Vec<String> = need.done_when.iter().map(|s| humanize(s).to_lowercase()).collect();
|
||||
let line: Vec<String> = need.stages.iter().filter(|s| !need.done_when.contains(s)).map(|s| stage_text(need, s)).collect();
|
||||
let ends: Vec<String> = need.done_when.iter().map(|s| stage_text(need, s)).collect();
|
||||
let mut out = String::from("It is ");
|
||||
out.push_str(&line.join(", then "));
|
||||
if ends.len() == 1 {
|
||||
@@ -534,6 +555,14 @@ fn stages_sentence(need: &PlanNeed) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn capitalize(text: &str) -> String {
|
||||
let mut chars = text.chars();
|
||||
match chars.next() {
|
||||
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn humanize(id: &str) -> String {
|
||||
let mut words = id.replace(['_', '-'], " ");
|
||||
if let Some(first) = words.get_mut(0..1) {
|
||||
@@ -679,7 +708,7 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
||||
// Only the buttons this group presses; a watcher gets none.
|
||||
let moves: Map<String, Value> = moves_of(needs[0], &group)
|
||||
.into_iter()
|
||||
.map(|(from, to)| (format!("{from}->{to}"), Value::String(humanize(&to))))
|
||||
.map(|(from, to)| (format!("{from}->{to}"), Value::String(capitalize(&stage_text(needs[0], &to)))))
|
||||
.collect();
|
||||
(bucket.to_string(), json!({ "heading": humanize(bucket), "description": "", "moves": moves }))
|
||||
})
|
||||
@@ -749,7 +778,7 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
||||
};
|
||||
let mut per_stage: Map<String, Value> = Map::new();
|
||||
for (stage, to) in &need.mails {
|
||||
let stage_word = humanize(stage).to_lowercase();
|
||||
let stage_word = stage_text(need, stage);
|
||||
let what = if stage == &need.stages[0] {
|
||||
format!("{} {}.", t.received_what_you_sent_about, need.wants)
|
||||
} else {
|
||||
@@ -1440,6 +1469,7 @@ pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BT
|
||||
"lands_in": n.lands_in, "handled_by": n.handled_by,
|
||||
"stages": n.stages, "done_when": n.done_when, "success": n.success,
|
||||
"max_steps": steps, "max_fields": n.max_fields,
|
||||
"stage_words": n.stages.iter().map(|s| (s.clone(), json!(stage_text(n, s)))).collect::<Map<_, _>>(),
|
||||
});
|
||||
if let Some(group) = &n.enters_as { need["enters_as"] = json!(group); }
|
||||
let others: BTreeSet<&String> = n.moves_by.values().flatten().filter(|m| *m != SUBMITTER && **m != n.handled_by).collect();
|
||||
@@ -1612,7 +1642,7 @@ mod tests {
|
||||
lands_in: "b".into(), handled_by: "g".into(),
|
||||
stages: v(stages), done_when: v(done), success: v(success), skippable: v(skippable),
|
||||
moves_by: BTreeMap::new(), watched_by: vec![],
|
||||
events: BTreeMap::new(), attended_by: None, mails: BTreeMap::new(),
|
||||
events: BTreeMap::new(), attended_by: None, mails: BTreeMap::new(), stages_text: BTreeMap::new(),
|
||||
collects: vec![], max_fields: 3,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ needs:
|
||||
# who hears when a case enters a stage: `submitter` (the form
|
||||
# must then collect a required `email`) or one address. Tell the
|
||||
# person on arrival and on every decision that is about them.
|
||||
stages_text: { open: <words>, <stage>: <words> }
|
||||
# each stage in the intake's language, where the ASCII id
|
||||
# would read wrong to a person: "ikke_lost" is "ikke løst"
|
||||
events: { open: applied } # ONLY when rebuilding a live site: the event names its
|
||||
# aggregates.yaml already has, so its records keep replaying
|
||||
attended_by: > # ONLY for a stage no desk button moves: the automation that does
|
||||
|
||||
+5
-3
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user