What changed, and why
A run's objective brief is the description field on GameObjective. It shows at mission select and folds behind "▾ brief" in the HUD — but it is not flavor text. The same string threads into the Character, Timeline Engine, Chronicler, research, and figure-suggester prompts every turn, so the model reads it constantly.
The old briefs were vague and quietly assumed a US/Western frame. "Abolish Slavery a century early" — a century early relative to what? It leaned on an unstated 1865. A player who does not hold that date had no anchor.
This change rewrites all 10 curated briefs to carry, explicitly: the real historical date(s) the counterfactual departs from, and where on Earth it is centered — in short • bullet lines. The AI generator that composes fresh objectives is taught the same shape. The fields and types are unchanged (description stays a string), so the blast radius is small: the pool data, one prompt, a char cap, two render sites, and tests.
The grounded briefs
The shape decision was the issue's open question: keep description a string, or add a structured contextBullets field? This took the KISS path — a string of newline-separated • lines — so nothing downstream (schema, guards, e2e mocks) has to move. The type doc now spells out the contract, including the one trap worth naming.
The contract: a bulleted brief, rendered newline-aware, and explicitly not the same as anchorYear.
server/utils/objectives.ts · 152 lines
⋯ 8 lines hidden (lines 1–8)
⋯ 122 lines hidden (lines 31–152)
Two briefs below show the bar. Each leads with the real dates and place, then what winning looks like. The dates were adversarially fact-checked (two independent lenses per brief), and the corrections show: the Black Death brief names the 1330s Central-Asian origin — distinct from the 1347 anchor — and the Industrial-East brief dates Watt's engine to its 1769 patent, not the 1776 first install.
Slavery (anchor 1750, real dates 1807-1888) and Black Death (1330s origin vs 1347 anchor).
server/utils/objectives.ts · 152 lines
⋯ 103 lines hidden (lines 1–103)
⋯ 10 lines hidden (lines 114–123)
⋯ 19 lines hidden (lines 134–152)
Teaching the generator the same shape
Objectives can also be composed live by the model. The old prompt asked for "a vivid one-or-two-sentence description." It now requires the same dated, located, bulleted form, with a few-shot exemplar so the model has the shape in front of it, and the schema's field annotation matches the prompt. So a composed objective reads like a curated one.
The reworked user prompt (with exemplar) and the matching schema annotation.
server/utils/objective-generator.ts · 78 lines
⋯ 37 lines hidden (lines 1–37)
⋯ 18 lines hidden (lines 61–78)
Raising one cap without moving another
Dated, bulleted briefs run longer than the old one-liner, so the objective-description cap rises from 400 to 700. But that constant had a second, unrelated tenant: the timeline-ledger detail field (a model-written ripple sentence) borrowed the same MAX_OBJECTIVE_DESC_CHARS. Raising it would have silently widened the ledger bound too. So the ledger cap is split out as its own MAX_LEDGER_DETAIL_CHARS, holding the old 400.
The description cap rises to 700; the ledger-detail cap splits out and keeps 400.
server/utils/validate.ts · 44 lines
⋯ 12 lines hidden (lines 1–12)
⋯ 21 lines hidden (lines 24–44)
The ledger detail now uses MAX_LEDGER_DETAIL_CHARS — the same fix landed in chronicle.post.ts.
server/api/send-message.post.ts · 365 lines
⋯ 92 lines hidden (lines 1–92)
⋯ 266 lines hidden (lines 100–365)
Rendering, and the tests that pin it
Both render sites showed description in a single element where newlines collapse. The fix is one Tailwind utility — whitespace-pre-line — so the • lines render as separate rows. Mustache ({{ }}) still auto-escapes, so no new injection surface; the unsafe-render and a11y-source loops were run over the touched components.
The folded HUD brief, now newline-aware.
components/ObjectiveDisplay.vue · 30 lines
⋯ 9 lines hidden (lines 1–9)
⋯ 17 lines hidden (lines 14–30)
The mission-select row, same treatment.
components/MissionSelect.vue · 133 lines
⋯ 28 lines hidden (lines 1–28)
⋯ 103 lines hidden (lines 31–133)
The discriminating tests assert the new layer, not coverage. Every curated brief must carry a real date, read as bullet lines, and fit the cap; and the slavery brief's real dates must be present and distinct from its 1750 anchor — the conflation the issue warns against.
The grounded-brief contract: real date + bullets + cap, and anchorYear kept distinct.