ADR-034: Case-study voice as the scenario voice contract¶
Status: Accepted
Date: 2026-04-29
Context: A static-arc rewrite on Apr 23 (driven by scripts/audit-static-arc.ts) re-formatted all 152 static scenario descriptions to open with a "Quick Context" block — a bulleted orientation header listing You are:, Key people:, and The situation:, followed by a Your position: prose recap. The intent (from ADR-029 and earlier voice notes) was to give cold players a fast orientation. In practice the format produced verbose, roleplay-style scenarios that read like LARP briefings instead of decision cases. The user playing "The Floor Plan Map" (Ch.1 of The Fever Road) flagged it as "super verbose and not really case study-ish" — and confirmed that every scenario in this batch had the same problem because the same template had been applied to all 22 audited arcs.
Decision¶
Scenario descriptions — both static-arc scenarios.description rows and live LLM-generated reactive scenarios — must use case-study voice:
- The first sentence establishes role + situation as prose. No preamble, no orientation block.
- 2-4 short blocks follow, with optional Bold Headers: when structure helps scanning.
- Named characters appear only when their view changes the decision; quotes earn their place.
- The description ends with a one-sentence decision prompt naming the deadline or trigger.
- Length: under 350 words (boardroom) / 450 words (themed/narrative).
The following formats are banned and will be rejected at runtime:
**Quick Context:**blocks of any kind- Bulleted orientation headers used as a top-of-scenario block:
**You are:**,**Key people:**,**The situation:**,**Your position:**,**Your role:** - Cast-list intros that name 3+ people with parenthetical roles before any situation is described (e.g. "Dayo (caravan leader), Fen (navigator), Cass (sick child)")
- Stage-play formatting (a character name on its own line above their dialogue)
- Re-stating the same setup twice (orientation block then prose recap)
A shared regex guard (detectBannedVoicePatterns) lives in src/lib/ai/scenario-generator.ts and is invoked by:
validateScenario(rejects and triggers the existing retry loop ingenerateScenario)POST /api/campaigns/generate-arc(returns 502 if any chapter intro/synopsis matches)scripts/audit-static-arc.ts(throws before the rewrite reaches the proposal file)
Rationale¶
- The ADR-029 contract held; the prompt template betrayed it. ADR-029 said synopses should "match Coach J's solo quick-round quality bar" — crisp role + situation language. The audit prompt re-interpreted that as "add a Quick Context block above every scenario." The synopsis field stayed correct; the
descriptionfield regressed. - Case-study voice is what the original static arcs already had. The rollback dry-run showed the originals were 800-1400 chars of clean prose with bold section headers, named voices, and a decision prompt — exactly the voice we want. Restoring originals is cheaper, faster, and lower-risk than re-rewriting.
- A guard at three call sites makes "never again" enforceable. A documentation rule alone can't prevent regression; runtime rejection can. The guard lives next to the validator so any new generation path that reuses validation gets it for free.
Alternatives Considered¶
- Re-rewrite all 152 scenarios in case-study voice via LLM. Rejected: the originals were already in the target voice. Regenerating would burn ~$0.50, take an hour, and risk a third drift. The audit JSON files already preserved every original, and
rollback-audit.tsalready existed. - Soft-warn instead of hard-fail in the guard. Rejected: "never happen again" requires hard enforcement. The retry loop in
generateScenarioalready exists; a hard fail there is invisible to players (they get a re-rolled scenario) and loud to operators (logs). - Update only the prompt, leave the DB alone. Rejected: the user was actively encountering the verbose format mid-playthrough. Prompt changes affect future generation; they don't repair stored content.
- Delete
audit-static-arc.tsoutright. Rejected: the audit-*.json files it produced are the source of truth forrollback-audit.ts. Deleting the script orphans those files. Instead, the script's system prompt was rewritten to match the case-study contract, with a deprecation banner pointing to this ADR.
Discussion¶
The instinct on first read was to fix the prompt and call it done. The user's "fix this and never let it happen again for any scenario ever" reframed the scope: this isn't a prompt patch, it's a contract that has to hold across three generation paths and one batch-rewrite script. The guard was the answer to "any scenario ever" — without it, the next person who writes a new generation pathway can re-introduce the format with no friction.
The other deliberation point was whether the LLM-generated reactive scenarios would suffer from removing the orientation block. The Quick Context block was originally added because "new players are dropped in cold." That concern is real, but the solution was wrong: the fix is a strong opening sentence (case-study voice already requires this), not a redundant header above the prose. The first sentence doing both jobs is more scannable than a header block followed by a recap, not less.
Consequences¶
- 152 static scenario descriptions reverted to their pre-Apr-23 originals (one DB write each via
rollback-audit.ts --all). SCENARIO_GENERATOR_VERSIONbumped from 1.2.0 to 1.3.0 to signal the prompt contract change.- Future LLM generations that regress will fail validation and either retry (reactive scenarios) or surface a 502 (custom arc generation). Operators see the
BANNED_VOICE_PATTERNSrejection in logs. - ADR-029's contract is unchanged; this ADR documents the voice contract that ADR-029 implied but did not enforce.
Key files:
- src/lib/ai/scenario-generator.ts — case-study voice section, BANNED_VOICE_PATTERNS, detectBannedVoicePatterns, validateScenario guard
- src/app/api/campaigns/generate-arc/route.ts — guard against banned patterns in chapter_intro / chapter_synopsis
- scripts/audit-static-arc.ts — system prompt rewritten, deprecation banner, runtime banned-pattern check
- scripts/rollback-audit.ts — used to restore the 152 originals
- scripts/data/audit-*.json — source of truth for the rollback (preserves originals)