What changed, and why
Two guardrails on who a player may message, added for legal, ethical, and safety reasons — the game puts AI-authored words in a real person's mouth, so the living and the not-yet-historical are off limits.
- Deceased only (#72): a figure is contactable only when we can confirm they have died. Living and undatable figures fail closed. - Require grounding (#73): you can only reach a real figure resolved against Wikipedia/Wikidata. The old free-form fallback — type any name, the AI invents a persona — is gone; an unresolved name is guided to a real match instead.
Both rules are enforced at three layers — the client store gate (UX), the authoritative server re-ground (the guarantee), and the suggester — off one predicate. This guide reads the arc end to end.
One predicate: what 'contactable' means
isDeceased is the single source of truth, shared by the server gate and the suggester. It is deliberately strict: a resolved figure with no death year is living (or too undatable to prove otherwise), so it is not deceased — fail closed.
The transient flag (for fail-closed), isDeceased, and the recent-estimated-death guard.
server/utils/figure-grounding.ts · 323 lines
⋯ 51 lines hidden (lines 1–51)
⋯ 182 lines hidden (lines 70–251)
⋯ 31 lines hidden (lines 257–287)
⋯ 22 lines hidden (lines 302–323)
The client gate (UX)
contactLiveness folds both rules into one getter, in order: an ungrounded name is 'unresolved' (#73); a resolved figure with no death is 'living' (#72); a resolved, deceased figure is then gated to its lifetime. canContact admits only the contactable states, so the Send button disables itself.
The whole rule in one place; 'unknown' survives only for the narrow resolved-deceased edge.
stores/game.ts · 1265 lines
⋯ 335 lines hidden (lines 1–335)
⋯ 917 lines hidden (lines 349–1265)
The send is stopped before the network, with an honest, specific reason — still living, couldn't be dated, no record found (or, on a network blip, try again).
Each blocked state gets its own message; nothing is sent.
stores/game.ts · 1265 lines
⋯ 862 lines hidden (lines 1–862)
⋯ 387 lines hidden (lines 879–1265)
The authoritative server gate (the guarantee)
The client gate is convenience; a direct POST or a send racing the grounding debounce would skip it. So the turn endpoint re-grounds the name itself — the client payload is untrusted — and blocks. It is cache-backed in the warm path (the same lookup the dossier just did), one bounded call on a cold cache.
resolved-not-deceased → block; unresolved-definitive → block (#73); transient → retry.
server/api/send-message.post.ts · 330 lines
⋯ 136 lines hidden (lines 1–136)
⋯ 165 lines hidden (lines 166–330)
The same re-ground gate on the research surface.
server/api/research.post.ts · 89 lines
⋯ 50 lines hidden (lines 1–50)
⋯ 23 lines hidden (lines 67–89)
The suggester never offers someone you can't reach
Suggestions are one-tap contacts, so they get the rule too: a deceased-only instruction in the prompt, and — defense in depth against a model that ignores it — a re-ground filter that drops anyone not confirmed deceased before they reach the player. The curated fallback is all-deceased (pinned by a test).
Prompt rule, the keepDeceased filter, and where suggestFigures applies it.
server/utils/figure-suggester.ts · 284 lines
⋯ 67 lines hidden (lines 1–67)
⋯ 60 lines hidden (lines 71–130)
⋯ 123 lines hidden (lines 147–269)
⋯ 9 lines hidden (lines 276–284)
The dossier tells the truth
The picker stops offering what it can't deliver. A resolved-but-living (or undatable) figure shows an honest block in place of the when-slider; an unresolved name shows a guide to a real match, not a "send into the unknown".
The contactBlocked computed + reason, the block notice, and the unresolved guide.