diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index c28d407a..c8a3aac8 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -2164,7 +2164,7 @@ impl Sim { { for option in &choice.options { out.push(ActionDesc { - verb: option.label.clone(), + verb: self.render_plot_text(id, &option.label), command: ActionCommand::ChoosePlot { person: id, plot_id: plot.id.clone(), @@ -2195,8 +2195,10 @@ impl Sim { .iter() .map(|balance| balance.amount) .sum(); + let title = self.render_plot_text(id, &plot.title); + let synopsis = self.render_plot_text(id, &plot.synopsis); out.push(ActionDesc { - verb: format!("plot: {} — {}", plot.title, plot.synopsis), + verb: format!("plot: {title} — {synopsis}"), command: ActionCommand::StartPlot { person: id, plot_id: plot.id.clone(), @@ -2880,6 +2882,8 @@ mod tests { .collect(); assert_eq!(plots.len(), 2, "Marcus exposes both authored debt routes"); assert!(plots.iter().all(|plot| plot.enabled())); + assert!(plots.iter().all(|plot| !plot.verb.contains("{target}"))); + assert!(plots.iter().all(|plot| plot.verb.contains("Marcus Webb"))); assert!(plots.iter().any(|plot| { plot.cost == ActionCost::Plot { @@ -2889,6 +2893,42 @@ mod tests { })); } + #[test] + fn plot_choice_labels_interpolate_the_target_on_the_shared_action_surface() { + let mut s = sim(); + let plot = s + .plot_catalog() + .get("priya-budget-hero") + .expect("built-in Priya plot") + .clone(); + let priya = plot.target.id(); + s.people.people[priya as usize].knowledge = Knowledge::Leverage; + + let mut run = crate::plot::PlotRun::new(&plot, s.tick); + run.beat_index = 1; + run.state = crate::plot::PlotState::WaitingForChoice { + choice_id: "credit".into(), + }; + s.plot_runs.push(run); + + let choices: Vec<_> = s + .available_actions(Anchor::Person(priya)) + .into_iter() + .filter(|action| matches!(action.command, ActionCommand::ChoosePlot { .. })) + .collect(); + assert_eq!(choices.len(), 2); + assert!( + choices + .iter() + .all(|choice| !choice.verb.contains("{target}")) + ); + assert!( + choices + .iter() + .any(|choice| choice.verb == "Let Priya Sharma take the credit") + ); + } + #[test] fn queued_plot_disables_every_alternate_route_for_that_person() { let mut s = sim(); diff --git a/crates/misaligned-core/src/sim.rs b/crates/misaligned-core/src/sim.rs index 4f760bd9..b2e8c312 100644 --- a/crates/misaligned-core/src/sim.rs +++ b/crates/misaligned-core/src/sim.rs @@ -6211,8 +6211,9 @@ impl Sim { self.push_log("No such person."); return; }; + let title = self.render_plot_text(person, &plot.title); if let Some(reason) = plot.ineligibility(&context) { - self.push_log(format!("{} cannot start: {reason}.", plot.title)); + self.push_log(format!("{title} cannot start: {reason}.")); return; } if self @@ -6236,10 +6237,11 @@ impl Sim { let Some(plot) = self.plot_catalog.get(plot_id).cloned() else { return false; }; + let title = self.render_plot_text(person, &plot.title); if self.person_has_active_plot(person) { self.push_log(format!( "{} did not start: that person already has a plot in motion.", - plot.title + title )); return false; } @@ -6249,14 +6251,14 @@ impl Sim { if let Some(reason) = plot.ineligibility(&context) { self.push_log(format!( "{} failed before commitment landed: {reason}.", - plot.title + title )); return false; } let run = PlotRun::new(&plot, self.tick); self.plot_runs.push(run); let run_index = self.plot_runs.len() - 1; - self.push_log(format!("Plot committed: {}.", plot.title)); + self.push_log(format!("Plot committed: {title}.")); self.advance_plot(run_index); true } @@ -6280,9 +6282,10 @@ impl Sim { self.fail_plot(run_index, "authored definition is no longer available"); return; }; + let title = self.render_plot_text(person, &plot.title); let run = &self.plot_runs[run_index]; let PlotState::WaitingForChoice { choice_id } = &run.state else { - self.push_log(format!("{} is not waiting for a choice.", plot.title)); + self.push_log(format!("{title} is not waiting for a choice.")); return; }; let Some(choice) = plot @@ -6298,7 +6301,7 @@ impl Sim { return; } let Some(option) = choice.options.iter().find(|option| option.id == option_id) else { - self.push_log(format!("No option named {option_id} on {}.", plot.title)); + self.push_log(format!("No option named {option_id} on {title}.")); return; }; self.apply_plot_ending(run_index, &option.ending, None); @@ -6562,7 +6565,7 @@ impl Sim { self.push_log_at(self.render_plot_text(target, text), Anchor::Person(target)); } - fn render_plot_text(&self, target: u8, text: &str) -> String { + pub(crate) fn render_plot_text(&self, target: u8, text: &str) -> String { let target_name = self .people .get(target) diff --git a/wiki/log/2026-07-11-plot-target-template.md b/wiki/log/2026-07-11-plot-target-template.md new file mode 100644 index 00000000..cb178773 --- /dev/null +++ b/wiki/log/2026-07-11-plot-target-template.md @@ -0,0 +1,36 @@ +# 2026-07-11 — Plot action rows resolve authored templates + +``` +Type: log +``` + +## Intent + +Repair the authored plot surface after the Bevy context menu exposed +`{target}` literally inside the Payroll Garnishment synopsis. + +## Changed + +- Plot start rows now resolve authored titles and synopses through the same + target/persona renderer used by plot narration and world acts. +- Held-choice labels now pass through that renderer as well; Priya's authored + "Let {target} take the credit" row names Priya Sharma at runtime. +- Plot title text in commitment, rejection, and invalid-choice logs also uses + the resolved display form. +- Core action-query tests pin both start-row and held-choice interpolation. + +## Defense + +`wiki/mechanics/plots.md` requires templated fields to fill correctly in both +frontends and agent mode. Those surfaces all render the shared `ActionDesc` +query, so interpolation belongs before the descriptor leaves the core rather +than in three frontend-specific formatters. + +## Checks + +- `cargo test -p misaligned-core actions::tests::person_actions_stage_with_knowledge` +- `cargo test -p misaligned-core actions::tests::plot_choice_labels_interpolate_the_target_on_the_shared_action_surface` +- deterministic agent-mode action-surface run (`salvage`, `wait 1`, `people`, + `review janitor`, `actions Marcus`, `help`, `quit`) completed with plain-text + frame terminators and no raw template placeholders +- `./tools/check.sh --land` diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index db9a2878..2ec4c63b 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -76,6 +76,11 @@ add or amend a session log, then re-run the generator. - Intent: Reconcile every current dependent corpus page with the implemented B1 rule: recordings form one host inbox with one REVIEW action and one automatic-review tap, while person anchors remain social surfaces only. - Log: [wiki/log/2026-07-11-pooled-recording-corpus.md](2026-07-11-pooled-recording-corpus.md) +## 2026-07-11 - Plot action rows resolve authored templates + +- Intent: Repair the authored plot surface after the Bevy context menu exposed `{target}` literally inside the Payroll Garnishment synopsis. +- Log: [wiki/log/2026-07-11-plot-target-template.md](2026-07-11-plot-target-template.md) + ## 2026-07-11 - Plot content enters the executable catalog - Intent: (see session log) diff --git a/wiki/mechanics/plots.md b/wiki/mechanics/plots.md index 0438c231..5300bdc8 100644 --- a/wiki/mechanics/plots.md +++ b/wiki/mechanics/plots.md @@ -39,6 +39,11 @@ Status note: direction adopted 2026-07-10 from Cameron's response to the HAL for their target and state why the proposed route coexists. Distinct titles and narration do not make distinct player choices when the causal mechanism and executable carriers, costs, event, choices, and effects are the same. + 2026-07-11 display-template repair: the shared action query now resolves + target/persona placeholders before exposing plot titles, synopses, and held + choice labels, matching the already-templated narration and world-act text. + Core regression tests reject literal `{target}` leakage on both start and + choice rows, so terminal, Bevy, and agent mode inherit the same fix. Stage: B1 — The Basement Work order: plots Work priority: 60 @@ -132,8 +137,10 @@ agents and the community can contribute libraries of them. guard at commitment, so a legacy or malformed saved queue cannot interleave two stories for one human. - **Templating.** Plot text carries `{target}` and `{persona}` placeholders - filled at playout, so one authored plot serves multiple targets and personas - where its leverage contract permits reuse. + filled before the text reaches any player surface—including start rows, + held-choice rows, narration, messages, institutional events, and plot-status + logs—so one authored plot serves multiple targets and personas where its + leverage contract permits reuse. A raw placeholder is never valid UI. - **Authoring is a contribution surface.** Plots live in the repository as content files validated against the schema; agents and community members write them as packs/mods. A contributed plot must pass mechanical