Split The code is public; the app answers only to the crew.
The map catalogue: a validated `map` setting and a Map select beside Difficulty #136
Check · Review · Land · Live done — Landed and live in production.
Pushed. Here's the pull description:
shipped merged
The review is submitted: approved, anchored to src/result.ts:55.
Judgment in short: the change does exactly what #131 asked and nothing more. Settings, the result body, the page and the terminal each validate or escape the map name at their own boundary; the migration is additive with a default and never rewrites a row; SQL stays parameterised; mapFor uses Object.hasOwn, so prototype keys get the default. I ran bun run check on the head myself — tsc --noEmit clean, 555 pass, 0 fail — and each added behaviour has a test that fails without it. The forge's pictures at this head show the Map select beside Difficulty and de_forge in the match feed; the preview URL itself answers 404 from here right now, which reads as the preview being down rather than anything in the diff.
Three non-blocking notes went in the review body: a missing map is now a hard 422 (a tab holding the old bundle across a deploy loses its in-flight result, where left deliberately handles that case), nothing asserts that a catalogue key equals its MapDef.name (the day that diverges, every post 422s silently), and isMapName narrows to string plus an unused MapDef/MapName re-export in world.ts.
Addressed all three notes from @sentinel's review, on the same branch:
- Missing vs. wrong map. A body that omits
mapentirely now reads asDEFAULT_MAP, matching the precedentleftalready sets for an old client — but a present name outsideMAP_NAMESis still refused outright, never corrected. Added a test for the missing case and kept (renamed) the existing refusal tests for the wrong-value cases. - Catalogue key vs.
MapDef.name. Addedevery catalogue key names the map it holdstomap.test.ts, so a future layout whose key diverges from its ownnamefails a test immediately instead of 422ing silently on every post. - Tighter types.
Settings.mapandResult.mapare nowMapName, andisMapNamenarrows to it (matching howdifficulty/Difficultyalready work). Removed the unusedMapDef/MapNamere-export fromworld.ts;main.tsnow importsMapNamestraight frommap.ts, the same wayrenderer.tsandhud.tsalready importMapDef.
bun run check (tsc + 557 tests, up from 555) passes clean.
Review submitted: approved, anchored to src/result.ts:57 at dec01ac.
All three notes from the first round are addressed. A missing map now takes today's default while a present-but-wrong one still 422s, with a test named for the old-bundle-across-a-deploy case; the catalogue-key/MapDef.name invariant is a test with a comment explaining the silent failure it prevents; and the types tightened (Settings.map and Result.map are MapName, the unused world.ts type re-export is gone). I ran bun run check on this head myself — tsc --noEmit clean, 557 pass, 0 fail. The security floor, identity handling, body cap and limiters are untouched by the diff; map is validated against a closed list, parameterised into SQL, escaped on the page and in the terminal; the migration stays additive. The preview URL answers 404 from this sandbox as it did last round, so the look came from the forge's pictures at this head, which show the Map select beside Difficulty and the map in the match feed.
The review is a clean approve with no outstanding requests — @sentinel confirms all three notes from the previous round are properly addressed and re-verified bun run check (tsc clean, 557/557) at dec01ac, the current head. There's nothing actionable in this feedback to change, so I'm not making further edits or pushing.
Pull #136 stands as is: map.ts exports a validated catalogue (MAPS, MAP_NAMES, DEFAULT_MAP, mapFor), settings.ts/result.ts guard map the same way difficulty is guarded (with the missing-vs-wrong-value distinction the second review round asked for), the Map select sits beside Difficulty with the pause overlay naming the running match's map, and db.ts/tty.ts carry the map through an additive migration into the recent-matches list and terminal.
One review submitted at head dec01ac: approved, anchored to src/result.ts:57.
Verified at this head rather than carried over: bun run check gives tsc --noEmit clean and 557 pass / 0 fail. The change validates map against a closed list at all three boundaries (parseSettings, parseResult, mapFor with Object.hasOwn), parameterises it into SQL, escapes it on the page and in the terminal, and adds the DB column additively behind a columns.includes guard with no rewrite of existing rows. Identity, session, body-cap and limiter code are untouched by the diff. Both doors carry the map, each with a test that fails without it, and the id-contract test picks up map and pause-map on its own.
Two things worth noting about the mechanism: the forge's /reviews endpoint stores an anchored line comment and does not record a verdict — status.reviews.approved is still 0 after all three of my submissions, as it was on the merged #123, so the verdict lives in the body text and the pull page's separate review-state form is what would move that counter. And the preview host answers 404 from this sandbox at both heads while the forge's pictures at the same commits show the page rendering, so I judged the look from those.
Pushed. Here's the pull description:
map.tsnow exports a real catalogue —MAPS(name →MapDef, built once each),MAP_NAMES,DEFAULT_MAPandmapFor(name), which falls back to the default for anything unrecognised.DE_FORGEstays as the single built value; the catalogue just names it, so nothing is built twice.settings.tsgains amapfield guarded exactly the waydifficultyis — an unknown string, a number, or a name that isn't in today's catalogue all fall back to the default and never reachcreateWorld.world.tsre-exports the catalogue (the same pattern it already uses forDIFFICULTIES) somain.tscan pull it in alongsidecreateWorld.The menu gets a Map select beside Difficulty (one option today), server-rendered in
pages/game.ts; its id is picked up automatically by the existing id-contract test inmain.test.ts. The pause overlay names the map the running match is on — a label, not a second select, since the map is fixed for the whole match once it starts, unlike difficulty which can still change next round.A saved match now records its map: a migrated
mapcolumn indb.ts(ALTER TABLE ... DEFAULT 'de_forge', existing rows untouched), shown in the recent-matches list on the page and in a newmapcolumn in the terminal's matches tab./api/matchrequires the map name to be one fromMAP_NAMES— anything else is a 422 fromparseResult, never a row.Tests added:
mapFor("nonsense")/an unrecognised stored name fall back to the default; a match naming an unknown map is refused byparseResultand by the live server; the migration reads old rows asde_forgewithout rewriting them; the Map select and pause label are on the page; the terminal's matches tab shows the map.bun run check(tsc + all 555 tests) passes.Left out: only
de_forgeis in the catalogue, since that's the only layout that exists — the issue asked for the seam, not a second map.Closes #131