Press Raven · Framing Proposal · Rev 2

Educational Game Design

A game is a document type. Its game types are the sub-grouping. Its content items are the entries. What doesn't fit inside one game is the mechanics catalog — and that's the only thing that lives outside.

Rev 2 supersedes Rev 1. The first pass proposed three categories — playbook, game-design, bank. That was wrong in the middle: the "how we adapted it" tier isn't entries at all, it's authored structure in guide.json, the way days and chapters already are. Rev 2 collapses to two things and derives the shape from the three shipped categories instead of inventing alongside them.

§1

The shape

A game is one category, parallel to manual and itinerary. One guide per game.

The only thing that can't live inside a game guide is the mechanics catalog — because it's cross-game, and its whole value is that Drive Corps and Peregrine draw from the same shelf.

Category · one guide per game

The Game

Grouped by game type, authored in guide.json the way days and chapters are. Entries are content items whose fields depend on which game type they feed. Drive Corps is a guide. Peregrine is a guide.

Reader: the game's loader — and you, doing QA

One guide, cross-game

The Mechanics Catalog

One entry per core mechanic: its verb, its loop, the kind of knowing it exercises, the data shape it demands. Brainstormed, not given — which is exactly why it's a catalog and not a fixed enum.

Reader: you, starting game #3

§2

The mapping, made exact

LevelField ManualItineraryGame
A · Document the manual the trip the game
B · Sub-group Chapter — authored in guide.json chapters[]; broadly the same chapter to chapter Day, then kind roster — days authored in guide.json; kind vocabulary guide-extensible via kind_extensions Game Type — authored in guide.json games[]; carries its mechanic, subject, and priority ranking
C · Entry Block — class is rule / hack / checklist / callout / passage Activity — class is place / plan_item / note; fields follow class Content item — fields follow game type, supplied per-guide by schema-extensions.json

The complexity ladder holds and gets steeper at each rung: manual groups by authored chapters with essentially flat entries; itinerary adds derived kind rosters, three classes, and cross-references; game adds a sub-group vocabulary that is invented per guide and that changes the entry's schema. Game is strictly the most complex of the three, which is the right expectation to carry in.

Where "how we adapted it" lives

Not in entries. In the game-type object, exactly the way a DayConfig carries label / title / date / headline and a ChapterConfig carries title / numeral:

"games": [
  { "id": "lineup",
    "label": "The Lineup",
    "order": 4,
    "mechanic": "judging-a-narrated-chain",     // → catalog entry id
    "subject": "Reading & Writing — con detection",
    "reskin": "A suspect narrates his reasoning; commit clean or con
               as early as you dare, then pick him from the mugshots.",
    "priorities": { "speed":"secondary", "accuracy":"primary", "stress":"secondary" },
    "divergence": "Adds the lineup after the call — naming the error,
                   not just flagging it. Feed this back to the catalog.",
    "bank_target": 40,
    "build_status": "built" }
]

divergence is the field that earns its place: it records how the built thing departed from the catalog entry and why, so discoveries flow back up instead of stranding in one game.

§3

The real disanalogy with itinerary

There are two differences, and the bigger one is not the obvious one.

The one that's already solved

Game types are brainstormed rather than given, where eat / do / shop / photo are just… known. But the corpus already anticipated this: itinerary's kind_extensions lets a guide extend the kind vocabulary in guide.json, precisely because Asheville needed shelves NYC didn't. "The sub-group vocabulary varies by product" is a solved problem with shipped precedent.

The one that actually bites

The structural difference

In itinerary, the sub-group does not change the entry's fields. A restaurant and a museum are both class: place — same address, hours, price, badges. kind is a shelf label and nothing more.

In a game, the sub-group is the schema discriminator. A Recon Sweep item (target name, image, decoy group) and a Lineup item (passage, reasoning[], break_idx, con id) share a title and nothing else.

So the C-row parallel is more true of games than the itinerary comparison suggests. There, parameters depend on class, not on section. Here they depend on the sub-group itself — and there are eleven of them in Peregrine, not four.

§4

The schema language already handles it

Verified in source rather than assumed: a schema-extensions.json with extend omitted appends fields at the top level (SchemaModels.swift:129), and extension fields are ordinary FieldDefs, so they carry applies_when (SchemaModels.swift:57). That's the same mechanism Things With a Cost already uses to give only situation entries an incantation.

The category schema — what every item has

categories/game/schemas/entry.json

{"fields": [
  {"key":"title","required":true,"type":"string",
   "prompt":"How this reads in the navigator — not what the player sees."},

  {"key":"family","label":"Game Type","required":true,"type":"string",
   "prompt":"Which game type this feeds — an id from the games array in guide.json."},

  {"key":"class","label":"Item Class","type":"enum",
   "options":["item","flavor","asset"],
   "prompt":"item = the thing the player answers · flavor = in-fiction copy
     (transmissions, murmurs, taunts) · asset = the record about a picture or scene."},

  {"key":"number","type":"int"},

  {"key":"concept","type":"string",
   "prompt":"The skill this scores against. Miss logs and spaced repetition key on
     this, not on the id — three sign names share two pictograms apiece."},

  {"key":"difficulty","type":"enum","options":["intro","standard","hard","boss"]},

  {"key":"stem","label":"The Prompt","type":"prose",
   "prompt":"What the player is shown or asked. Empty for purely visual items."},

  {"key":"choices","type":"string_list"},
  {"key":"answer","type":"string"},

  {"key":"trap","label":"The Trap","type":"prose",
   "prompt":"The wrong answer a smart player reaches for, and why it's tempting."}
]}

A note on family. The key is named family and labelled "Game Type" on purpose: the app's generic template groups by family when the schema declares one (DocumentTemplates.swift:300), so the document chapters itself by game type with no Swift written. When you later add a real game template case, renaming to game_type is a mechanical sweep across entry files — or you leave it, since the label is what anyone actually sees.

The guide extensions — what this game type needs

guides/peregrine/schema-extensions.json

[
  {"fields":[
    {"applies_when":{"family":"lineup"},"key":"passage","type":"prose"},
    {"applies_when":{"family":"lineup"},"key":"reasoning","type":"object_list",
     "prompt":"The suspect's narration, one line per object.",
     "fields":[{"key":"t","label":"Line","type":"string"}]},
    {"applies_when":{"family":"lineup"},"key":"break_idx","label":"Break Index","type":"int",
     "prompt":"Index into reasoning where the con begins. Script-validated —
       the app cannot check a pointer into a sibling array."},
    {"applies_when":{"family":"lineup"},"key":"con_id","type":"string"}
  ]},
  {"fields":[
    {"applies_when":{"family":"target-practice"},"key":"claim","type":"prose"},
    {"applies_when":{"family":"target-practice"},"key":"verdict",
     "type":"enum","options":["clean","con"]}
  ]},
  {"fields":[
    {"applies_when":{"family":"recon-sweep"},"key":"image","type":"string",
     "prompt":"Path under assets/. The entry is the record about the picture,
       never the picture."},
    {"applies_when":{"family":"recon-sweep"},"key":"decoy_group","type":"string",
     "prompt":"Which near-twins this is confusable with. Drives the difficulty
       ramp — currently pure human judgment with no tooling anywhere."},
    {"applies_when":{"family":"recon-sweep"},"key":"shape","type":"enum",
     "options":["diamond","octagon","rectangle","pennant","circle","triangle"]}
  ]}
]

One block per game type, each self-contained. Adding a twelfth game type is appending a block — not touching the category, not touching the other eleven.

The mechanics catalog — and it may need no new category at all

Because root-level extensions work, the catalog can be a catalog-category guide with its beats added by extension. family, number, metadata, similar_to are already in that schema and already group correctly:

// guides/game-mechanics/schema-extensions.json  (no "extend" key = append at root)
{"fields":[
  {"key":"beats","type":"object","fields":[
    {"key":"player_verb","label":"The Verb",
     "prompt":"What the hands do. Tap, flick, strike, order, hold, draw.","type":"string"},
    {"key":"loop","label":"The Loop",
     "prompt":"One turn, from start to consequence.","type":"prose"},
    {"key":"knowledge_fit","label":"What It Teaches Well",
     "prompt":"The kind of knowing this exercises — visual recognition, name→image
       recall, free retrieval, discrimination between near-twins, procedure,
       judging someone else's reasoning. And what it teaches badly.","type":"prose"},
    {"key":"consequence","label":"The Consequence",
     "prompt":"How feedback lands inside the fiction. Never a red X.","type":"prose"},
    {"key":"repetition","label":"Why You Play Again",
     "prompt":"What makes the next round wanted, not assigned.","type":"prose"},
    {"key":"failure_mode","label":"When It Becomes a Worksheet",
     "prompt":"The specific way this mechanic collapses into drill.
       Name it so you can watch for it.","type":"prose"}
  ]},
  {"key":"priorities","type":"object","fields":[
    {"key":"speed","type":"enum","options":["primary","secondary","ignored","penalized"]},
    {"key":"accuracy","type":"enum","options":["primary","secondary","ignored","penalized"]},
    {"key":"stress","type":"enum","options":["primary","secondary","ignored","penalized"]}
  ]},
  {"key":"content_contract","type":"object","fields":[
    {"key":"unit","label":"One Unit Is","type":"string"},
    {"key":"requires","label":"Required Fields",
     "prompt":"One per line. This becomes a game's schema-extensions block.","type":"string_list"},
    {"key":"assets","label":"Asset Needs",
     "prompt":"Art, audio, scenes — state it now. Sniper Spot stalled here.","type":"prose"},
    {"key":"volume","label":"Viable Bank Size","type":"string"}
  ]},
  {"key":"mockup","label":"Behavioral Reference",
   "prompt":"Path to the HTML mockup that IS the spec. Prose cannot carry feel —
     link it, never claim to replace it.","type":"string"},
  {"key":"instances","label":"Built As",
   "prompt":"game:game-type — where this exists today.","type":"string_list"}
]}

The honest tradeoff: family in the catalog schema is a plain string, so the five motivators — Finding, Dodging, Optimizing, Building, Surviving — are free text rather than a picker, and a typo silently creates a phantom section. Extensions append, they don't override, so you can't tighten it to an enum without a new playbook category. Start with the string; promote to a category only if the typos actually bite.

content_contract is the load-bearing field either way. It's what makes the catalog generative rather than a documentation chore: a mechanic declares the fields a game must supply to feed it, which is precisely the schema-extensions block you write when you adapt it.

§5

The join

One mechanic, three touchpoints, flowing one direction:

WhereWhat it holds
Catalog entryThe mechanic itself, and content_contract.requires — the fields any game needs to supply.
guide.json games[]The adaptation: mechanic: "signal-detection", the reskin, the priority ranking for this subject, the divergence.
schema-extensions.jsonThe contract's requires list, realised as real fields gated on applies_when: {"family": "<game type>"}.

Nothing flows back automatically — divergence is a note to yourself, not a sync. That's deliberate; Press Raven exports one direction and shouldn't learn to do otherwise.

§6

Why the catalog has to be separate

This is the part that isn't a matter of taste. Four things found in the repos:

The same mechanic has been invented twice, in two repos, with no shared name. Drive Corps's Recon Sweep — a target is named, cards stream past, tap only when the named one passes — and Peregrine's Target Practice — flick at a moving one-line claim, clean or con, hesitation drains the hit — are one signal-detection mechanic in two costumes. Nothing inside either game's guide would ever catch this. Sources/Games/ReconSweep/ReconSweepEngine.swift · Peregrine/Views/TargetPracticeView.swift
Peregrine's own spec asks for this document by name, and it doesn't exist. docs/spec/verb-registry.md is referenced as companion to the ten Knowledge Rules and was never written. The catalog is the verb registry, promoted out of one game. Peregrine/docs/spec/knowledge-rules.md
Reuse is already happening informally and losing its reasoning. Kingpin Showdown is a boss-gauntlet reskin of Snap Call — a games[] divergence recorded as a parenthetical in a view file. Peregrine/Views/KingpinShowdownView.swift
A mechanic stalled on a content requirement nobody wrote down. Drive Corps's Sniper Spot is blocked on scene art that does not exist, with no pipeline specified to produce it. A content_contract.assets line would have said so before the mechanic was greenlit. Drive Corps/README.md · drive-corps-handoff-v1.md §5.3

Across both games there are roughly fifteen built game types and four specced-but-unbuilt. My guess is they collapse to eight or nine distinct mechanics. Finding that out is the catalog's first deliverable, and it's worth the exercise even if nothing else here gets built.

§7

The philosophy belongs in the Brief

A category's brief.json can declare a slot with "writes": "editorial_notes", and editorial_notes is emitted into the CONTEXT half of every session packet. It's how Things With a Cost carries "the cost test" into every drafting session.

The move

Make "Don't gamify the question. Gamify the reason you care about the question." the admission test on the game category's brief, and give the catalog guide its own: "the player must be solving a problem in the world, not answering a question about it."

Then every packet you export — to draft a mechanic, to generate forty Lineup rounds — arrives with that sentence stated as an admission bar, at the moment of authoring. Which is the only place it erodes.

The game brief should also carry the game's forbidden vocabulary. Drive Corps bans quiz, test, practice, and study in UI copy; right now that rule lives in a handoff doc, which means it protects nothing at 2am. In the brief it rides along in every packet.

§8

The dividing line

Press Raven owns authored content. The app owns runtime content. Draw it once and it stops being ambiguous later.

In — becomes entries

  • vigilance-bank.json — 35 lineup rounds, 28 wager questions
  • Target Practice's 16 claims, a static let in the view
  • Go Undercover's 8 rounds, likewise
  • Forensics' job pool · Shakedown's conMoves · the murmurs
  • Drive Corps's signs.json (54) and dmv_questions.json (100)
  • Intercept's 12 transmission phrases — class: flavor

Out — permanently

  • Peregrine's SwiftData Question rows — photographed by the player, classified at runtime by the vision call
  • PlayerState miss logs and concept weights
  • VigilanceResult rows and the Trap Atlas

User data. Press Raven exports one direction by design; a round trip is not on offer and shouldn't be.

Assets follow the acrylics precedent: images live in guides/<game>/assets/ and the entry is the record about the asset — name, concept, decoy group, shape — never the asset itself. Drive Corps's signs.json is already exactly this shape, which is the cheapest possible proof that the model fits.

§9

What it costs, stated plainly

  1. The machine export target is declared, not built. framework.json lists it as "status": "declared"; ExportService emits paginated print HTML and MobileGuideExporter emits the mobile page — that is all that ships. Getting entries back into Data/signs.json is a script you write. Consistent with the README's own position that the press is already Claude Code pointed at the entries folder, but it is not a feature you have, and it remains the single largest gap in this proposal.
  2. The games[] array won't be read yet — but it will survive. days and chapters are typed fields on GuideConfig with hardcoded template cases; a games array is not modelled. The good news, verified: updateGuideConfig round-trips guide.json through JSONValue specifically so keys the app doesn't model are preserved (CorpusStore.swift:591). You can author it today and the app won't eat it — it just won't render from it.
  3. Making it first-class is a known, small piece of Swift: games: [GameTypeConfig]? on GuideConfig, plus a case "game" in DocumentTemplates mirroring what manual already does. Same shape of work, already precedented — not speculative. Until then the generic family grouping gives plain string headings and no per-type metadata on the page.
  4. Volume is only proven to 144. The acrylics guide is the largest thing ever exercised. Drive Corps at ~154 items is a hair past it and a reasonable stress test. Peregrine — eleven game types, each wanting its own bank — is where the document metaphor stops meaning anything. Expect to split it, and treat the first migration as a measurement.
  5. Some structure the schema language genuinely can't validate. A lineup round's break_idx points into its own reasoning array. object_list holds the shape, but nothing checks the pointer. That stays a script's job, and the field prompt should say so out loud.
§10

Sequencing

Step 1 · do this one

The mechanics catalog, as a catalog guide

No new category, no Swift, no schema work beyond one extensions file. File all ~19 mechanics from both repos as seeds — title plus seed note — then run the packet loop to draft the beats. You learn how many are actually distinct, Peregrine's missing verb registry gets written as a side effect, and if this step feels like a chore rather than a relief, you've discovered that cheaply.

Step 2 · prove the model

categories/game/ + guides/drive-corps/

Drive Corps first, not Peregrine: two game types instead of eleven, content that already exists as clean JSON mapping one-to-one onto entry fields, and the sharpest content pain of the two — its hundred-question bank has no authoring path at all, and decoy_group and shape are described in that repo as pure human judgment with no tooling. That's exactly what a schema with prompts and a confidence score fixes. Two game types is also enough to exercise applies_when properly, which is the whole bet.

Step 3 · only after you've felt the gap

The Swift: games[] and the game template

Write it once you know what the generic template can't show you — probably the priority ranking and the mechanic name in the section header. Doing it before Step 2 is guessing at a layout contract.

Step 4 · the real test

Peregrine

Eleven game types, ~40 HTML mockups that function as the actual spec, and an existing Python pipeline that already emits app-bank JSON. Press Raven would replace the authoring stage, not the ingest — and only if Steps 1–3 have earned it.

Design systems, while you're here

The framework already has the vocabulary: noir (charcoal paper, warm white ink, brass) for Peregrine; fieldkit (utilitarian kit monochrome, heavy rules, oversized numerals) for Drive Corps. The catalog itself wants editorial or fieldnotes — it's a reference you read, not a mood piece.

§11

Final review

Pros

Cons

Edge cases where this is inadequate

Adjustments worth considering

88 / 100 Confidence in this framing · was 78 at Rev 1

Up ten because the shape is now read off manual, itinerary, and the schema resolver rather than proposed alongside them, and because the two mechanisms it leans on hardest — root-level extensions and applies_when on extension fields — were verified in source rather than assumed.

The residual doubt is entirely in one place: whether the mechanics catalog earns being its own guide, or is really just the first game's game-type list, generalised too early. Step 1 is designed to answer that in an afternoon rather than a quarter.