Peregrine v4 — Non-Game Screens: Build Plan
Date: 2026-09-19
Source: docs/Peregrine-Plot/2026-09-19 - Peregrine v4 - Non-Game Screens Handoff Spec.md (cited as §n)
Mockups on disk: docs/Peregrine-Plot/plot ui mockups/peregrine-case-board-v4.html, peregrine-get-out-of-town-v4.html
Status (2026-09-19, overnight):
- Built: Slices 1, 3, 5 and 6, plus a partial Slice 4.
- Not built: Slice 2 (the renumber waits on §D1 and §D3), and Slices 7–9 (they need the story documents).
- Proposed: the beat schedule is in
.claude/plans/peregrine-v4-beat-schedule.md. - Gating: every v4 screen is behind
-seasonV4.-seasonState act1|burn|act2b|clearpreviews a mocked state in debug builds without writing anything.
A. What exists today
| Spec concept | In the app now | Gap |
|---|---|---|
| Season act enum, flags (§4) | Nothing. ActRecord / Acts (NarrativeState.swift) are the con-narrative acts (Pattern, Crew), a different system shown on Case Board caseFileSection | New model; naming collision to settle (§D4) |
| Credential number (§4, §9.3) | CredentialBadge has face/name/role/stamp, no number, no status | Number + three states |
| Session-open routing (§5) | PeregrineRootView.init picks a tab from launch args only; default Play. No session notion; ProfileManager.activate is the only open point | Router + session ordinal |
| Docket (§6) | Copy only: You row "The Docket" → CasesClosedView | Model, verbs, countdown, UI |
| Documents / file room (§7, §9.2) | None | Ledger, drawers, reader |
| Case 1 closed | PrimeSuspectRun.solved for CaseLibrary.openingCaseID ("CASE001" = The Calder Cipher) | Agency case must become Halloran's (Slice 2) |
| Agency play-day gate (§3) | None. The committed play gates (fde786b) are in-case gates only | New |
| Theme tokens (§2) | All eleven colours exist by value under other names (PG.ink, Desk.ink, PG.bg, PG.surface, Desk.chrome, PG.clock, Desk.lamp, PG.clean, PG.con, PG.alert, PG.taupe/PG.inkSoft) | None. Desk.gold (#D6A62C) is not Stake Gold. |
B. Slices
Each slice lands on main in working increments, except Slice 2 (see its note).
Slice 1 — Season state + session-open routing ✅ this session
PeregrineCore/Season/:SeasonAct,Credential(+ status, reissue),SeasonFlags(the seven),DocketVerb(the locked thirteen),DocketEntry,SeasonDocument(ledger row),BoardLine,SeasonStatewith pure transitions,SeasonRouter,SeasonStore(JSON in the profile's defaults suite, keyseason.state).- The credential number is issued when a profile is created, or lazily on first open for existing profiles, together with the Day 1 Docket line.
ProfileManager.activateopens a session: it bumps the session ordinal, reconciles case-1 closure fromPrimeSuspectRun, delivers the burn if due, and hands the destination toPeregrineRootView.CaseRunViewrecords the agency case's closure (Day 8 line) the moment it closes.- The burn and the post-burn default route are behind
-seasonV4until Slices 2 and 4 land. Without the flag, closing today's CASE001 (Calder) would burn a student into a file room that doesn't exist. State still accrues without the flag. The only thing the flag withholds is the burn transition and the route change. - Tests: pure
SeasonState/SeasonRouter/SeasonStoresuites in PeregrineCoreTests (no model container, to stay clear of the SIGTRAP class).
Slice 2 — Case renumber: Halloran's books becomes case 01 ⚠️ branch
Wide data migration (corpus, exported files, saved progress, tests), so it runs on a branch, -u on first push, and merges as soon as verified.
- Corpus:
ps-case101-hallorans-books.json→case_id CASE001,number 1,validation_status playable(after Sarah's blind-solve sign-off). Cases 001–008 → 002–009. Stage via/cr-corpus; Sarah commits in Press Raven. - Export with
--write, thengit rmthe eight stalecaseNNN-*.json. The exporter never deletes, and--verifywon't catch duplicates. - Progress migration: remap
PrimeSuspectRun.caseIDold → new once per profile, guarded by a defaults key. Without it, old CASE001 (Calder) wins count for Halloran's, and an in-progress CASE005 run resumes against a different case with the same version. Bumping every case's version was considered and rejected, because it discards progress. - Tests that assume Calder is
opening(): PrimeSuspectContentTests :15/:92/:111-117, EngineTests :43-57/:108, CaseLintTests :93/:140-145/:241 (shortest route 2; Halloran's is 3)/:386, SessionTests :43/:112, UI tests PrimeSuspectUITests :43/:317-345/:360/:378. - Case 1 then needs its season emissions: the Day 4 trace line on clue E03, and the verbatim Day 8 line at close (already wired in Slice 1).
Slice 3 — Case Board v4 + Agency assignment ✅ built (behind the flag)
Direction (Sarah, 2026-09-19): the Cases tab becomes the plot-driven board, and the routine games stay available and reachable in every season state.
- The Play tab's screen gets its name, The Rounds (§B2), as a header on
LaunchDeckView. This is a small layout change, since the screen has no title today. - Header (credential chip + stamp), four hero states (§9.1), THE SEASON rows (dark numeral / lit / closed), THE DOCKET (log + countdown), THE ROOM compact (nemesis + Lineup record).
- Agency assignment card on top in Act I with the play-day gate (Next assignment opens tomorrow. Agency rule.), keyed on the calendar day of the last agency play. Removed by the burn. The gate holds back only the next Agency case. It never touches a routine game, Play, or Training.
- Routine access, held as a test. Play (launch deck) and Training (
GamesView) stay untouched tabs in every state. A UI test walks Act I → burned → cleared with-seasonV4and asserts that every routine game still opens from Play and Training in each state. That test enforces §9.3's "the burn changes chrome and copy, never access". - Two features that only the old board reaches. Today they have no other entry point, so they need a home before the old sections come off the board (
HomeViewalso links Capture but is never instantiated): - Capture / "Load the Docket" (
CaptureView, the Bluebook screenshot intake) lives only in the board's evidence section. It has to keep a direct entry point. The proposal is a row in THE ROOM plus a Training card. Its label also collides with the season's Docket (§D5). - The Fence's Book (
Ledger.byGame) lives only on the board. The proposal is a compact row in THE ROOM. - The rogues gallery and rematch fold into THE ROOM (nemesis + Lineup record), per §9.1.
caseFileSection(the con-storyActRecordcards) comes off the board. Its system stays in place until §D4 is settled. - Art: see §C. The Act I hero and the unlit courtyard have no full-resolution originals yet.
Slice 4 — File room + reader + burn delivery (turns -seasonV4 on by default) ◐ partial
- Night Room table (burn night, Act III fallback, Wren's memo), drawers, reader, Pin to the board.
- The death-file text (§9.2) waits on the word ceiling (§D6).
- Result screens archived as
kind: voicedocuments.
Slice 5 — Credential states (You tab + Night Desk card) ✅ built (behind the flag)
- One shared card with three states (§9.3), plus the Night Desk subline and stamp by state. The burn changes chrome and copy, never access.
Slice 6 — Handler's Notes ✅ built (behind the flag)
- Header with portrait slot (dashed → face-on → turned away), feed, last-note border,
DAYS SILENTcounter, desk-file cards. Art: Falconer face-on and turned away (delivered).
Slice 7 — Connection Board
- Four relations, three line states, cap 3 with refund, five canonical connections, the draw sheet.
neighborPinnedis set by the Act II line.
Slice 8 — The Proof
- Six-slot chain, eligibility from drawn lines, read-in-order-stop-at-first-fail, accepted state. Credential reissue happens here (
SeasonState.acceptProofalready exists from Slice 1).
Slice 9 — Get Out of Town
- Briefing on real readiness numbers, platform scene, chalk overlay (full / faint),
chalkSeen. Art: platform (delivered), chalk overlay (rough; a clean 400×400 PNG is wanted).
B2. Names
| Name | Is | Status |
|---|---|---|
| The Rounds | The screen behind the Play tab (today LaunchDeckView, which has no title). It pairs with Cases → The Case Board. Tab labels stay plain function words: Play · Cases · Training · You | Sarah's proposal, 2026-09-19. Header lands in Slice 3 |
| a round | One game played from The Rounds. The Docket countdown (§6) counts in this unit: 4 rounds until something turns up. Could be in The Blotter, on your rounds. | Rule. Season copy never uses "round" for anything smaller than a game. The in-game step counters (NightTake.rounds, Target Practice SUSPECT 3/5, Go Undercover 2 of 5) keep their visible "n of m" labels |
| The Blotter | The new name for Fault in the Record (corpus fault-in-the-record, order 20, the Writing game) | Sarah, 2026-09-19 ("I think"), matching the 2026-09-16 unlock-verbs review's inference. The rename isn't recorded in guide.json, so stage it through /cr-corpus. The game is build_status: proposed, with no bank and no code (only the game_fault_in_the_record image set) |
Countdown rule that follows. The countdown names only rooms that exist in the build. Until The Blotter ships, the countdown can't say Could be in The Blotter, and the mockup's line (The Blotter · Client file 02) needs a built room in its place. The "measure a Blotter line" word-ceiling item in the solved-case doc stays unmeasurable for the same reason.
B3. What the overnight build did, and the calls made
Where things live
- Core (
PeregrineCore/Season/): SeasonBoardprojects a state onto the screens: phase, hero, act rows, countdown, credential face, Falconer's notes.SeasonFixturesbuilds the four mocked states through the real transitions.- Tests: 22 in
SeasonTestsandSeasonBoardTests. - App (
Peregrine/Season/): SeasonModelis session-scoped and injected at the root. A preview never writes.SeasonKitholds the shared pieces: stamps, section headers,…emphasis.PlotBoardParts,FileRoomViewandHandlersNotesVieware the new screens.CaseBoardViewstays the one Case Board. It renders the plot layout when live.CaseBoardView(mode: .room)is the room page holding today's sections whole: gallery and naming, evidence, the Fence's Book, the Training Ground.- UI tests:
SeasonBoardUITestscovers every state, the gate, the hero actions, the room page, and routine play staying reachable from Play and Training in every state.SeasonScreensUITestscovers the credential, Handler's Notes and the Night Desk.
Calls made where the sources disagreed or were silent. Each is reversible.
1. UNDER REVIEW stamp colour. The spec contradicts itself. §2's palette row gives Oxblood to "the UNDER REVIEW chip stamp", while §2's stamp rule says "under review is a status… so it is ink". The mockup uses oxblood. Built: ink, following the rule.
2. Dark act rows. They show the mockup's — / Dark. beside the numeral. The spec says "a numeral and nothing else". Neither is a title, so nothing leaks.
3. Act I open questions. Both of the spec's questions show (the mockup shows one), followed by the blank slot and Answered on the board, not here.
4. The Day 3 assignment line is written at intake. The Act I hero shows the assignment from the first open, so the Docket carries it from the start.
5. The in-fiction day is the latest day anything was logged or drawn. That's how the hero reads DAY 6, 9, 16 and 41.
6. Falconer's notes are posted by Docket lines. Each note appears only when the line it answers exists, so the feed can't run ahead of the Docket.
7. Handler's Notes entry point. The spec doesn't say where it lives. It's a row in You → Your record, beside The Docket.
8. The countdown is hidden in every state except cleared until the beat schedule is approved. That avoids showing a made-up number.
9. The file room is partial. The table, the courier slip (spec copy), drawers, reading and pinning all work. The reader says This page isn't written yet because no page text exists: all eight documents are unwritten, and the death-file draft is in the missing mockup.
10. Burn routing still lands on the Cases tab rather than pushing the file room. The hero's gold button opens the room.
Known issues
- Back buttons on dark navigation bars.
- A tint set on a pushed screen doesn't reach the system back button.
- Handler's Notes hides the system button and draws its own tan Back, which loses edge-swipe back.
- The file room keeps the system button, which is dim on the Night Room bar.
- The fix belongs at the navigation level: a per-stack tint, or a UIKit appearance for dark bars. It's worth one focused change for both.
- **The room page still shows the con-story Case File,** including Same knife work. Different men., which the spec removes as the sequel tease. Its fate is §D4.
- Capture's Training card (proposed in Slice 3) isn't built. Capture is reachable from THE ROOM's direct row and the room page.
- The Contradiction badge (§9.3) isn't built. There's no badges system.
- Two hero images are interim. The house and the unlit courtyard use the mockup's 1200px copies. The cleared hero is the house with its brightness lowered, the same approach as the mockup's filter.
- The neighbor-pinned crop is set to
focusX 0.3, so both the man at the gate and the lit window stay in frame.
C. Art inventory
| Asset | Status |
|---|---|
Image-Assets/Dark-Courtyard.png 1448×1086 | Landed 2026-09-19. Courtyard with the window lit, man at the gate, lanterns off: the neighbor pinned hero |
Image-Assets/Dark-Station.png 1448×1086 | Landed. Platform at night (Get Out of Town) |
Image-Assets/Falconer-Face.png, Falconer-Back.png 1000×1250 | Landed. Handler's Notes portrait slot (Act III / cleared) and the Proof header |
Image-Assets/Desk.png, Envelope.png 800×800 | Landed. Act III and IV season-row thumbnails |
Image-Assets/Chalk-Outline.png 800×800 | Landed. The chalk bird on stone, an opaque RGB photo. The transparent overlay for the platform lip still has to be made from it |
| House from the street (214 on the transom), courtyard with nothing lit, house darker (cleared) | Not in Image-Assets/. Only the 1200×900 copies embedded in the case-board mockup exist. They are the Act I, burn-night and cleared heroes |
| Act I / II thumbnails | Plan crops. Wanted: front room from the couch; study door, closed (800×800) |
| Act III thumbnail | The night desk with the falcon, clean, no numeral |
| Act III–IV heroes | IV emptied drawer (1600×1200); III TBD |
| Chalk overlay | Rough extraction in the mockup; a clean transparent PNG is wanted |
| File-room drawer icons | Optional, six paper objects (400×400) |
D0. Decided 2026-09-19 (morning)
| # | Question | Decision | Consequence |
|---|---|---|---|
| D1 | 214 vs 2214 | Change both. The account gets a number that doesn't end in 14, and the transom is repainted to a number unlike either. | Corpus edit to ps-case101-hallorans-books.json (the figure trace needs re-approval), a new Day 8 line, and a repaint of the Act I house art. Candidate numbers are proposed with the corpus staging. |
| D2 | Dates | DAY n everywhere. | Case 1's "7 April" becomes a day number on its evidence. The season record keeps 7 April as an internal anchor only. |
| D3a | Halloran's sign-off | Sarah plays it blind first. | Make it playable behind a debug flag with the new account number, then run the renumber on a branch once it passes. |
| D3b | Cases 3–8 | Renumber regardless. | Their review stays independent of the plot. |
| D4 | Old con-story acts | Retire them. | Stop firing Acts, and remove ActInterstitial and the Case File section. Existing ActRecord rows sit unused. |
| D5 | You tab "The Docket" row | Rename to "Cases closed" (live only). | "The Docket" means only the season log. |
| D6 | Story documents | Claude drafts them into the corpus from the season record's notes and locks, staged via /cr-corpus with a review page. | This unblocks the file room pages, the Connection Board and The Proof. |
| D7 | Credential number | P-0417 for everyone, reissued as P-0418. | Replace random issuance with a constant. Stored seasons keep their number unless reset. |
| — | UNDER REVIEW stamp | Ink, as built. | — |
| — | Dark-bar back buttons | Navigation-level fix. | Restores the system Back and swipe-back on Handler's Notes and the file room. |
| — | Capture | The room rows are enough. No Training card. | — |
| — | The Blotter rename | Hold. It stays "I think" in the plan and isn't staged. | — |
| Beats | Mechanism | Approved as proposed, with placeholder round counts tuned in playtest, and un-stick after 6 rounds. | — |
| Beats | Unbuilt hosts | Wait for the games. There's no table fallback for The Dossier, The Blotter or Before She's Back. | See the beat schedule §7 for what this blocks. |
D. Decisions needed (none block Slice 1) — superseded by §D0 where decided
1. 214 vs 2214. These are two different things, and neither record reconciles them.
- 2214 is Corrigan's cash account at Halloran's. It has been in the corpus case since its first draft (corpus 175041d), and the figure trace was approved 2026-09-17. It recurs on the Act III pharmacy log, so the log can be checked against case 1. Its lock says a document naming 2214 after Act I also names Corrigan.
- 214 is the house number painted on the transom in the Act I hero art. The v4 spec made it canon (Attended 214) because the Docket line then matches the picture.
- The recorded reason contradicts the number.
docs/reviews/2026-09-16-season-record-writing-rules-case-one.htmlsays the account number must not rhyme with SP-14 orP-[n], and that "an account number ending in 14 would rhyme with both, which is why this one does not." Yet 2214 ends in 14, and so does 214. - Risk: a student who remembers 2214 meets 214 on the Docket and the house. The near-match reads like a clue that leads nowhere, which is the kind of false link the season asks students to strike.
2. In-fiction dates. The spec says DAY n, never wall-clock. The season record anchors burn night at 7 April. Slice 1 uses day numbers. Which one does a document show?
3. Cases 3–8 sign-off. They're exported and playable, but nothing records your review. The renumber moves them to 4–9.
4. Two "act" systems. The board is settled: it's plot-driven, and the con-story ActRecord cards come off it (Slice 3). Still open: whether the con-story acts keep firing at the debrief (ActInterstitial) under a new name, or retire.
5. You tab "The Docket" row currently opens Cases Closed. Rename it, or point it at the new Docket?
6. Death-file word ceiling (§11), the Wren reveal in the accepted note (§11), and the un-stick documents (§7, confidence 65).
7. Credential number format. Slice 1 issues P- + four digits, random 0100–9898, and reissues as +1 (matching P-0417 → P-0418). If P-0417 is canon for every student, say so and it becomes a constant.
E. Final review
Pros. Slice 1 is pure data with no UI risk. It doesn't depend on the renumber, because the agency case id is one constant. The defaults-suite store keeps SwiftData's schema and the export archive untouched. The Night Desk can read a profile's credential without opening its store.
Cons and edges.
- Season state lives in the defaults suite and can't be
@Query'd. Views get it through an observable on the session instead. That's fine for a ~50-row blob, but it's a second persistence path next to SwiftData. - The burn condition depends on case 1 closing in an earlier session. A student who closes case 1 and never re-opens the app never gets the burn. That matches the spec (§5: "It waits as long as it has to").
- Backgrounding and resuming is not a session open (no
scenePhasehandling). Only a cold launch or a profile switch counts. The spec's "new session" is ambiguous here. Treating a foreground after N hours as a new session is a separate call. - The Docket copy is authored in Swift (
SeasonCanon) as[draft]. It should move to the corpus onceguide.jsongrows aseasonkey. Until then it's hand-maintained copy outside the exporter.
Confidence: 80 that Slice 1's shape is right. 75 that the built Slices 3, 5 and 6 match the spec closely enough to review on device. 55 on the slice order from here, because Slices 7–9 depend on documents that aren't written.