ADR-063: Cast block and scan strip clarity rules¶
Status: Accepted Date: 2026-05-22 Context: ADR-061 (pre-commitment planning block) raised scenario quality significantly — concrete numbers, named characters, sharp per-option lock-in costs. But two new problems surfaced once players started reading the resulting scenarios:
-
Mid-option character introduction. Named characters whose pushback was option-specific were appearing for the first time inside an option's description. A player reading Option B would meet "Britt Solano" with no prior context, then have to infer her role from a sentence about her leaving creating "a 6-year community-partnerships gap" — implying she runs community partnerships. This back-construction is the exact cognitive load the scan strip was meant to remove.
-
Scan strip jargon that didn't land cold. The scan strip's contract was "parses cold without reading the description." In practice, the model was reaching for compact domain terminology that fit the ≤ 70-char target but required insider knowledge. Examples flagged by CoachJ on review:
- "Program grants came in at 38% of projection; CFO flagged $1.36M endowment risk" — "endowment risk" and "38% of projection" are nonprofit-finance jargon, and "38%" requires the reader to infer that the number means a shortfall, not upside.
- "Article 14 panel convenes in 18 hours; late submission defaults to Meridian" — "Article 14" and "defaults to Meridian" are undefined proper nouns the description has to teach the reader.
- Option C in the same scenario: "you lose the framing entirely" — "the framing" appears nowhere else in the option; reader must back-construct what it refers to.
The structural format from ADR-061 + Variant D (scan strip introduced in migration 105) was sound. The failure was at the language layer, not the structure layer.
At a glance
What it decides: The Variant D scenario layout gains a cast_block that introduces every named character (name, role, stake) once up front, and the scan strip gets four explicit forbidden-language patterns — so players never back-construct who someone is or what a row means. Shipped as generator v1.7.0.
- Cast block (migration 106,
ScenarioCastBlockcomponent) renders between scan strip and description when a scenario has ≥ 2 named characters, capped at 3-5 entries; character-light scenarios skip it. - Four forbidden scan-strip patterns — no domain jargon, no undefined proper nouns, no inference-required shorthand ("38% of projection" → "62% short of expected"), no summary-noun abstractions ("the framing"). Also apply to cast role/stake fields, not to the description (which glosses jargon inline per ADR-054).
- Word budget relaxed to 200-250 target / 270 hard ceiling — empirically scenarios landed at 211-260 words; "wordy and clear beats concise and unclear."
- Rejected role gloss in options (doesn't scale past 2-3 characters), sidebar cards (render complexity), options-first rendering (breaks orient→ground→choose), and runtime word-count validation (treats calibration as compliance).
- Watch for: mobile complaints about orientation-heavy headers; players still confused by unintroduced characters despite a present cast block (means it isn't being read). No curated backfill until a triggering confusion report.
flowchart TD
A["Title"] --> B["Scan strip<br/>(≤ 70 char rows, plain language)"]
B --> C["Cast block<br/>(name · role · stake, 3-5 entries,<br/>cyan left edge)"]
C --> D["Description<br/>(200-250 words, jargon OK with<br/>inline gloss)"]
D --> E["Options A-D"]
B -.->|"orient"| F["Player reading order"]
C -.->|"ground"| F
E -.->|"choose"| F
Variant D orientation surfaces, top to bottom — orient (scan strip), ground (cast block), then choose (options).
Decision¶
Two additions to the Variant D scenario contract, shipped together as scenario generator v1.7.0:
-
Cast block — a new
cast_block jsonbfield (migration 106) onscenarios, rendered between the scan strip and the description by a newScenarioCastBlockcomponent. When a scenario has ≥ 2 named characters across description + options, the cast block introduces each with{ name, role, stake }before any option mentions them. Capped at 3-5 entries. Character-light scenarios (0-1 named characters) skip it entirely. The cast block deprecates the "role gloss in options" alternative — the cast block does the introduction once, and options refer to characters by name (or first name on second mention) without inline role glosses. -
Scan strip clarity rules — promoted from "scan strip is plain language" (already in v1.6.0) to four explicit forbidden patterns enforced at the prompt-rule layer:
- No domain jargon in scan strip rows. The description carries jargon (and glosses it inline); the strip cannot. Applies to nonprofit-finance, legal, medical, technical, government jargon alike.
- No undefined proper nouns. Statutes, articles, systems the reader hasn't met by the time they read the row. Replace with the function of the named thing (and ensure the function-name is itself plain English — "Compact review" still throws cold readers).
- No inference-required shorthand. Phrases where direction (good/bad, up/down) requires inference. "38% of projection" out, "62% short of expected" in.
- No summary-noun abstractions. "The framing," "the math," "the constraint" as primary carriers of consequence — they require referent back-construction.
The same forbidden patterns also apply to cast block role and stake fields (cold-readability extends across both orientation surfaces). They do not apply to the description, which can use jargon freely with inline glosses (per ADR-054 jargon-translation rule).
Additionally, the description target was relaxed from 180-220 / 250 hard ceiling to 200-250 target / 270 hard ceiling. Empirical observation across six v1.6.0 test outputs settled at 211-260 words; the 180-220 target was aspirational, not realistic. The 270 ceiling buys headroom for definitional clarity per the new principle "wordy and clear beats concise and unclear" — a guidance line CoachJ articulated during the v1.7.0 review.
Rationale¶
The cast block is structurally the smallest change that removes the inference-tax problem without enlarging the description budget. Three alternatives were considered (see below); the cast block won on three grounds:
- Cost per scenario is bounded — 3-5 short rows, capped. Doesn't grow with character count beyond the cap.
- Render slot is shared — same orientation surface as the scan strip, slightly differentiated (left-edge cyan accent) so they read as a paired set without competing.
- Feeds existing image-gen metadata pipeline — the
scenario_character_namestable (migration 100) already collects identity tags; the cast block surfaces the same characters in a player-visible form without new generator work.
The scan strip clarity rules are codifying what the prompt already gestured at but didn't enforce. The original rule said "plain language, no markdown, ≤ 70 chars" — but "plain language" was interpreted by the model as "no markup," not "no jargon." Naming the forbidden patterns explicitly closes the gap that produced Sample 4's "Article 14 panel" scan strip and Sample 1's "endowment risk" row.
The word budget relaxation acknowledges that ADR-061's concrete-detail pressure (numbers, characters, prior history, lock-in) plus ADR-054's inline-gloss requirement plus the new clarity rules collectively cost words. Holding the tight 180-220 target while adding requirements produced a model that either truncated context or padded into the gloss-budget. The 270 ceiling gives the model room to satisfy all three pressures simultaneously.
Alternatives Considered¶
-
Role gloss in options (Alternative A) — when a character first appears in an option, prepend a 2-3 word role descriptor: "Britt Solano, head of community partnerships, stays and leads…" Rejected because it doesn't scale: scenarios with 4 named characters spread across 4 options force the reader to absorb introductions one option at a time, fragmenting the mental model of who's in play. The cast block introduces everyone once, up front. (Role gloss was also flagged by an external reviewer who'd seen the same problem independently — both reviewers preferred the cast block approach for the same scale reason.)
-
Sidebar character cards (Alternative B) — render named characters in a sidebar with avatars + role + stake. Rejected on render complexity (mobile reflow, avatar generation pipeline, layout shift) and information density (sidebar is harder to scan than an inline strip). The cast block is the same data in a simpler container.
-
Options-first rendering (Alternative C) — render the four options at the top with the description below as "context." Rejected because it changes the player's reading order in a way that interacts poorly with driver-tracking signal: skim options first → pattern-match the option set against driver labels → choose by perceived identity rather than weighted trade-off. Variant D's "orient → ground → choose" sequence preserves the dilemma-engagement target from ADR-027.
-
Runtime word-count validation (rejected for description target) — add a validator that rejects descriptions over 250 words and forces regeneration. Rejected because it treats a calibration problem as a compliance problem — the model has already optimized; the empirically-observed length is telling us the target was wrong. Tightening the constraint with validation produces truncated scenarios or expensive retries, neither of which serves quality.
Discussion¶
The conversation that produced this ADR was a multi-stage critical evaluation of Variant D after v1.6.0 shipped. CoachJ asked an outside reviewer (this assistant) to not validate the existing decision and to flag specific failures. The discussion identified two distinct failures (character mid-option introduction, scan strip jargon), and the response had to disentangle which was a format problem and which was a language problem.
The cast block emerged from comparing two alternatives (role gloss vs. cast block) and recognizing that role gloss didn't scale beyond 2-3 characters. External feedback CoachJ had received independently confirmed the cast block direction — a second reviewer had also recommended "a list of characters up front."
The scan strip clarity discussion went through three iterations as CoachJ pressure-tested each proposed fix cold: 1. The first fix ("name the function, not the article") was insufficient because the function-name itself ("Compact review") was still jargon to a cold reader who wasn't sure "Compact" = "pact." This produced the additional rule: function-names must themselves be plain English; drop qualifiers when in doubt. 2. The second fix ("Expected grants came in 62% short") was readable but still ambiguous on what kind of grants. CoachJ pushed for "Expected revenue from grants" — the additional word removed the last bit of inference. This calibrated the principle "wordy and clear over concise and unclear" with a concrete example. 3. The discussion of "the framing" in Option C extended the same rule from scan strip to options: summary-noun abstractions are forbidden anywhere the abstraction's referent isn't stated in the same option/sentence.
The driver-label leak risk was raised separately but is not in scope for this ADR. The proposal — strip (principle), (pragmatism), (growth), (caution) tags from rendered option titles to prevent identity-vs-process performance — was confirmed as already-internal-only by CoachJ. If they ever do render, this ADR's identity-vs-process discussion would extend to require their removal. The flag is in the conversation memory (feedback_wordy_clear_over_concise_unclear) for future review.
The HTML preview at docs/design/variant-d-cast-block-preview.html was used to visually validate the final format before code work began. Two preview iterations were required: the first was generic dark theme, the second matched the actual play-page styling (--surface card on --background page, border-2 options with teal selection). CoachJ confirmed the second matched their mental model of the game well enough to ship.
Consequences¶
- New schema column
scenarios.cast_block jsonb(migration 106), nullable. Pre-Week-24 scenarios render with no cast block; the UI checks for non-empty array presence. No backfill required — historical scenarios were generated without character density in mind and would not benefit uniformly from a cast block. - Generator version bump to v1.7.0. The
planningblock now requirescastBlockDraft; the response includescast_block; validation enforces shape on present arrays but accepts absence for character-light scenarios. - Description target relaxed to 200-250 / 270 hard ceiling. Existing scan-strip validator (
≤ 100 chars/row) is unchanged but now documented as the hard cap (target remains ≤ 70). - Render footprint above the description grows by 1 block on scenarios with ≥ 2 named characters. Vertical density above the prose is title + scan strip + cast block. On mobile, this is a longer scroll before reaching the description; watch for player complaints about orientation-heavy headers.
- Curated scenario backfill defer — the 254 clean curated scenarios are not getting cast blocks in this pass. Per CoachJ's "scan-strip-only first" approach from the v1.6.0 backfill, cast blocks are a fresh-generation-only addition until a triggering instance (a curated scenario actively surfacing in play whose lack of cast block produces a confusion report).
- Trigger to revisit:
- Cast block rows breaking the layperson-readable rule (e.g., role text using jargon) — would indicate the prompt rule isn't being held consistently in the cast block surface
- Render complaints on mobile about vertical density of orientation blocks
- Players reporting confusion at unintroduced characters in options despite the cast block being present — would indicate the cast block isn't being read, not that it's missing
- Driver labels ever appearing in rendered option titles — re-open identity-vs-process review
Key files:
- migrations/106_scenarios_cast_block.sql — schema column for the cast block
- src/types/database.ts:166-184 — CastMember type + cast_block?: CastMember[] | null on Scenario
- src/lib/ai/scenario-generator.ts — v1.7.0 prompt, sanitization, validation, and toDbScenario persistence
- src/components/game/ScenarioCastBlock.tsx — render component, mirrors ScenarioScanStrip treatment with cyan left edge
- src/app/play/[sessionId]/page.tsx — wired into active card (~1616) and both review cards (~1986, ~2632)
- src/app/solo/page.tsx — wired into solo active card (~1276)
- docs/design/variant-d-cast-block-preview.html — design preview used to validate the format before code work