What changed, and why
The anachronism tier — how far a 160-character message reaches beyond the figure's own era — drives a deterministic wager amplifier (server/utils/anachronism.ts): the further you reach, the wider the swing, both ways. The player is meant to read that tier and trust it. The problem: it was rated inside the Timeline Engine, the same call that already knows the goal, the dice roll, the progress, and the ledger. A number that load-bearing should not also be a place those things can leak in. Same idea, same figure, same year — but the tier could flex with the objective or the roll, because the call rating it could quietly soften or sharpen the reach to fit the outcome it was narrating.
This change splits the rating into its own call, **callReachRater**, that sees only the message and the figure's era/contact-year. No goal, no dice, no progress, no ledger. The tier becomes the same stable fact on every reading. The Timeline Engine stops rating it and starts receiving it: the tier is now an input that feeds the unchanged amplifier, and the engine echoes it straight back into its result, so nothing downstream (the ledger badge, the snapshot, the reveal equation, the e2e harness) sees a different contract.
It is the sibling of #162, which made the craft Judge objective-blind for the same reason. The two now run side by side, both deliberately blind. And because the rater needs nothing from the turn's outcome, it is the same call a pre-send wager estimate can reuse while you are still composing (#134).
The blind rater's prompt
The prompt states its single job and its blindness in the first sentence, then hands the model just the figure, their time, and the message. The era line prefers the signed contact year (best for the AD/BC arithmetic), falls back to a free-text when, and finally to the figure's own known era — so a grounded turn, a loosely-dated one, and a bare name all get a sensible anchor.
Era resolution, then the prompt: blind by the first line, tiers verbatim from the old engine, reach written out before the enum.
server/utils/prompt-builder.ts · 590 lines
⋯ 239 lines hidden (lines 1–239)
⋯ 325 lines hidden (lines 266–590)
The rater call: never-throw, coerced tier
callReachRater follows the same envelope as every other layer: build prompt, structured call, parse, coerce, return a typed result, never throw. The reach string is required in the schema (it forces the generation step) but the caller only needs the tier; toAnachronism coerces anything off-menu to the safe in-period. On an empty or failed call it returns success: false, and the orchestrator falls back to in-period — no widening, never a punishment for an infra hiccup.
The response type, then the call: reach-before-tier schema, coerced tier, graceful failure.
server/utils/openai.ts · 650 lines
⋯ 285 lines hidden (lines 1–285)
⋯ 18 lines hidden (lines 296–313)
⋯ 303 lines hidden (lines 348–650)
The Timeline Engine: from author to consumer
The engine's own diff is mostly subtraction. The tier leaves its output schema and its parse entirely; it arrives instead as an optional input arg, defaulting to in-period. The amplifier line is unchanged — it just reads args.anachronism now instead of parsed.anachronism — and the tier is echoed back into the ripple, so the wire contract is identical.
anachronism becomes an input; gone from the schema/required; the amplifier reads the arg and the value is echoed onward.
server/utils/openai.ts · 650 lines
⋯ 193 lines hidden (lines 1–193)
⋯ 15 lines hidden (lines 198–212)
⋯ 15 lines hidden (lines 219–233)
⋯ 409 lines hidden (lines 242–650)
The prompt loses the rating instructions but keeps one line: the engine is told the reach is priced separately and deterministically, so it must not inflate its own swing for boldness. That keeps the amplifier applied exactly once.
The do-not-double-count line survives; the four-tier rating block is gone.
server/utils/prompt-builder.ts · 590 lines
⋯ 206 lines hidden (lines 1–206)
⋯ 383 lines hidden (lines 208–590)
Orchestration: beside the Judge, into the amplifier
Both pre-roll classifiers read the same (message, figure, when) and neither depends on the other, so they run together under one Promise.all. The reach-rater adds no latency to the turn. Each fails safe independently: the Judge to sound (no die tilt), the reach-rater to in-period (no widening).
Judge and reach-rater, concurrent and both objective-blind; the tier resolved with its safe fallback.
server/api/send-message.post.ts · 387 lines
⋯ 194 lines hidden (lines 1–194)
⋯ 163 lines hidden (lines 225–387)
The resolved tier is then handed to the Timeline Engine alongside the craft grade and momentum, where the unchanged amplifier consumes it.
The rated tier flows into callTimelineAI as an input.
server/api/send-message.post.ts · 387 lines
⋯ 284 lines hidden (lines 1–284)
⋯ 98 lines hidden (lines 290–387)
The route, and dev-mode parity
The new task gets its own lane in the routing table: Haiku at temperature 0, a cheap classifier off the critical generation path, mirroring the Judge.
The reach-rater route — Haiku, temp 0, blind by intent.
server/utils/ai-routing.ts · 194 lines
⋯ 99 lines hidden (lines 1–99)
⋯ 85 lines hidden (lines 110–194)
Dev mode scripts every AI task so a forced turn runs the real pipeline with no model calls. The shared fixture moves anachronism off the timeline script and onto a new reachRater script, and the dev route registers it under the reach-rater task — so a forced turn still drives the real amplifier from a chosen tier.
The reach-rater fixture script, built from the forced tier.
utils/fixtures/turn-fixture.ts · 143 lines
⋯ 122 lines hidden (lines 1–122)
⋯ 17 lines hidden (lines 127–143)
Registered under the reach-rater task, beside the other layers.