Peregrine · build report

Playtest build, unlock inventory,
verb check, measurements

Everything on the 2026-09-15 list that needed no narrative input. The playtest build is made and verified; the unlock is inventoried and untouched; the verb check is drafted for your ruling; the corpus rename is staged, not written.

2026-09-16 · app f133736 · corpus 3acb581
Prime Suspect's sequential unlock is unchanged. Nothing under PeregrineCore/…/Resources/cases was edited. No narrative content, placeholder documents or story flags were added.

1 Playtest build

The Lineup and Prime Suspect, and nothing else. Prime Suspect keeps b3fc666's sequential case unlock exactly as it stands. No game content changed.

How a build becomes a playtest build

A new Playtest build configuration (Release-type) defines a PLAYTEST compilation condition, and a new Peregrine-Playtest scheme runs, profiles and archives against it. A -playtest launch argument flips any build into the same roster, which is how UI tests and a local run reach it without switching schemes.

# make it
xcodegen generate
xcodebuild archive -scheme Peregrine-Playtest -configuration Playtest

# see it without switching schemes
xcrun simctl launch <device> com.lochlustra.Peregrine -playtest
Files changed
FileChange
Peregrine/PlaytestRoster.swiftnew One roster gate both surfaces ask, so they cannot drift apart.
Peregrine/Views/LaunchDeckView.swiftDeck cards filtered at source, so the ordering, the hero slot and every accessibility card count read the same list.
Peregrine/Views/GamesView.swiftEach of the 12 game stations wrapped in its roster id. The two dossier rows are not games and stay.
project.ymlThe Playtest config and the Peregrine-Playtest scheme.

Verified

Two judgment calls worth overturning if you disagree

The Rematch is gated out. It calls itself a game, owns its own verb (turn), and appears in the launch deck's hero slot and the Training Ground marquee once the player has a nemesis. Playing The Lineup earns one, so an ungated playtest would grow a third game partway through the session in the most prominent slot on the screen. It is gated under the id rematch; delete one line in PlaytestRoster.underTest to put it back.

The bundle id is unchanged. A separate id would give the tester a separate app, but the SwiftData store lives in the app group group.com.lochlustra.Peregrine, and a new bundle id needs that group re-provisioned under automatic signing. The playtest build therefore replaces a normal build on the same device. Only the display name differs (Peregrine Playtest).

Still open from the meeting

Who runs the session and what it records. The build is ready either way; nothing in it assumes an answer.

2 Unlock inventory — nothing changed

Every piece of unlock logic and every test that asserts unlock order. Read only; no file below was edited.

The logic
FileLinesWhat it holds
PeregrineCore/…/PrimeSuspect/
CaseLadder.swift
66–93
102–105
108–110
The whole rule. rungs(cases:runs:) walks cases in caseID number order carrying a previousClosed flag: a case with no solved run and an unclosed predecessor is .locked. Status.isOpen is self != .locked. next(after:in:) is list order. number(of:) parses 3 from "CASE003"; a case without digits sorts last. Pure — cases and run summaries in, rungs out.
PeregrineCore/…/PrimeSuspect/
CaseLadder.swift
96–99 rungs(in: ModelContext?) — the bundled library read against a profile's store. The only place the rule meets persistence.
PeregrineCore/…/PrimeSuspect/
CaseRun.swift
136–203 No unlock logic. It supplies the four facts the ladder reads: moveCount (opened clues), minutesSpent, isResolved, and the accusation history. isOpened at line 131 is an evidence thread's state, unrelated to case unlock.
PeregrineCore/…/Models/
PrimeSuspectRun.swift
19–63 Not on your list, but the ladder's actual input. RunSummary.init(_ run:) reads caseID, solved, isLive (completedAt == nil), minutesSpent, moveCount.
Peregrine/Views/PrimeSuspect/
PrimeSuspectView.swift
18–20
27, 41
64–70
Loads the rungs on appear and again whenever a case closes back to the list. Holds the doc comment naming the sequential unlock.
Peregrine/Views/PrimeSuspect/
CaseListView.swift
48–51
75–77
87, 91
104–105
132–133
The only place the lock is rendered. locked = !rung.status.isOpen guards the tap, dims the row to 0.62, disables the button, swaps the footer to "Close case NN to open this file", shows the 🔒 locked tag, and speaks "Locked. Close case N to open it." A locked row still shows its title and premise.
Peregrine/Views/PrimeSuspect/
CaseRunView.swift
33, 58–60
133–134
144–151
refreshNextRung() re-reads the ladder from the store once Case Closed is on screen, so the unlock the charge just earned is already counted, and offers the next case only when next.status.isOpen.
Peregrine/Views/PrimeSuspect/
CaseClosedView.swift
23–24
47–52
Not on your list. Renders the handoff: the ps-next-case button appears only when CaseRunView passed a next rung.
Tests that assert unlock order
TestWhat it pinsSurvives the replacement?
PrimeSuspectLadderTests
.testOnlyTheFirstCaseIsOpen­OnAFreshProfile
Three cases, no runs → [.new, .locked, .locked].rewrite Under the replacement this becomes [.new, .locked, .locked] pre-burn and all-open post-burn.
…testClosingACase­OpensTheNextOneOnlyClosing 1 opens 2 and leaves 3 locked; best score is (deadline − spent) × 10.rewrite The "next one only" clause is exactly what goes.
…testInProgressNeedsAMove­AndKeepsTheNextCaseLockedA live run with no moves is still .new; one pull makes it .inProgress; in progress does not open the next case.split The .new / .inProgress half is about run state and should survive untouched. Only the trailing lock assertion is order.
…testBestScoreIsTheBestSolvedRun­AndAReplayKeepsTheCaseClosedBest across solved runs; a replay in progress does not reopen the case. Asserts rungs[1].status == .new.split Scoring half survives; the one .new assertion is order.
…testAClosePastTheDeadline­ScoresZeroAndStillOpensTheNextCaseScore floors at 0 and a late close still unlocks.split The floor survives; "still opens the next" is order.
…testNextFollowsListOrdernext(after:) is list order; nil past the end.keep Pure list navigation. Independent of what is open.
…testResolvingThroughTheSession­OpensTheNextBundledCaseAgainst a real store: resolving case 1 flips case 2 from .locked to .new. Skips when the bundle holds one case.rewrite The only store-backed unlock test. Replacement needs the same shape against seasonState.
PrimeSuspectUITests
.testClosingACaseOpensTheNext
On a fresh profile ps-case-CASE002 is not enabled; closing case 1 offers ps-next-case; back on the list case 2 is enabled and case 1 reads "Closed".rewrite The two isEnabled assertions at lines 247 and 275 are the unlock. The Case Closed handoff is worth keeping.
…testLaterCasesUnlockAndShow­TheirOnFileTablesCloses cases 1 and 2 in sequence, asserting each isEnabled on the way, then that case 3 is enabled, then that its first clue draws its on-file table.split The on-file table is the real subject and has nothing to do with unlock. Post-replacement this test can open case 3 directly, which makes it shorter and faster.
Answering the meeting's build-detail question

"Which db85da4 tests check unlock order, and which only check case content?" — Of the nine above, four are purely unlock order and must be rewritten, four mix an order assertion into a test whose real subject is something else (run state, scoring, the on-file table) and only need the order clause lifted out, and one — testNextFollowsListOrder — is untouched by the change.

The replacement needs a seasonState flag that does not exist yet: case 1 open before the burn, cases 2–10 after it. Nothing here was edited, and none of it was deleted.

3 Verb check — draft, for your ruling

The gate is "the concept names a one-word verb that no kept game owns." The binding clause behind it is docs/spec/verb-registry.md §2.1: no two games in the same section share a primary verb, and a game that cannot name a free verb is a reskin of one that already exists.

What the kept games own
Kept gameVerbSectionWhat the hands do
Prime SuspectpinMathSet a suspect's read on the board. Terminal: charge, press-and-hold.
The Lineupcall itR&W + MathClean or con, at a line of your choosing. Terminal: the mugshot pick.
Target Practiceaim-fireR&WPress to aim, release to fire at one of two swaying targets.
The Wagerpush a chipR&W + MathDrag-forward commit that prices confidence without resolving it.

Clear the Room — drafted verb: strike

It already holds this row in the registry, and it is one word. Against the four kept games it collides with none of them on the letter of the gate.

Against each kept verb

The one conflict worth your eyes

Clear the Room and Target Practice are adjacent in feel and share a section. Both are reflex-tier, both decay a per-second meter, both reward speed on a wrong-thing-spotted. Target Practice drains 90 → 10 over the claim's reading budget with a ×3 streak multiplier; Clear the Room drains 80 → 20 over a nine-second window with a +120 completion bonus.

What separates them, and I think it holds: the object being judged is different. Target Practice judges a claim standing alone — is this reasoning sound. Clear the Room judges an answer against a stem — is this the wrong answer to that question. And the hands genuinely differ: a two-stage press-and-release at a moving target chosen from two, against a single tap on one of four in a fixed floor plan where spatial memory is part of the game.

What does not separate them: nothing in the scoring. If you want the gap widened rather than argued, the cheapest move is to take the streak out of one of them. Target Practice's ×3 is a true streak; Clear the Room's +120 is a completion bonus, which is closer to a streak than it should be. Your call.

Before He's Back — three candidates, one recommendation

The meeting named the skill: "the skill is knowing what to skip." A registry row names something narrower — what the hands do on a live call site that stops field 3. Those are not the same sentence, and the gap is where the draft has to be made.

Candidates
VerbThe case for itThe failure mode
open
recommended
Names the hand motion. Live on the first frame — the desk accepts it immediately, so field 3 stops honestly. Owned by no game. The skill lives in which file you open and how many you leave shut, which is opportunity cost stated as a hand. It is Prime Suspect's priced clue-pull promoted to a primary verb. §2.1 could read two document games under a clock as one game. The counter-argument is that the registry already says pulling is a resource spend and not Prime Suspect's verb — so Before He's Back would be making a game out of the thing Prime Suspect prices and moves past. That is either elegant or a reskin, and it is the call only you can make.
skip It is the sentence the meeting actually wrote, and it names the thing the game is about. A negative act has no call site. If the verb is "do nothing to this file," there is no first frame that accepts it and field 3 never stops. Recommendation: keep skip in the reskin line, where it belongs, and off the verb row.
allocate The verb the record already owns as Casing the Room — "crack or back out, one clock." That shape survives the rename intact, and it collides with no kept game, so it clears the gate with no work at all. Choosing it is a statement that Before He's Back is the renamed game at order 7 rather than a new one. Which may be exactly true. It is worth deciding on purpose rather than by default.
Conflict to name out loud: The Wager

Any verb for Before He's Back that means rate how much this item is worth is push-a-chip with paper instead of chips, and §2.1 bars it. The Wager is already the triage game — "skim a run of questions, bet each fast."

The distinction that keeps Before He's Back clear of it: The Wager makes you price every item and then answer every item. Before He's Back's player never touches most of the desk. The Wager's currency is calibration; Before He's Back's is refusal. Keep the verb on the refusal and the collision does not open. Point the verb at valuation and it does.

One design check the verb draft turned up

I considered toss — sweeping a file off the desk — and dropped it. The meeting settled that "files never rearrange mid-round" and that the flags stay visible on files that do not move. A verb that removes a file from the desk contradicts that rule directly. Worth recording, since toss is the obvious first guess for a game about discarding.

Nothing here is decided. Both games stay exactly as they are until you rule.

4 Corpus rename — staged, not written

Routed through /cr-corpus, which never writes to the corpus. Guide resolved as peregrine at tier 1 (project_path: ~/Developer/Peregrine).

Three items, all session notes against the guide-level mini_games record casing-the-room. A mini-game record is guide data rather than an entry, so no entry and no question was added:

  1. label → "Before He's Back".
  2. reskin → "Ten files on his desk, three minutes. Read what you can and get out."
  3. Drop consumes: "the-wager". It launches with real documents, its unit is a document rather than a question, and the host/consume pattern recorded on 2026-08-08 no longer describes it. bank_target stays 0; the content waits on the solved case and the season record, so this records the unhooking and nothing behind it.
One thing I left for you

I renamed the label and deliberately left the id as casing-the-room. Re-keying it would move order: 7 in group_order and break the app's casingTheRoom game key, telemetry and familiarity counters. The meeting asked for a rename, not a re-keying. The note says so; decide the id separately.

Batch: PressRaven-Corpus/corpus-inbox/2026-09-16-014650-peregrine.md — open it with File → Import Session Return… in Press Raven. Nothing reaches the corpus until you commit it in the staging view.

5 Measurements — read only

(a) Word counts, smallest text unit per game

Counted off the shipped banks, not off the spec. Words are whitespace-and-punctuation tokens; numerals and hyphenated forms count as one.

Measured from the shipped resources
GameSmallest text unit nminmedianp90max
The Lineupone reasoning line of the monologue5641122030
  · whole roundpassage + stem + choices + monologue11358116243285
Target Practiceone claim455101616
Go Undercoverone line (con line or honest line)757121618
  · whole roundclaim + every line on the table2534506064
Red Stringone sentence104182022
  · whole exhibitthe strung paragraph281838585
Clear the Roomone answer choice (one strike)180031120
  · whole roompassage + stem + four hidden answers451345102110
The Wagerone question, as skimmed451345102110
Casing the Roomone question (crack or back out)451345102110
Prime Suspectone clue card, as answered (account + prompt + four choices)654887110129
  · account onlythe evidence account6531516977
  · whole casepremise + brief + every clue841891810371161

The Blotter

It cannot be measured, and the id is an inference

No game called The Blotter exists in the corpus roster. The closest record is fault-in-the-record (order 20, Fault in the Record) — the Writing container, whose reskin is "a record is being reconstructed from a corrupted transcript; fragments come up one at a time." Record, transcript, grammar, a stream of fragments: that matches the 2026-09-14 line "the stream is a transcript, and around line forty it stops being about grammar."

I am reporting that as a likely match and not a recorded equivalence — the corpus nowhere states it. And it is build_status: proposed with no bank_target and no bank, so there is no content to count. The Blotter line's word ceiling cannot be measured today under any id.

The nearest usable proxy is The Lineup's reasoning line — the only built line-by-line stream on the slate. It runs 1–30 words, median 12, p90 20. If The Blotter's unit is a line, that is the number a ceiling should be set against.

Answering the 2026-09-14 rubric question directly

"Record the word ceiling of the smallest arc-carrier unit (Blotter line, Prime Suspect clue card)."

Prime Suspect clue card: 110 words at p90, 129 hard maximum, across all 65 clues in the eight shipped cases. Blotter line: unmeasurable; the proxy says 20 at p90, 30 hard maximum.

(b) Points per correct answer, every built game

What one correct act pays
GameWhat one correct act paysRangeShape
The Lineup Two separate awards per suspect. Your own straight read: +15 flat. The call: 80 × multiplier, ×4 on the tell, −1 per line late, floor ×1, 0 before the tell. A right con call with the wrong face pays a flat +40, and only on or after the tell. 15
80–320
Speed-scaled
The Wager The bet pays its locked win: Lock +100, Lean +50, Coin +20, each scaled at push by round(win × meter/100). A circled item answered later pays +10. Losses are flat: −100 / −25 / 0. 10–100 Wins scale, losses flat
Target Practice Hit value 90 → 10 decaying over the claim's reading budget, × streak multiplier capped at 3. Sparing an honest line pays the same as downing a con. 10–270 Speed × streak
Clear the Room Per strike: 80 → 20 over a nine-second window. All three wrong answers struck: +120. Waking the live one: −15. 20–80
+120
Speed + completion
Casing the Room Flat 100 per mark cracked, by design — "the easy ones are worth exactly as much as the hard ones." A wrong crack burns 90 seconds and costs no points. 100 Flat
Prime Suspect Nothing per correct answer. The case pays 10 × minutes left when the charge sticks. A clue costs 20 minutes (= 200 points); a wrong charge costs 30 (= 300). Misreads and second looks cost 0. Case 001's 80-minute deadline caps a run at 800. 0–800
per case
Time is the only currency
Beat the Shakedown Coin drains 100 → 5 over the question's reading budget and is awarded on a right answer. The pattern call adds +10; calling it before the choices deal adds +20. 5–130 Speed-scaled
The Make Payout 150 → 40 across the stages, plus +40 flat for naming the move. Wrong man: −100. 40–190 Evidence-scaled
Forensics Three parts per print: job +40, plant +40 (or −15), solve +20. −15 to 100 Flat, three-part
Red String No award. Starts at 120 coins; a wrong slot costs 40, a wrong word 20. 120 → 0 Subtractive budget
Go Undercover unscored nightPoints: 0. The cover meter runs 100 → 0 as pressure and banks nothing. — Unscored
Kingpin Showdown unscored nightPoints: 0. Counts kills and fights. — Unscored
The commensurability finding

The slate spans 0 to 320 points for one correct act, and "100" means six different things:

Three structural mismatches sit under that, and they matter more than the numbers:

  1. Two games pay nothing at all (Go Undercover, Kingpin Showdown) and a third is subtractive rather than additive (Red String). A currency the player carries between rooms cannot skip three rooms.
  2. Prime Suspect has no per-answer value to normalize. It pays for time, not for correctness, so making it commensurate is not a tuning pass — it is a decision about whether the case keeps time as its only currency.
  3. Only one game is deliberately flat (Casing the Room), and it is flat on purpose, as the opportunity-cost lesson.

One thing already agrees. Before He's Back's proposed 100 points per file matches Casing the Room's flat 100 per mark exactly — which follows, since it is the same record renamed. The rename carries the point value forward untouched, so that number needs no decision.