Figures in Peregrine

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.

8 August 2026 · proposed, not started · side track · source: docs/pressraven-figures-spec.md

The decision this rests on

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.

What is unreachable today

Observed across the six-test calibration corpus. Every authored item in every Peregrine bank is pure text, so all of this reads zero.

CodeThe mistakeObservedAuthored
M-FIG-MISREADEyeballs the graph — wrong series, wrong endpoint, a gap read as a value810
R-FIG-MISREADThe same, on a reading passage's chart660
R-FIG-UNSUPPORTEDAnswers from knowledge rather than from what the graphic shows310
W-FIG-INVERTDescribes the graphic's hierarchy backwards170
W-FIG-MISREADStates a value the graphic does not show30
Total distractor instances1980
S-FIGURE-ONLYThe counter-move: require the answer to be verifiable from the graphic alone1162

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.

Why data rather than images

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.

The mirrored assertion

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:

The cost, stated plainly

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.

What already exists

No new category, no new template, no new asset plumbing. One new entry class usage and one new field.

The figure record

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]
  ]
}
KindCarriesServes
tablecolumns, rowsThe most common observed stem shape — "based on the table…"
baraxes, seriesCategorical comparison; the misread codes
lineaxes, seriesTrend and endpoint reading
scatteraxes, pointsRelation reading; problem-solving-data
hierarchynodes (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.

The validation contract

Extends tools/validate-peregrine.py. These are the checks that make figures worth doing, and none is expressible in schema.

The last two are the ones that will actually catch mistakes in authoring.

Which mini games

Mini gameFiguresNote
The WageryesStrongest 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 LineupyesBest 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 RoominheritedBoth read the wager bank; they get figures when it does, with no work of their own
The MakelaterFigure panels as evidence cards is a strong fit, after the first three ship
ForensicslaterFour figures as the four "prints" — interesting and untested
Snap Call · Shakedownn/aPlayer's own captured library; already supports images
Target PracticenoExcluded by decision. Single-line claims at speed
Cold CasenoProse editing

Phasing

Phase 1 — the thinnest end-to-end slice

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.

Phase 2 — the second kind, and print

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.

Phase 3 — into the game

Peregrine renders figures in noir; the Wager stake-timing question gets resolved by playing it. Gated on the round trip existing.

Phase 4 — scale

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.

Open questions

Final review

What 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.

Cons, stated plainly

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.