Peregrine after the migration

One decision, one writing desk, three loose ends. The work that remains is three different activities, so it is planned as three tracks rather than one list.

2026-08-22 · corpus guides/peregrine/ — 327 entries · app ~/Developer/Peregrine · follows 2026-08-15-peregrine-machine-target.md

What the migration left standing

Seven outputs project from one guide, and all three game guides verify clean.

FileRowsOwner
vigilance-bank.json69corpus
cons.json19 + Innocentcorpus
undercover.json6 + 4corpus
playbook.json17corpus
heckler.json150corpus
strategy.json10corpus
bestiary.json22 + bosscorpus
generated-*.json281to_app_bank.py

PeregrineCopy.swift stays in Swift on purpose: centralised UI chrome with a Placeholder enum a test asserts against, and the guide's own rule is that entries carry content and never behaviour.

Track A — the held decision

Target Practice: 16 claims become 45

The only task carried over, and it is not a migration. The con count is identical at nine. What the corpus adds is twenty-nine more clean claims.

cleanconratio
app, shipped790.8 : 1
corpus3694 : 1

The contract requires 4:1 and the shipped bank breaks it. Below one in five, firing at everything is the winning strategy, so the mini game currently trains the opposite of the restraint it exists to teach.

It also nearly triples the bank and changes how a run feels, which is why it sits here rather than in Track C. Sarah decides; nothing else in this plan depends on it. Once decided, the implementation follows the pattern already used five times.

Track B — the writing desk

Authored content the corpus can now count, and could not while it was a Swift literal. None of it needs a developer.

B1 · Go Undercover is six rounds where it wants seventeen

The bank deals five rounds a run and holds six, so a single sitting nearly exhausts it. The honest target is seventeen, not nineteen — Fumbler and Doubler are keystroke slips with no reasoning to voice, the same reason they carry no Playbook entry.

Eleven lineup-eligible cons have no round:

reverse · distort · scope · irrel · footnote · early
wrench · invert · splice · dangle · agree

Each round needs a claim, a con line, two honest critiques and a counter. The two-critique rule is load-bearing: with one, the true critique is a lone odd one out and the round reads itself.

Worth doing first. Teach the exporter the denominator is seventeen. It currently reports "13 cons have no undercover round", counting two that never should, so it overstates the gap every time it prints.

B2 · Five heckler pools can go silent

recentQueueLimit is 25 and persists across sessions, so a pool smaller than that can sit entirely inside the ledger. fire returns nil rather than erroring, so the symptom is the heckler simply going quiet.

poollineswhen it is heard
payoff-exit3the payoff, §6
huh5first correct after 4+ misses, §3.2
post-commit6after the commit
respect7earned by a hot streak
hesitation812s without a commit

The spec's own note says these are seed content at roughly half target size. Doubling the five smallest is the shortest path out. Two constraints any new line must pass, both now enforced at the boundary: the Ceiling Rule (§2.2) — mock the play, never the player — and no duplicate line across pools, since the anti-repeat ledger keys on the line itself and one line in two pools mutes in both.

B3 · The register pass on the wager mechanisms

102 distractors now carry an authored mechanism that renders at the reveal, beneath the con line. The corpus text was written analytically ("A student picks this because…") and the game is 1940s noir with a banned vocabulary. The lines have never been read in place.

This needs to be seen before it is edited, which makes it Sarah's first, and it is the only item in this plan with a hard ordering claim.

Track C — engineering follow-ons

C1 · The Lineup's 17 analysed distractors have nowhere to go

Seventeen Lineup distractors carry mechanism / drill_rx / error_mode and are discarded at the border, printed on every export. The Wager had an obvious home — the moment the player picks a distractor. The Lineup's reveal narrates the suspect's reasoning instead and never names a chosen option, so there is no equivalent moment.

A design question before it is a code change. Options: attach the mechanism to the segment at breakIdx, where the tell lands; surface it only in the post-round debrief; or decide the Lineup does not want it and drop the fields rather than carry a permanent export warning.

C2 · The other half of a con rename

The exporter blocks a rename it cannot see art for and prints the five surfaces that do not follow. Two are reachable only from inside the app: VigilanceResult.conID and ConProgress are SwiftData rows on the learner's device, and ProfileArchive carries conID in four row types, so exported archives stop resolving on import.

A migration keyed on the corpus's previous_ids would close it. Do this only when a rename is actually wanted — building it now is speculative, and the guard already prevents the silent failure. Recorded so the gap is not rediscovered.

C3 · The blueprint is stale

Three contract changes a reader would now get wrong: an output can span several families and classes into one file; a target can be an SPM package's Resources and the app bundle rather than Data/; and one directory's contents are owned by two generators, split by file.

Run /cr-blueprint machine-export.

Sequencing

One hard dependency exists in this plan: B3 before any editing of the mechanism text, because the register question cannot be answered from the JSON.

Otherwise A is Sarah's alone, B is writing, C is code. Inside C: C3 first (cheap, and the blueprint actively misleads), then C1 (a standing export warning), then C2 only on demand.

Where this plan is inadequate

Track B has no estimate and should not have one

Eleven undercover rounds and roughly thirty heckler lines are authoring tasks. Guessing at hours would turn a writing desk into a burndown.

C1 is stated as a question, not a design

I have not read LineupGameView's reveal closely enough to say whether the breakIdx segment can carry a second line without crowding the narration. That reading is the first step, not the implementation.

Track A's content is unverified in play

The 45 claims were migrated and authored, and nobody has fired at a 4:1 stream. The ratio is right on paper. Whether 36 clean claims is tedious rather than tense is a thing only playing will answer, which is exactly why it is parked.

Confidence: 84

High on Tracks A and C, which are well-scoped and rest on facts the exporter now prints. Lower on B, where the work is real and the finish line is editorial: "enough heckler lines" has no test, and doubling the five smallest pools is drawn from the spec's own note rather than from anything measured in play.