build: every feature says something; a lift carries words, not absences
Five features were emitted empty, as layout: a door's strip, a desk's list, an invited list, a public list, an invite form. Each now has a title and a line that says what a row is and what the buttons under it do, or what the list leaves out - true from the plan before a model writes a word, and a slot the model can improve. Desk cards, their links and the desk page itself get real fallbacks too. The lift no longer copies an empty live string over a fallback, which would have emptied every new slot on the next rebuild. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c6d0701639
commit
743f4a8563
+45
-11
@@ -692,9 +692,13 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
|||||||
"description": plan.about,
|
"description": plan.about,
|
||||||
"opener": { "heading": "What we do", "description": "", "features": does },
|
"opener": { "heading": "What we do", "description": "", "features": does },
|
||||||
"shopping": { "heading": "Have a look around", "description": "", "items": shopping },
|
"shopping": { "heading": "Have a look around", "description": "", "items": shopping },
|
||||||
|
// A door's feature says what is behind it, from the needs
|
||||||
|
// that sit there: a card that leads somewhere has to say
|
||||||
|
// where, or it is a button with decoration around it.
|
||||||
"doors": plan.doors.iter().map(|d| (d.id.clone(), json!({
|
"doors": plan.doors.iter().map(|d| (d.id.clone(), json!({
|
||||||
"heading": humanize(&d.id), "description": format!("For {}.", d.who), "button": "Continue",
|
"heading": humanize(&d.id), "description": format!("For {}.", d.who), "button": "Continue",
|
||||||
"icon": "", "for_whom": "",
|
"icon": "", "for_whom": "",
|
||||||
|
"behind": format!("{}.", capitalize(&plan.needs_behind(&d.id).iter().map(|n| n.wants.as_str()).collect::<Vec<_>>().join(", "))),
|
||||||
}))).collect::<Map<_, _>>(),
|
}))).collect::<Map<_, _>>(),
|
||||||
"needs": direct.iter().map(|n| (n.id.clone(), form(n))).collect::<Map<_, _>>(),
|
"needs": direct.iter().map(|n| (n.id.clone(), form(n))).collect::<Map<_, _>>(),
|
||||||
}),
|
}),
|
||||||
@@ -744,14 +748,23 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(from, to)| (format!("{from}->{to}"), Value::String(capitalize(&stage_text(needs[0], &to)))))
|
.map(|(from, to)| (format!("{from}->{to}"), Value::String(capitalize(&stage_text(needs[0], &to)))))
|
||||||
.collect();
|
.collect();
|
||||||
(bucket.to_string(), json!({ "heading": humanize(bucket), "description": "", "moves": moves }))
|
// The list itself says what a row is and what the
|
||||||
|
// buttons under it do; the card's own description says
|
||||||
|
// what the cases are.
|
||||||
|
let note = if moves.is_empty() {
|
||||||
|
"Each row is one case and the stage it has reached. Another desk moves these.".to_string()
|
||||||
|
} else {
|
||||||
|
"Each row is one case. The buttons record what you have just done with it.".to_string()
|
||||||
|
};
|
||||||
|
let from_whom = format!("Sent in by {}.", needs.iter().map(|n| n.who.as_str()).collect::<Vec<_>>().join(" or "));
|
||||||
|
(bucket.to_string(), json!({ "heading": humanize(bucket), "description": from_whom, "moves": moves, "list_title": humanize(bucket), "list_note": note }))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let links: Map<String, Value> = plan
|
let links: Map<String, Value> = plan
|
||||||
.needs
|
.needs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|n| n.enters_as.as_deref() == Some(group.as_str()))
|
.filter(|n| n.enters_as.as_deref() == Some(group.as_str()))
|
||||||
.map(|n| (n.id.clone(), json!({ "heading": n.wants, "description": "", "button": "Open" })))
|
.map(|n| (n.id.clone(), json!({ "heading": n.wants, "description": format!("For {}.", n.who), "button": "Open" })))
|
||||||
.collect();
|
.collect();
|
||||||
// One invite form per group this desk may fill, and the list of
|
// One invite form per group this desk may fill, and the list of
|
||||||
// invites sent, when it may fill any.
|
// invites sent, when it may fill any.
|
||||||
@@ -764,6 +777,7 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
|||||||
"description": format!("They get an account, a place in {}, and a one-time link to set their own password or passkey.", label_of(plan, g)),
|
"description": format!("They get an account, a place in {}, and a one-time link to set their own password or passkey.", label_of(plan, g)),
|
||||||
"button": "Invite",
|
"button": "Invite",
|
||||||
"form_title": "Who",
|
"form_title": "Who",
|
||||||
|
"form_description": "Their name, and the address the account and its one-time link go to.".to_string(),
|
||||||
"labels": { "name": "Name", "email": "Email" },
|
"labels": { "name": "Name", "email": "Email" },
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@@ -771,11 +785,17 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
|||||||
let invited = if invites.is_empty() {
|
let invited = if invites.is_empty() {
|
||||||
Value::Null
|
Value::Null
|
||||||
} else {
|
} else {
|
||||||
json!({ "heading": "Invitations sent", "description": "Each invite with the link it made. Mark it done when the person is in.", "done": "Is in" })
|
json!({
|
||||||
|
"heading": "Invitations sent",
|
||||||
|
"description": "Each invite with the link it made. Mark it done when the person is in.",
|
||||||
|
"done": "Is in",
|
||||||
|
"list_title": "Who has been invited",
|
||||||
|
"list_note": "Each row is one invitation, with the one-time link while it is unused.",
|
||||||
|
})
|
||||||
};
|
};
|
||||||
words.insert(
|
words.insert(
|
||||||
format!("questions/{group}/index.yaml"),
|
format!("questions/{group}/index.yaml"),
|
||||||
json!({ "question": "What needs doing?", "description": "", "confirm": "Confirm", "buckets": buckets, "links": links, "invites": invites, "invited": invited }),
|
json!({ "question": "What needs doing?", "description": format!("Everything {} looks after.", label_of(plan, &group).to_lowercase()), "confirm": "Confirm", "buckets": buckets, "links": links, "invites": invites, "invited": invited }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// A public view per bucket the plan opens: a read-only list.
|
// A public view per bucket the plan opens: a read-only list.
|
||||||
@@ -787,6 +807,10 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
|||||||
"description": format!("Everything sent in by {}, and where each one stands.", needs.iter().map(|n| n.who.as_str()).collect::<Vec<_>>().join(" or ")),
|
"description": format!("Everything sent in by {}, and where each one stands.", needs.iter().map(|n| n.who.as_str()).collect::<Vec<_>>().join(" or ")),
|
||||||
"heading": humanize(bucket),
|
"heading": humanize(bucket),
|
||||||
"list_description": "",
|
"list_description": "",
|
||||||
|
// A public list says what a row is and what it leaves
|
||||||
|
// out, since the reader cannot know what was pruned.
|
||||||
|
"list_title": humanize(bucket),
|
||||||
|
"list_note": "Each row is one case and the stage it has reached. Names and contact details are not shown here.",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1099,6 +1123,9 @@ fn slots_in(value: &Value, key: &str, parent: &str) -> Value {
|
|||||||
(_, "road_icon" | "who_icon") => "an icon name from the list at the end that fits this section; or empty".to_string(),
|
(_, "road_icon" | "who_icon") => "an icon name from the list at the end that fits this section; or empty".to_string(),
|
||||||
(_, "who_title") => "a short title, two to four words, for the line that says who reads it".to_string(),
|
(_, "who_title") => "a short title, two to four words, for the line that says who reads it".to_string(),
|
||||||
(_, "list_description") => "one line over a public list of what people have sent in, or empty".to_string(),
|
(_, "list_description") => "one line over a public list of what people have sent in, or empty".to_string(),
|
||||||
|
(_, "list_title") => "a short title, two to four words, naming the list itself".to_string(),
|
||||||
|
(_, "list_note") => "one sentence saying what a single row is, and what the buttons under it do or what the list leaves out".to_string(),
|
||||||
|
(_, "behind") => "one sentence naming what a reader can do behind this door, in their words".to_string(),
|
||||||
(_, "who_description") => "one sentence: who reads what they send, and how to reach them meanwhile".to_string(),
|
(_, "who_description") => "one sentence: who reads what they send, and how to reach them meanwhile".to_string(),
|
||||||
("hints", field) => format!("an example answer for the field `{field}`, as a placeholder; or empty"),
|
("hints", field) => format!("an example answer for the field `{field}`, as a placeholder; or empty"),
|
||||||
(_, other) if other.contains("->") => format!("a button, two to four words, for a case going {other}"),
|
(_, other) if other.contains("->") => format!("a button, two to four words, for a case going {other}"),
|
||||||
@@ -1278,8 +1305,9 @@ pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BT
|
|||||||
// ride an empty section, which portal renders as a
|
// ride an empty section, which portal renders as a
|
||||||
// labelled strip on the card.
|
// labelled strip on the card.
|
||||||
let (icon, for_whom) = (icon_name(s(&dw["icon"])), s(&dw["for_whom"]).trim());
|
let (icon, for_whom) = (icon_name(s(&dw["icon"])), s(&dw["for_whom"]).trim());
|
||||||
if icon.is_some() || !for_whom.is_empty() {
|
let behind = s(&dw["behind"]).trim();
|
||||||
let mut f = json!({ "name": for_whom, "description": "" });
|
if icon.is_some() || !for_whom.is_empty() || !behind.is_empty() {
|
||||||
|
let mut f = json!({ "name": for_whom, "description": behind });
|
||||||
if let Some(icon) = icon { f["icon"] = json!(icon); }
|
if let Some(icon) = icon { f["icon"] = json!(icon); }
|
||||||
alt["features"] = json!([f]);
|
alt["features"] = json!([f]);
|
||||||
}
|
}
|
||||||
@@ -1366,7 +1394,7 @@ pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BT
|
|||||||
"description": s(&bw["description"]),
|
"description": s(&bw["description"]),
|
||||||
"action": format!("/{group}"),
|
"action": format!("/{group}"),
|
||||||
"consequence": [s(&w["confirm"])],
|
"consequence": [s(&w["confirm"])],
|
||||||
"features": [{ "name": "", "description": "", "resource": {
|
"features": [{ "name": s(&bw["list_title"]), "description": s(&bw["list_note"]), "resource": {
|
||||||
"source": { "kind": "kv", "bucket": bucket },
|
"source": { "kind": "kv", "bucket": bucket },
|
||||||
"requires_group": group,
|
"requires_group": group,
|
||||||
// This group's buttons only. The same bucket sits on
|
// This group's buttons only. The same bucket sits on
|
||||||
@@ -1402,7 +1430,7 @@ pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BT
|
|||||||
"action": format!("/{group}"),
|
"action": format!("/{group}"),
|
||||||
"consequence": [s(&iw["button"])],
|
"consequence": [s(&iw["button"])],
|
||||||
"invite": { "group": target },
|
"invite": { "group": target },
|
||||||
"features": [{ "name": s(&iw["form_title"]), "description": "", "requirements": [
|
"features": [{ "name": s(&iw["form_title"]), "description": s(&iw["form_description"]), "requirements": [
|
||||||
{ "name": "name", "label": s(&iw["labels"]["name"]) },
|
{ "name": "name", "label": s(&iw["labels"]["name"]) },
|
||||||
{ "name": "email", "label": s(&iw["labels"]["email"]), "type": "email" },
|
{ "name": "email", "label": s(&iw["labels"]["email"]), "type": "email" },
|
||||||
]}],
|
]}],
|
||||||
@@ -1414,7 +1442,7 @@ pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BT
|
|||||||
"description": s(&lw["description"]),
|
"description": s(&lw["description"]),
|
||||||
"action": format!("/{group}"),
|
"action": format!("/{group}"),
|
||||||
"consequence": [s(&w["confirm"])],
|
"consequence": [s(&w["confirm"])],
|
||||||
"features": [{ "name": "", "description": "", "resource": {
|
"features": [{ "name": s(&lw["list_title"]), "description": s(&lw["list_note"]), "resource": {
|
||||||
"source": { "kind": "kv", "bucket": "portal_invites" },
|
"source": { "kind": "kv", "bucket": "portal_invites" },
|
||||||
"requires_group": group,
|
"requires_group": group,
|
||||||
"transitions": [{ "from": "open", "to": "done", "label": s(&lw["done"]) }],
|
"transitions": [{ "from": "open", "to": "done", "label": s(&lw["done"]) }],
|
||||||
@@ -1457,7 +1485,7 @@ pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BT
|
|||||||
{
|
{
|
||||||
"name": s(&w["heading"]),
|
"name": s(&w["heading"]),
|
||||||
"description": s(&w["list_description"]),
|
"description": s(&w["list_description"]),
|
||||||
"features": [{ "name": "", "description": "", "resource": {
|
"features": [{ "name": s(&w["list_title"]), "description": s(&w["list_note"]), "resource": {
|
||||||
"source": { "kind": "kv", "bucket": bucket },
|
"source": { "kind": "kv", "bucket": bucket },
|
||||||
"public": true,
|
"public": true,
|
||||||
"jq": projection,
|
"jq": projection,
|
||||||
@@ -2227,7 +2255,13 @@ fn collect_words(a: &serde_yaml::Value, b: &serde_yaml::Value, found: &mut std::
|
|||||||
(serde_yaml::Value::String(t), serde_yaml::Value::String(live)) if t.starts_with('\u{1}') => {
|
(serde_yaml::Value::String(t), serde_yaml::Value::String(live)) if t.starts_with('\u{1}') => {
|
||||||
// An icon slot holds the bare name; the file carries `lucide:`.
|
// An icon slot holds the bare name; the file carries `lucide:`.
|
||||||
let word = if t.ends_with("/icon\u{3}") { live.trim_start_matches("lucide:") } else { live.as_str() };
|
let word = if t.ends_with("/icon\u{3}") { live.trim_start_matches("lucide:") } else { live.as_str() };
|
||||||
found.insert(t.clone(), word.to_string());
|
// A lift carries words across, not absences: where the live
|
||||||
|
// page said nothing, the fallback stands. Otherwise a slot
|
||||||
|
// that has since been given something to say would be
|
||||||
|
// emptied again by every rebuild.
|
||||||
|
if !word.trim().is_empty() {
|
||||||
|
found.insert(t.clone(), word.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user