feat: nation-op rename review — confirm/reject routes, client dialog, AGENTS.md (P4a, #16)
Wire the 'LLM proposes, player confirms' nation-rename flow end to end: - server: POST /api/games/:id/nation-op/confirm|reject (404/400, version-bumped SSE state) - client: confirmNationOp/rejectNationOp api helpers + NationOpDialog (editable rename) wired into the Action panel via a 'Nation proposals' trigger - AGENTS.md: document the pendingNationOps mechanism, routes, nationRef resolution, stub-never-proposes
This commit is contained in:
@@ -52,7 +52,7 @@ history/
|
||||
├── eslint.config.js # flat config (ESLint 9)
|
||||
├── tsconfig.json # strict; aliases @/ → src/, @shared/ → shared/
|
||||
├── vite.config.ts # dev server + proxy /api,/sse → :4321; @ / @shared aliases
|
||||
├── vitest.config.ts # unit tests (server/**/*.test.ts + scripts/**/*.test.ts; 127 tests)
|
||||
├── vitest.config.ts # unit tests (server/**/*.test.ts + scripts/**/*.test.ts; 151 tests)
|
||||
├── playwright.config.ts # deterministic e2e (webServer runs npm run dev, LLM_STUB=1)
|
||||
├── index.html
|
||||
├── presets/ # 5 scenario JSON files + index.json (manifest)
|
||||
@@ -67,10 +67,14 @@ history/
|
||||
│ └── *.test.ts # turf-free vitest tests pinning the dataset + both manifests
|
||||
├── shared/ # NO runtime deps — the single source of truth
|
||||
│ ├── types.ts # domain types (Nation, GameState, RoundItem, Preset, …)
|
||||
│ └── protocol.ts # LLM TurnResult schema (incl. `unowned`, v0.22 #16 P3) + AddAction/AddDiplomacy/RemoveQueueItem/Jump request types + pure helpers (incl. the no-man's-land map-presentation contract)
|
||||
│ └── protocol.ts # LLM TurnResult schema (incl. `unowned`, v0.22 #16 P3, and
|
||||
│ # `nationOps`, v0.23 #16 P4a) + AddAction/AddDiplomacy/RemoveQueueItem/
|
||||
│ # Jump + NationOp confirm/reject request types + pure helpers
|
||||
├── server/
|
||||
│ ├── index.ts # Express: /api/* (health|action|diplomacy|queue/remove|jump|advisor|games|map|presets), /sse/:id; reads PORT/LLM_*/DATA_DIR
|
||||
│ ├── engine.ts # pure: prompts / parse+validate / applyTurnResult (sole-ownership + no-man's-land releases, v0.22 #16 P3) / runTurn (tool path + plain fallback) / getNeighbours tool + executor / playerFrontier / queue ops / advisor
|
||||
│ ├── index.ts # Express: /api/* (health|action|diplomacy|queue/remove|jump|advisor|
|
||||
│ │ # nation-op/confirm|nation-op/reject|games|map|presets), /sse/:id; reads
|
||||
│ │ # PORT/LLM_*/DATA_DIR
|
||||
│ ├── engine.ts # pure: prompts / parse+validate / applyTurnResult (sole-ownership + no-man's-land releases, v0.22 #16 P3) / runTurn (tool path + plain fallback) / getNeighbours tool + executor / playerFrontier / queue ops / advisor / nation-op ops (resolveNationRef / applyNationOp / confirmNationOp / rejectNationOp, v0.23 #16 P4a)
|
||||
│ ├── llm.ts # LlmClient: chat() + chatWithTools() (bounded function-calling loop), injectable transport, retry+backoff, logging + env config
|
||||
│ ├── regions.ts # turf-free region-key data access: load manifest/geo/adjacency (public/ then dist/), country→keys expansion, reference table, neighboursOf/hasAdjacency
|
||||
│ ├── stub.ts # deterministic LLM transport (tests/e2e): one frontier conquest per turn + ADVISOR_MARKER
|
||||
@@ -91,7 +95,8 @@ history/
|
||||
├── PresetPicker.tsx / NationSelect.tsx # home + nation pick screens
|
||||
├── WorldMap.tsx # MapLibre basemap + choropleth + click→dossier + hover tooltip + player highlight
|
||||
├── NationPanel.tsx # a nation's stats/territory/diplomacy dossier
|
||||
├── ActionPanel.tsx # tabs: Action / Diplomacy / Advisor + pending-queue list (per-item trash button + confirm dialog) + "Jump forward"
|
||||
├── ActionPanel.tsx # tabs: Action / Diplomacy / Advisor + pending-queue list (per-item trash button + confirm dialog) + "Jump forward" + "Nation proposals" trigger (v0.23 #16 P4a)
|
||||
├── NationOpDialog.tsx # review LLM-proposed nation ops (rename/split/merge) — confirm (editable rename) or reject (v0.23 #16 P4a)
|
||||
├── EventFeed.tsx # scrolling event log (newest first)
|
||||
├── FloatingWindow.tsx # the HUD window primitive — free-floating (draggable, position persisted), or `pinned` (fixed top-center bar), or `dock`ed (full-height left/right column); renders dock controls when `onDockChange` is set
|
||||
├── StatBar.tsx
|
||||
@@ -105,7 +110,7 @@ history/
|
||||
| `npm run dev` | Express on :4321 (tsx watch) + Vite on :5173 (proxies `/api`,`/sse`) concurrently |
|
||||
| `npm run build` | `vite build` (client → `dist/`) then `tsup server/index.ts` → `server/dist/index.js` |
|
||||
| `npm start` | run the compiled server (`node server/dist/index.js`) on :4321 |
|
||||
| `npm test` | vitest unit tests (`server/` + `scripts/`; 127 tests) |
|
||||
| `npm test` | vitest unit tests (`server/` + `scripts/`; 151 tests) |
|
||||
| `npm run typecheck` | `tsc --noEmit` (strict) across src/server/shared/e2e/configs |
|
||||
| `npm run lint` | ESLint flat config |
|
||||
| `npm run e2e` | Playwright, fully deterministic (`LLM_STUB=1`); **runs in CI on every PR** (see below) |
|
||||
@@ -184,6 +189,24 @@ history/
|
||||
— the distinction is decided by the client from `GameState.unowned` (via `regionFillStyle` /
|
||||
`unownedTooltipLabel`), never by the `RegionMap`, which only knows "owned" vs "absent". A fresh
|
||||
game always starts with `unowned: []` (legacy SQLite rows load with it defaulted).
|
||||
- **Nation ops — “LLM proposes, player confirms” (v0.23, #16 P4a).** `TurnResult.nationOps` (required
|
||||
in the schema) carries the LLM’s proposed **`NationOp`s** — a discriminated union of `rename` /
|
||||
`split` / `merge`. `rename` is fully applied; `split` and `merge` are **declared shapes only**
|
||||
(accepted and stored as pending, but a no-op until P4b implements them). Each op names its target
|
||||
via `nationRef` — the LLM may use a nation’s **id or name**; `resolveNationRef` resolves id-first,
|
||||
then a case-insensitive unique-name match, and returns `null` for an unknown or ambiguous ref (such
|
||||
ops are dropped defensively in `validateNationOps`, never thrown). A turn’s ops are appended to
|
||||
`GameState.pendingNationOps`, which is **never cleared on a turn commit** — pending proposals persist
|
||||
across turns until resolved (like the action queue). The player then decides per op via
|
||||
`POST /api/games/:id/nation-op/confirm` (`{index, edits?{name?}}`) or `.../nation-op/reject`
|
||||
(`{index}`): `confirmNationOp` applies the op to a clone (a rename sets `Nation.name` to the edited
|
||||
value or, failing that, the proposed `to`; id / territory / relations / `playerNationId` stay valid,
|
||||
so a renamed player is re-named in place) and `rejectNationOp` just drops it — both splice by index
|
||||
and bump `version` once (an invalid index → `null` → HTTP 400). The response is `{ game }` and the
|
||||
server broadcasts a version-bumped SSE `state`, so every tab’s version-guarded `applyState` stays in
|
||||
sync. The client lists the pending ops in a “Nation proposals” dialog (in the “Your move” panel) with
|
||||
an editable rename name plus Confirm / Reject. The stub never proposes a nation op (`nationOps: []`),
|
||||
so this path is covered by unit tests only (`server/engine.test.ts`).
|
||||
- **Map click → dossier target (owner resolved from the RegionMap, by region key).**
|
||||
Clicking a sub-region does NOT open a popup — it switches the dossier to that sub-region's
|
||||
owning nation (`focusNation(ownerId, {name})` in `App.tsx`, which also records which region was
|
||||
@@ -209,9 +232,12 @@ history/
|
||||
in `wwii-1939`) transfers nothing. Since v0.22 (#16 P3) the stub **never releases** a region to
|
||||
no man's land (it always returns `unowned: []` and ignores the payload's `unowned` field) — its
|
||||
one-conquest behavior is what the e2e assertions rely on, so the release/re-claim path is
|
||||
covered by unit tests only (`server/engine.test.ts`, `server/map.test.ts`). The stub answers
|
||||
plain `content` (never tool calls), so it naturally bypasses the tool loop in a single round.
|
||||
Unit + e2e assertions rely on these exact values, so keep the stub deterministic.
|
||||
covered by unit tests only (`server/engine.test.ts`, `server/map.test.ts`). Since v0.23 (#16 P4a)
|
||||
the stub also returns `nationOps: []` — it **never proposes** a nation op (rename/split/merge), so
|
||||
the confirm/reject path is covered by unit tests only, mirroring the P3 approach; all e2e
|
||||
assertions are unaffected. The stub answers plain `content` (never tool calls), so it naturally
|
||||
bypasses the tool loop in a single round. Unit + e2e assertions rely on these exact values, so
|
||||
keep the stub deterministic.
|
||||
- **HUD layout.** The game screen is a full-viewport map (z-0) with `FloatingWindow` overlays. The
|
||||
top bar is **pinned** — a fixed, non-movable bar centered at the top a few px down (it re-centers
|
||||
on resize and always floats above the other windows). The **dossier / event-feed / action**
|
||||
|
||||
Reference in New Issue
Block a user