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
+44
-10
@@ -692,9 +692,13 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
||||
"description": plan.about,
|
||||
"opener": { "heading": "What we do", "description": "", "features": does },
|
||||
"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!({
|
||||
"heading": humanize(&d.id), "description": format!("For {}.", d.who), "button": "Continue",
|
||||
"icon": "", "for_whom": "",
|
||||
"behind": format!("{}.", capitalize(&plan.needs_behind(&d.id).iter().map(|n| n.wants.as_str()).collect::<Vec<_>>().join(", "))),
|
||||
}))).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()
|
||||
.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 }))
|
||||
// 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();
|
||||
let links: Map<String, Value> = plan
|
||||
.needs
|
||||
.iter()
|
||||
.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();
|
||||
// One invite form per group this desk may fill, and the list of
|
||||
// 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)),
|
||||
"button": "Invite",
|
||||
"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" },
|
||||
}))
|
||||
})
|
||||
@@ -771,11 +785,17 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
||||
let invited = if invites.is_empty() {
|
||||
Value::Null
|
||||
} 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(
|
||||
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.
|
||||
@@ -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 ")),
|
||||
"heading": humanize(bucket),
|
||||
"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",
|
||||
}),
|
||||
);
|
||||
@@ -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(),
|
||||
(_, "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_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(),
|
||||
("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}"),
|
||||
@@ -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
|
||||
// labelled strip on the card.
|
||||
let (icon, for_whom) = (icon_name(s(&dw["icon"])), s(&dw["for_whom"]).trim());
|
||||
if icon.is_some() || !for_whom.is_empty() {
|
||||
let mut f = json!({ "name": for_whom, "description": "" });
|
||||
let behind = s(&dw["behind"]).trim();
|
||||
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); }
|
||||
alt["features"] = json!([f]);
|
||||
}
|
||||
@@ -1366,7 +1394,7 @@ pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BT
|
||||
"description": s(&bw["description"]),
|
||||
"action": format!("/{group}"),
|
||||
"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 },
|
||||
"requires_group": group,
|
||||
// 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}"),
|
||||
"consequence": [s(&iw["button"])],
|
||||
"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": "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"]),
|
||||
"action": format!("/{group}"),
|
||||
"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" },
|
||||
"requires_group": group,
|
||||
"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"]),
|
||||
"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 },
|
||||
"public": true,
|
||||
"jq": projection,
|
||||
@@ -2227,8 +2255,14 @@ 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}') => {
|
||||
// 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() };
|
||||
// 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