feat: nation-op rename — LLM proposes, player confirms (v0.23.0, #16 P4a) #38

Merged
kamilkrzywda merged 3 commits from feat/16-p4a-rename into master 2026-10-14 15:03:41 +02:00
Owner

Part of #16 — major nation changes, P4a: nation renames ("LLM proposes, player confirms").

The LLM can now propose that a nation be renamed during a turn. Unlike stat / relation /
territory changes (applied automatically), a rename is stored as a pending proposal and only
takes effect when the player confirms it (optionally editing the new name) or is dropped on
reject. The NationOp contract is stabilized now: split and merge are declared in the same
union but are accepted-and-stored, not applied until P4b.

What changed

  • shared/protocol.ts — new NationOp discriminated union (rename / split / merge), each
    naming its target via a nationRef the model may give as a nation id or name. TurnResult gains a
    required nationOps: NationOp[] (the model's proposals this turn). New request/response types:
    NationOpConfirmRequest { index, edits?: { name? } }, NationOpRejectRequest { index }, response { game }.
  • shared/types.ts — GameState.pendingNationOps: NationOp[] (type-only import, no runtime cycle).
    A turn appends to it (never cleared on commit), so a proposal persists across turns until resolved.
    Legacy SQLite rows and new games default it to [] (server/db.ts).
  • server/engine.ts (all pure) —
    • resolveNationRef(state, ref): exact id first, else a case-insensitive unique-name match, else null (unknown/ambiguous never throws).
    • validateNationOps: drops invalid ops defensively.
    • applyTurnResult: keeps existing pending ops and appends this turn's valid ones; a rename-only turn is isMeaningful (survives retry/nudge).
    • applyNationOp(state, op, edits?): mutates a clone (rename → Nation.name = edited value or the proposed to; id / territory / relations / playerNationId stay valid, so a renamed player is re-named in place; split/merge are no-ops for now).
    • confirmNationOp(state, index, edits?) / rejectNationOp(state, index): splice by index + bump version once; return null for an invalid index.
    • System prompt (tool-mode + plain-mode) gains the nationOps JSON field and the rename-proposal rule.
  • server/index.ts — POST /api/games/:id/nation-op/confirm and .../reject (404 unknown game, 400 bad index), each returning { game } and broadcasting a version-bumped SSE state — mirrors the existing queue add/remove routes. Fresh games start with pendingNationOps: [].
  • Client — src/lib/api.ts confirmNationOp / rejectNationOp; new src/components/NationOpDialog.tsx (editable rename + Confirm/Reject, Dismiss for split/merge); wired into the "Your move" panel (ActionPanel.tsx) via a "Nation proposals (N)" trigger and App.tsx (opBusy flag + version-guarded applyState).
  • server/stub.ts — returns nationOps: []; the stub never proposes a nation op, so the e2e suite is unaffected (rename is covered by unit tests only, mirroring the P3 no-man's-land approach).
  • AGENTS.md / CHANGELOG.md — refreshed for P4a; version bumped to 0.23.0.

Verification

  • npm run typecheck, npm run lint, npm test (151 unit tests, up from 127), and npm run build all pass locally.
  • e2e (CI, LLM_STUB=1) has no nation-op assertions and stays green with the unchanged stub.
  • Deploy: manual docker compose up -d --build after merge (no down -v — GameState only gains a defaulted field, so existing saves stay compatible).

Related: #16 (major nation changes — this is P4a; P4b will implement split/merge).

Part of #16 — **major nation changes, P4a: nation renames ("LLM proposes, player confirms").** The LLM can now propose that a nation be **renamed** during a turn. Unlike stat / relation / territory changes (applied automatically), a rename is stored as a *pending proposal* and only takes effect when the player **confirms** it (optionally editing the new name) or is dropped on **reject**. The `NationOp` contract is stabilized now: `split` and `merge` are declared in the same union but are *accepted-and-stored, not applied* until P4b. ## What changed - **`shared/protocol.ts`** — new `NationOp` discriminated union (`rename` / `split` / `merge`), each naming its target via a `nationRef` the model may give as a nation **id or name**. `TurnResult` gains a **required** `nationOps: NationOp[]` (the model's proposals this turn). New request/response types: `NationOpConfirmRequest` `{ index, edits?: { name? } }`, `NationOpRejectRequest` `{ index }`, response `{ game }`. - **`shared/types.ts`** — `GameState.pendingNationOps: NationOp[]` (type-only import, no runtime cycle). A turn **appends** to it (never cleared on commit), so a proposal persists across turns until resolved. Legacy SQLite rows and new games default it to `[]` (`server/db.ts`). - **`server/engine.ts`** (all pure) — - `resolveNationRef(state, ref)`: exact id first, else a case-insensitive unique-name match, else `null` (unknown/ambiguous never throws). - `validateNationOps`: drops invalid ops defensively. - `applyTurnResult`: keeps existing pending ops and appends this turn's valid ones; a rename-only turn is `isMeaningful` (survives retry/nudge). - `applyNationOp(state, op, edits?)`: mutates a clone (rename → `Nation.name` = edited value or the proposed `to`; id / territory / relations / `playerNationId` stay valid, so a renamed player is re-named in place; split/merge are no-ops for now). - `confirmNationOp(state, index, edits?)` / `rejectNationOp(state, index)`: splice by index + bump `version` once; return `null` for an invalid index. - System prompt (tool-mode + plain-mode) gains the `nationOps` JSON field and the rename-proposal rule. - **`server/index.ts`** — `POST /api/games/:id/nation-op/confirm` and `.../reject` (404 unknown game, 400 bad index), each returning `{ game }` and broadcasting a version-bumped SSE `state` — mirrors the existing queue add/remove routes. Fresh games start with `pendingNationOps: []`. - **Client** — `src/lib/api.ts` `confirmNationOp` / `rejectNationOp`; new `src/components/NationOpDialog.tsx` (editable rename + Confirm/Reject, Dismiss for split/merge); wired into the "Your move" panel (`ActionPanel.tsx`) via a "Nation proposals (N)" trigger and `App.tsx` (`opBusy` flag + version-guarded `applyState`). - **`server/stub.ts`** — returns `nationOps: []`; the stub **never** proposes a nation op, so the e2e suite is unaffected (rename is covered by unit tests only, mirroring the P3 no-man's-land approach). - **`AGENTS.md` / `CHANGELOG.md`** — refreshed for P4a; version bumped to **0.23.0**. ## Verification - `npm run typecheck`, `npm run lint`, `npm test` (**151** unit tests, up from 127), and `npm run build` all pass locally. - e2e (CI, `LLM_STUB=1`) has no nation-op assertions and stays green with the unchanged stub. - Deploy: manual `docker compose up -d --build` after merge (no `down -v` — `GameState` only gains a defaulted field, so existing saves stay compatible). Related: #16 (major nation changes — this is P4a; P4b will implement split/merge).
kamilkrzywda added 3 commits 2026-10-14 15:01:12 +02:00
- shared/protocol.ts: RenameOp/SplitOp/MergeOp + NationOp union; TurnResult.nationOps (required); ConfirmNationOp/RejectNationOp request+response types
- shared/types.ts: GameState.pendingNationOps (type-only import of NationOp — no runtime cycle)
- server/engine.ts: validateNationOps parser wired into validateTurnResult; isMeaningful counts proposals; applyTurnResult appends to the surviving pending list (never applies); runTurn fallback literal; system prompt gains the nationOps JSON field + rename proposal rule (both modes); new resolveNationRef/applyNationOp/confirmNationOp/rejectNationOp (id-first then unique case-insensitive name; rename changes Nation.name only; split/merge no-op until P4b; index validation returns null for 400)
- server/db.ts: legacy rows load with pendingNationOps defaulted to []
- server/index.ts: fresh games start with pendingNationOps: []
- server/stub.ts: deterministicTurn returns nationOps: [] (the stub never proposes — unit-only coverage, matching P3)
- tests: fixture updates (engine/db/map/regions) + new suites for resolveNationRef, validateNationOps, proposal lifecycle, confirm/reject (151 tests total)
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
chore: bump to v0.23.0 (nation-op rename, #16 P4a) + CHANGELOG
CI / build + unit + e2e (pull_request) Successful in 2m26s
6ebe21591e
kamilkrzywda scheduled this pull request to auto merge when all checks succeed 2026-10-14 15:01:30 +02:00
kamilkrzywda merged commit f72a59bf5e into master 2026-10-14 15:03:41 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OpenCode/History#38