- shared/protocol.ts: TurnResult.unowned (keys released this turn) + the map
presentation contract (regionFillStyle / unownedTooltipLabel, hue-less grey)
- shared/types.ts: required GameState.unowned (cumulative mid-game releases;
[] at init, distinct from preset-unassigned regions)
- server/engine.ts: applyTurnResult applies releases first (sole-owner only,
never fabricating no man's land from unassigned keys), then claims against
the post-release snapshot (gain on an ownerless key = re-claim); lone
release is isMeaningful; prompts + user payload carry the unowned field
- server/stub.ts: unchanged behavior (never releases; e2e stays stable)
- server/db.ts: legacy rows load with unowned defaulted to []
- server/index.ts: create-game sets []; jump reference table includes
released keys so the model can name them for re-claim
- WorldMap/App: unowned prop -> visible grey + 'No man's land' tooltip
- unit tests: 105 -> 127 (release/re-claim suites, presentation contract,
legacy-row normalization)
The single source of truth now keys ownership by region key (one per owned
sub-region) instead of a parent-country ISO_A3:
- shared/types.ts, shared/protocol.ts: TerritoryChange / RegionMap / regionOwner
doc flips; mapFeatureOwner now reads the feature's stamped properties.regionKey
(never the repacked numeric feature.id, nor the sub-region id — the #6 guard).
- server/regions.ts (new, turf-free): loads region-keys.json + regions.geo.json
once at boot; expands a nation's ISO_A3 list to its sub-regions' keys, builds
the LLM 'regions' reference table (key -> "name (parent)"), and exposes the
set of valid keys. Degrades gracefully when the files are missing.
- server/engine.ts: buildSystemPrompt instructs the model to use region keys;
buildUserPrompt gains an optional regionReference table; applyTurnResult gains
an optional validKeys set so hallucinated keys are ignored.
- server/index.ts: loads region data at boot; expands each nation's country list
to region keys at game creation; the jump route passes the reference + valid
keys through runTurn.
The stub is unchanged (it emits an empty, region-key-conformant territoryChanges).
- DELETE /api/games/:id route + Db.delete() (404 when absent)
- PresetPicker: per-saved-game trash button + confirm dialog (Cancel/Delete)
- App: runDeleteGame; useSse onError -> goHome on 'deleted'/'not found'
(a deleted/missing game now returns the live tab and any deep link home)
- DeleteGameResponse in shared/protocol.ts; api.deleteGame()
- unit test (Db.delete) + 2 e2e tests (home-screen delete; in-flight delete
returns the active tab home and a fresh deep link lands on home)
- version 0.7.0 -> 0.8.0 + changelog
Fixes#20