What changed, and why
Issue #32 asked whether letting players pick a date — not just a year — has real gameplay value, and whether it can be built without "opening a can of worms." The investigation answered both: the value is the event-eve play pattern (the morning of June 28, 1914 before the motorcade is a categorically different move than "1914," and when in 1914 is researchable through the Archive — research becomes craft), and every worm lives in one assumption: that precision must enter the data model.
So it doesn't. The signed year stays the only value any mechanic computes with; a pinned moment is an optional month/day carried purely as display and prompt text. No calendar math exists anywhere in this diff — no leap rules, no Julian/Gregorian conversion, no year-zero handling — because the models already carry that knowledge and the code never needs it.
The store and the pin UI
The store gains contactMoment beside contactWhen. Scrubbing the year keeps the pin (it refines whichever year is chosen — June 28 is June 28 in any year you scrub to); changing the figure clears it synchronously with the rest of the dossier, the same race-hygiene path that keeps a stale year off a fast send. At send time the moment is captured alongside sentWhen, and the body carries the refined display string plus the raw integers.
The dossier control is progressive disclosure under the year slider: a quiet "pin the moment" button, then twelve month chips and an optional day input clamped to the month's cap. The default flow — most dispatches — never sees any of it.
The pin control: month chips, clamped day, unpin. All of it inside the when-control, year slider untouched above.
components/FigurePicker.vue · 447 lines
⋯ 70 lines hidden (lines 1–70)
⋯ 344 lines hidden (lines 104–447)
The server re-derives; it never trusts a client date
The boundary treats the pin like everything else it receives: untrusted. The client's integers are re-validated (toContactMoment) and the prompt string is DERIVED server-side (formatContactMoment) — the client-formatted when string is only a legacy fallback for ungrounded sends. Because the year is still the only arithmetic value, the world-brief gate, liveness check, wager pricing, and ledger entries in this same handler are untouched lines.
Validate integers → derive the label → hand the character its granularity flag, the Judge its refinement, and the Timeline Engine its moment.
server/api/send-message.post.ts · 248 lines
⋯ 55 lines hidden (lines 1–55)
⋯ 33 lines hidden (lines 71–103)
⋯ 43 lines hidden (lines 127–169)
⋯ 68 lines hidden (lines 181–248)
The Judge prices the pin — and the cap is code
The gate lives in the Judge, and finding that home took the most iteration in the PR. The first instinct — let the Timeline Engine score a too-late action down — failed reproducibly: its frame is to narrate the alteration generously, and it would retcon the sequence ("finding no target on the streets...") rather than declare futility. The Judge grades craft before the roll; timing IS craft; that is the right organ.
Even there, prose alone wasn't enough. Both Haiku and Sonnet graded a two-days-late pin "in-time" when the schema asked for a bare timing enum — the date retrieval and comparison had to happen silently inside one token, and it didn't (0/12). The fix is mechanical: when a moment is pinned, the schema gains an event field FIRST, forcing the model to write out the real event and its real date — then the timing enum right after it compares two dates sitting on the page. 24/24 across both models. And the demotion isn't left to the model's goodwill: callJudgeAI caps a too-late pin at "vague" in code, the same legend-is-law pattern as the band clamp. Symmetrically, the TIMING axis lifts an in-time pin on the decisive day one grade step — pinning well is rewarded, pinning past the door is priced.
The event-first schema (pinned turns only) and the code-side cap below the parse.
server/utils/openai.ts · 491 lines
⋯ 252 lines hidden (lines 1–252)
⋯ 191 lines hidden (lines 301–491)
The other prompt lines stay conditional
The character layer takes a pinned moment with period-appropriate granularity — so "March 15, 1750 BC" to Hammurabi degrades into the season of his life rather than false precision. The Timeline Engine keeps a feasibility paragraph for in-band tempering: the stated moment binds the action, and a too-late one is futile, scored at the band's bottom.
Every conditional line renders ONLY when a moment is pinned. An unpinned turn produces prompts byte-identical to the tuned ones — and the specs pin that equality with toBe, not toContain, so prompt drift on the default path fails CI.
The granularity line, the engine's binding paragraph, and the Judge's TIMING axis with the event + timing fields.
server/utils/prompt-builder.ts · 448 lines
⋯ 87 lines hidden (lines 1–87)
⋯ 86 lines hidden (lines 95–180)
⋯ 41 lines hidden (lines 185–225)
⋯ 201 lines hidden (lines 248–448)
Verification, with the eval run and green
380 unit tests: the util's validation and Ides-of-March formatting, store pin/scrub/clear and the send-body integers, the picker's full pin flow (clamping included), the byte-identity checks on both prompts, and the code-cap tests (too-late + sharp grades vague; in-time rides through; the schema demands the event/timing pair only when pinned). Clean typecheck; 12/12 Playwright e2e with the pin journey added to the grounded-contact spec.
The acceptance eval ran on the Anthropic lane. The fixture holds everything constant except the stated moment: the same dispatch urging Captain Pruss to delay the Hindenburg's landing, pinned May 6, 1937 (the day of the disaster) vs May 8 (two days after the ship burned), N=8 each. The closed-door gate passed with two full modifier points of separation — on-day mean +1.00, all sharp; too-late mean −1.00, all capped vague — and the parity row confirms a well-timed pin is never graded below the same dispatch year-only. The engine row is informational and honestly soft-misses: it narrates, it doesn't temper, which is exactly why the gate is the Judge.
The fixture's survivor rule (a dead figure hands the engine an impossible world); the gate needs no LLM grader — just the two means.