A chart, table or diagram a question makes you read. About a third of the remaining coverage gap depends on them, and none of it is reachable today.
A figure is data, not a picture. The obvious build is to author PNGs. That is the wrong call for one reason that outweighs the rest: a picture cannot be validated.
Scope, settled 8 August: figures are in for The Wager, The Lineup and beyond; out for Target Practice, which flicks single-line claims past the player at speed and has no room for a chart.
Observed across the six-test calibration corpus. Every authored item in every Peregrine bank is pure text, so all of this reads zero.
| Code | The mistake | Observed | Authored |
|---|---|---|---|
M-FIG-MISREAD | Eyeballs the graph — wrong series, wrong endpoint, a gap read as a value | 81 | 0 |
R-FIG-MISREAD | The same, on a reading passage's chart | 66 | 0 |
R-FIG-UNSUPPORTED | Answers from knowledge rather than from what the graphic shows | 31 | 0 |
W-FIG-INVERT | Describes the graphic's hierarchy backwards | 17 | 0 |
W-FIG-MISREAD | States a value the graphic does not show | 3 | 0 |
| Total distractor instances | 198 | 0 | |
S-FIGURE-ONLY | The counter-move: require the answer to be verifiable from the graphic alone | 116 | 2 |
Figure-dependent question types sit alongside: figure-read (20), figure-integrate (12) and figure-accuracy (8), all at zero authored, plus data-read (36, one authored). 19 of the 54 calibration passages carry a figure; 76 of 924 questions hang a trap on one.
The worth of the trap catalog is that a distractor names a real cognition. R-FIG-MISREAD means the value is genuinely in the chart, just in the wrong series or at the wrong endpoint. If the chart is data, a script asserts exactly that. If it is a PNG, nobody can check, and within twenty items the bank quietly becomes plausible-looking charts with invented numbers.
R-FIG-UNSUPPORTED needs the opposite check: the distractor must sound true and not be derivable from the figure. Two traps, two assertions pointing in opposite directions — both machine-checkable if the figure is data, neither checkable if it is a picture. This is the single strongest argument in the spec.
Three supporting reasons, none decisive alone:
outputs is print-only today. A data figure typesets; a screenshot-resolution PNG does not.A renderer has to be written three times — SwiftUI for the document, HTML/CSS for the print exporter, and Peregrine's game view. A PNG pipeline would be roughly a day and this is not. The tradeoff is deliberate: figures that cannot be checked are worse than no figures, because they look finished.
Raster stays supported for the genuine photograph or scan. The rule: if it has values a distractor could misread, it must be data. If it is atmosphere or evidence-as-object, an image is fine.
No new category, no new template, no new asset plumbing. One new entry class usage and one new field.
visuals is already an envelope field on every entry, handled in EnvelopeViews.swift. Its shape is a wanted slot — {asset, caption, intent, status}, where status: "wanted" and a null asset means "an image belongs here and does not exist yet."guides/<guide>/assets/visuals/, with drop-to-fill wired in the app.asset entry class already exists in the game category: "the record about a picture or scene."group_order + group_labels, for the con taxonomy). A figures collection is the same pattern.Question.hasFigure and imageData. What is missing is an authored figure, not figure display as such.Figures are entries, not a sidecar collection, so they inherit ids, status lifecycle, confidence, provenance and the navigator for free. They live at guides/peregrine/entries/fig-*.json as class: asset, family: figures, surfaced by a roster group exactly as the con taxonomy is.
{
"id": "fig-media-trust",
"class": "asset",
"family": "figures",
"title": "Trust in news media, 2004–2016",
"figure_kind": "table",
"caption": "Percentage expressing a great deal or fair amount of trust.",
"description": "A four-column table of survey years against three age bands...",
"source_note": "Invented for Peregrine. Not real survey data.",
"columns": ["Year", "18–29", "30–49", "50+"],
"rows": [
["2004", 44, 51, 58],
["2008", 39, 47, 55],
["2012", 31, 42, 50],
["2016", 26, 36, 47]
]
}
| Kind | Carries | Serves |
|---|---|---|
table | columns, rows | The most common observed stem shape — "based on the table…" |
bar | axes, series | Categorical comparison; the misread codes |
line | axes, series | Trend and endpoint reading |
scatter | axes, points | Relation reading; problem-solving-data |
hierarchy | nodes (nested) | W-FIG-INVERT, which is specifically about nesting read backwards and has no other home |
description is mandatory and load-bearing — accessibility text, print fallback, and the thing a reviewer reads when judging whether the figure is fair. source_note is mandatory too: every figure is invented for Peregrine, and saying so stops a plausible chart being mistaken later for real data.
An item references one with a single figure_id, declared in schema-extensions.json and gated to the families that support figures. One figure may serve several items — that is the point of it being an entry.
Extends tools/validate-peregrine.py. These are the checks that make figures worth doing, and none is expressible in schema.
figure_id resolves to a figure entry in the same guidedescription and source_notefigure_kind matches the fields present — a bar without series failsS-FIGURE-ONLY made enforceableW-FIG-INVERT requires figure_kind: "hierarchy" — the trap is about nesting, so it cannot be set on a bar chartstatus: "seed"The last two are the ones that will actually catch mistakes in authoring.
| Mini game | Figures | Note |
|---|---|---|
| The Wager | yes | Strongest first host; already the general item bank. Open: the stake phase is timed and decays — is skimming a chart under a decaying stake fair? |
| The Lineup | yes | Best fit in the game. A suspect narrating "the 2012 bar is the tallest, so…" is a misread dramatised. Open: screen room for figure + narration + four options |
| Clear the Room · Casing the Room | inherited | Both read the wager bank; they get figures when it does, with no work of their own |
| The Make | later | Figure panels as evidence cards is a strong fit, after the first three ship |
| Forensics | later | Four figures as the four "prints" — interesting and untested |
| Snap Call · Shakedown | n/a | Player's own captured library; already supports images |
| Target Practice | no | Excluded by decision. Single-line claims at speed |
| Cold Case | no | Prose editing |
Roster group, schema fields, figure_id on the two hosts. Author two table figures and three Wager items — one covering a misread, one an unsupported claim, so both directions of the validator get exercised. Extend the validator. Render tables in the document view.
Success: three items read correctly in the app, and deliberately breaking a distractor's value makes the validator fail.
Add bar and line; inline SVG in the print exporter. Author a Lineup round whose suspect misreads a chart. Success: ⌘E produces a printable figure item.
Peregrine renders figures in noir; the Wager stake-timing question gets resolved by playing it. Gated on the round trip existing.
Author against the ranked gap. Add hierarchy when the 17 inversion instances are worth reaching; scatter last, lowest volume.
Phases 1–2 are not gated on anything and can run alongside other work. Only Phase 3 needs the round trip.
data-read belong here? 36 observed, 1 authored — and that one got by without a figure, which is worth understanding before authoring 35 moreWhat this gets right. It reuses everything that exists — entry class, roster groups, asset directory, visuals — and adds one field and one entry family. The validation contract is the real deliverable: two mirrored assertions that turn a category of item nobody could check into one a script can. Phasing puts the cheapest renderable kind first and gates only the app work on the round trip.
Pros. Figures become authorable by Claude Code in a session. One record serves review, print and play. The bank cannot silently fill with invented numbers.
Three renderers instead of zero. The five-kind vocabulary will be wrong somewhere and will need a sixth. And Phase 1 delivers three items against a 198-instance gap — the payoff is almost entirely in later phases, so if this stalls after Phase 1 it will have cost more than it returned.
Where it may be inadequate. If the Wager's timing question resolves as "figures don't fit a timed skim," the strongest host disappears and the Lineup carries it alone. If noir chart treatment proves expensive, Phase 3 could outweigh Phases 1–2 combined. Neither is knowable without building the slice.
Adjustments worth considering. Phase 1 could ship to print only and skip the SwiftUI renderer, proving the data model on the cheapest surface. And if the Lineup's screen room is the binding constraint, doing the Lineup first would surface it before any Wager work is spent.
Confidence: 80/100. High on the data-not-picture call and the validation contract, which rest on the observed trap definitions and existing corpus machinery. Deductions for the two open design questions inside the games themselves — Wager timing and Lineup room, genuinely unknown until played — and for the renderer cost, which is flagged but not estimated.