Prime Suspect — The Inference Pass
Date: 2026-09-20. Follows the feel playtest of all eight cases (docs/reviews/2026-09-20-prime-suspect-cases-decisions.html).
What we are fixing
Eight playtesters worked the cases as a 16-year-old. They agreed on four things:
| Finding | Addressed here | |
|---|---|---|
| 1 | You can tell which clues matter before pulling them | §1 Hide the tell |
| 2 | No clue needs another one; every elimination is a separate subtraction | §2 Load-bearing pairs |
| 3 | The ending is a tally — the culprit is whoever is left | §3 Name the chain |
| 4 | The math sits below the audience (1.8–2.2 of 5) | Not addressed. Separate pass; it means new questions, not rewiring |
Rejected on the way in: a positively incriminating clue per case. One clue that names the culprit has to be the last clue pulled or the case ends early, which wastes the authoring.
§1 Hide the tell (app only)
Now: every clue card on the working screen carries its category as a mono tag, pulled or not (ClueCard, CaseWorkView.swift). A category reads as "measurement" or "flavor", so the board sorts itself before a minute is spent.
Change: an unpulled card shows glyph, title and state only. The category appears on the card once the clue is pulled, and stays where it already is on the clue screen (the facts-card kicker).
Honest limit: the title still signals — "The Framer's Bill" reads as flavor beside "The Dust Print". Hiding the category narrows the tell rather than closing it. Closing it properly is an authoring rule (a clue that narrows nothing gets a title as inviting as one that does) plus §2, which reduces how many clues are inert.
Tests: an unpulled card's accessibility label carries no category; a pulled card's does.
Cost: one view, one test, no content. Nothing re-exported.
§2 Load-bearing pairs (content + lint)
The rule: in every case, exactly one innocent is pair-only — no single clue rules them out, and a named two-clue combination does. The player has to hold two findings at once and cross them.
Why this shape: the structure already exists. Cases 005–008 each carry a linked_clears group, and in every one it re-clears somebody a single clue already covers, so the subset search never needs it. Case 008's tester called that chain "the one real inference in the case and it is currently redundant"; case 005's called its pair the only thing ten clues bought.
The knock-on nobody would guess: raising a case's minimum covering set forces target_min_moves up with it (the validator errors if the minimum exceeds it), and target_min_moves is also the charge gate — CaseEngine.cluesToCharge. So promoting the existing tail pair in cases 005–008 does not just add an inference; it makes the game demand a fourth pull before a charge is legal, 80 minutes of a 100–120 minute clock. Three ways out:
| What it does | Cost | |
|---|---|---|
| a. Pair on the critical path | Make the joint finding out of two clues the 3-clue route already needs (an E02+E03 shape). Minimum stays 3, gate stays 3 | Cheapest. But both clues get pulled anyway, so the crossing is handed to the player rather than chosen |
| b. Promote the existing tail pair | Use the E09+E10 group that already exists. The pair-only suspect needs two clues nothing else covers | Minimum and gate both go to 4 |
| c. Decouple the gate, then do (b) | Let clues_to_charge be its own value instead of borrowing target_min_moves | One engine change, then (b) at no pacing cost |
Recommended: (c), then (b). The gate is a pacing device and the minimum is a property of the elimination graph; they were made the same number for convenience. Splitting them is a small change to one computed property plus an optional authored field, and it unblocks the strong version of the pair rule everywhere. Where a case has no tail pair to promote (003 and 004), shape (a) is free and keeps the minimum at 3 anyway.
Correction to an earlier assumption: every case needs prose edits, not just a clears entry moved. In all eight, the elimination being relocated is asserted somewhere in that clue's own arithmetic, log, finding or the case closing.
§2.1 Per-case rewire
From the elimination-graph analysis (pair-rewire-analysis.md, kept in the session scratchpad).
| Case | Pair-only suspect | Pair | Clue edited | Min after | Gate moves? |
|---|---|---|---|---|---|
| 001 Calder Cipher | — | — | — | — | Leave alone. The print margin was tuned to exclude exactly Maya and June, and a shipped lint test pins that clue's clears. It is also the teaching case |
| 002 Ivory Gate | Catalina | E04+E05 (new) | E04, E05 | 4 | Yes — expensive: both halves are independently conclusive and E04's right answer is the suspect's name |
| 003 Glass That Never Broke | Bruno | E05+E07 (new) | E05 | 3 | No |
| 004 Vanishing of Scout | Jon | E01+E04 (new) | E02, E04 | 3 | No |
| 005 Last Toast | Gavin | E09+E10 (exists) | E03 | 4 | Yes, unless the gate is decoupled |
| 006 Dead Circuit | Darius | E09+E10 (exists) | E03 | 4 | Yes, unless decoupled |
| 007 11:47 Message | Vivian | E01+E09+E10 (exists) | E03 | 4 | Yes — and its group is a triple including a deciding clue |
| 008 Four Seconds | Oscar | E09+E10 (exists) | E03 | 4 | Yes, unless decoupled |
Cheapest first: case 003. The joint argument is already written across two clues — E07 holds Bruno's rooftop frame and refuses to treat it as an alibi, and E05's ladder math names nobody in its finding or log. One clears entry out, one group in, two sentences edited, no numbers touched, minimum and gate both stay at 3.
Cleanest of the rest: case 006 — give Darius a 1:4 gear set and the group already points at him.
§3 Name the chain at the charge (engine + app)
Flow: Final Call → pick the suspect → one step before the hold: Which clues prove it? The clues read right are listed; pick two or three; Skip is always available. Then hold to charge.
Engine:
- -
accuse(_:chain:now:)takes the clue ids;RunState.chargeChainstores them. - -
ResolutiongainschainClaimed: [String],chainHolds: Bool, andchainLeaves: [String]— the suspects the chain fails to rule out, computed fromcleared(by:)so single clues and linked groups combine the one way they already do. - - The chain never gates the charge, costs nothing when wrong, and changes no score. Only clues the player read right may be named.
Case Closed prints one line under the verdict:
- - You named it: the Dust Print and the Marks in the Alley rule out everyone but Theo Vale.
- - Your chain left June Park standing.
- - You charged without naming a chain.
Why no points: the clock is the only economy in the game, and paying for a correct chain would turn the statement into a second score. The line itself is the payoff, and chargeHow keeps saying how the board stood.
Tests: engine — a chain that holds, one that leaves someone standing, a skipped chain, and a chain naming a clue read wrong (rejected). UI — the step appears, Skip works, the Case Closed line matches.
Sequencing
- 1. ~~§1 and §3 first: app and engine only, no content, no re-export.~~ Done 20 Sep (
c805cd0): the category is hidden until a clue is pulled, and the charge asks which clues prove it. 396 core tests and 12 Prime Suspect UI tests green, including two new ones. Engine carriesRunState.chargeChainandResolution.chargeChain(named / holds / leaves / skipped). - 2. ~~Decouple the charge gate from
target_min_moves.~~ Done 20 Sep: cases carry an optionalclues_to_charge, the engine defaults to three, and the validator warns when a hand-set gate asks for more pulls than the case needs. - 3. ~~Rewire two cases under §2 — 003 and 006.~~ Done 20–21 Sep. Bruno is cleared only by the ladder crossed with the attendance record; Darius only by the cage video crossed with the equipment roster. Blind solvers found both crossings unaided. A follow-up pass fixed both reading as bookkeeping: case 003's E07 now asks about the rooftop frame, and case 006's vest sighting moved to a cage where no suspect is posted.
What the pilot exposed: with the gate below a case's minimum, a coin flip at the gate paid 450 against 400 for settling it, because a miss (30 minutes) cost less than the two clues that prevent it (40). Fixed by paying a quarter more for a chain that accounts for everyone (Sarah's call, 21 Sep): the same line now pays 500. The alternative — a heavier miss penalty — was rejected because it prices out the guess without giving the ending anything, and eight playtests called the ending a tally.
- 4. Re-run the feel playtest on those two and compare: interest, deduction, and whether "the ending lands" flips.
- 5. Only then rewire the rest, and author cases 9–10 (CASE101 in the corpus) to the rule rather than retrofitting them.
Held in reserve: changing how evidence is offered
Two ideas from Sarah on 20 Sep, not specced here, kept as the next lever if the three changes above do not move the feel:
- - Unlock in series. Removes the selection game: there is nothing to buy, and the Case Closed cheapest-path panel has no meaning. It would fix the tell by deleting the thing that has a tell.
- - Offer one of two each pull. The strongest version of that family. It keeps a real cost (the branch not taken) and hides the board, so the tell goes. It needs authored pairing per case, caps a case at about five decisions however many clues it holds, and makes "two clues would have done it" unfair when the case never offered those two together. Better attempted after §2, when each fork is a choice between two clues that both mean something.
Final review
Pros. Two of the three changes are code-only and reversible, so the expensive half is deferred until a playtest says the cheap half moved the needle. The pair rule uses a mechanism the engine, the validator and the content already have, so no new authored field appears. The chain step is computed from the elimination graph, so it costs no authoring across any case, including ones not yet written.
Cons. Hiding the category makes a dead clue a real risk rather than an obvious skip, which is the intent but reads as punitive while half the board still gives nothing — §2 has to follow §1 or the game gets meaner without getting better. The chain step adds a beat to a commit flow that is deliberately two taps today. Rewiring clears is the kind of edit that quietly breaks a closing paragraph.
Edge cases. A player who read only two clues right cannot name a three-clue chain — skip covers it. A case whose pair-only suspect is cleared by a clue the player never pulled still resolves; the chain simply leaves someone standing. Cases 001 and 002 are the on-ramp: 001 stays simple by design, and 002 is the first candidate for a pair after 005 and 008.
Adjustments worth considering. Dead clues could pay something other than an elimination — a discount on the next pull, or a narrowed window — which would make §1 land better. That is a fourth change, not folded in here.
Confidence: 72. The three changes are well-grounded in what eight playtests said, and the two cheap ones are safe. The uncertainty is whether crossing two findings actually feels like deduction to a sixteen-year-old, or just like a longer subtraction — which is exactly what step 3 of the sequencing is for.