chore: bump to v0.24.0 (nation-op split/merge, #16 P4b) + CHANGELOG

This commit is contained in:
2026-10-14 13:42:46 +00:00
parent 14c5d4881d
commit 7eaed42d51
2 changed files with 14 additions and 1 deletions
+13
View File
@@ -2,6 +2,19 @@
All notable changes to **History** are listed here, newest first. All notable changes to **History** are listed here, newest first.
## v0.24.0 — 2026-10-14 13:41
- **Part 4b of #16: nation splits and merges — "LLM proposes, player confirms" is now complete.** P4a let the LLM propose a *rename* (a safe, single-field change). Now the same confirm/reject flow actually applies the two destructive ops: a **split** (one nation dissolves into several) and a **merge** (several nations fold into one). This is the last piece of #16 — all three `NationOp` kinds are now applied on player confirm.
- **Split (`server/engine.ts`).** `applyNationOp` now handles `kind: "split"`: it resolves the parent `nationRef` and requires **≥2** named parts (fewer → a safe no-op, the op is still resolved and removed). It partitions the parent's **region-key** territory across the parts **in order** — each part gets the keys of its `territory` that the parent actually owns and that no earlier part claimed (the **first part to name a key wins**, so a key is never held twice); any of the parent's keys no part claimed go to **part 0** (in the parent's territory order), so nothing is orphaned. Each new nation gets a **fresh id** (`freshNationId`: first free of `n1`, `n2`, …, tracked in a running `taken` set so ids never collide within the op), the baseline `NEW_NATION_STATS` (a neutral 50/50/50/50 profile — *not* a copy of the parent's, since a small rump state is not the parent's power), a copy of the parent's relations, and the parent's `type`; the parent is then removed and its id dropped from every other nation's relations map (`dropRelationsTo`). If the **parent was the player's nation**, `playerNationId` moves to the chosen part (`edits.playerPart`, clamped into range, default 0) — that same part also receives an `edits.name` override.
- **Merge (`server/engine.ts`).** `kind: "merge"`: every input ref and the survivor ref must resolve, at least **two distinct** inputs are required, and the **survivor must be one of the inputs** — any failure is a safe no-op. The survivor keeps its **id** and its **own stats**; it gains the **union of all inputs' territories** (the survivor's keys first, then each other input's unclaimed keys in its own order) and a **union of the inputs' relations** (the survivor's own view of a third party wins, else the first input's); it is renamed to `edits.name` → `into` → its own name (in that precedence). The other inputs are removed and their ids dropped from every remaining nation's relations maps; if the **player led a removed input**, `playerNationId` moves to the survivor.
- **Confirm contract extended (`shared/protocol.ts`).** `ConfirmNationOpRequest.edits` is now `{ name?: string; playerPart?: number }` — `name` overrides a rename's target or a merge's merged name; `playerPart` picks which new part the player's nation becomes in a split (default 0). The `SplitOp` / `MergeOp` / `NationOp` doc comments and the `TurnResult.nationOps` doc now describe the *applied* semantics (no longer "a no-op until P4b").
- **Prompt (`server/engine.ts`).** The system prompt's JSON contract now shows all three `nationOps` shapes (rename / split / merge), and the HARD CONSTRAINTS gain one line per op explaining when to use it and the "≥2 parts" / "survivor must be an input" rules — so the model knows what it may propose and that each is a player-confirmed PROPOSAL.
- **Client (`src/components/NationOpDialog.tsx`).** The dialog now renders all three kinds with a Confirm button (no more "dismiss-only" split/merge rows): a **split** lists each part (name + the number of regions it was assigned) and, when the split's parent is the **player's own nation**, shows a **radio group** to pick which new part the player leads (preselected 0, so the choice is always valid); a **merge** shows the input nations as chips (the **survivor** highlighted) and an **editable name** for the merged state (defaulting to `into`). Rename keeps its editable-name behavior. The new `onConfirm(index, edits?)` signature threads `playerPart` through `ActionPanel` and `App.tsx` (`runConfirmNationOp`) to the API helper.
- **Defensive invariants.** All ops are **pure and never throw**: an unresolvable or ambiguous ref, too few parts, or a survivor that isn't an input makes the op a no-op on the world (the proposal is still resolved and removed on confirm, mirroring P4a's ambiguous-ref behavior). **Sole ownership** is preserved by construction (a key lands in at most one new part / one survivor), and `playerNationId` is always re-pointed to a still-existing nation. The **stub is unchanged** — it still never proposes a nation op (`nationOps: []`) — so, like P4a/P4b before it, the new paths are covered by **unit tests only** and the e2e suite (CI, `LLM_STUB=1`) is unaffected.
- **Tests (`server/engine.test.ts`).** New `freshNationId`, `applyNationOp (split)`, and `applyNationOp (merge)` suites: the partition (first-wins, leftovers-to-part-0, sole ownership, fresh ids/stats/relations, parent removed, relations cleanup, player-pointer move, clamped `playerPart`, `edits.name` on the chosen part), the merge (union territory, survivor stats, relations union with survivor-wins, removed ids dropped from everyone else, into-name, player-pointer move, and the four no-op guards), plus an updated prompt-contract test asserting all three op shapes. The old "split/merge is a no-op" P4a test is retargeted to a 1-part split. The unit suite grows **151 → 164**.
- **Verification.** Typecheck, lint, **164 unit tests**, and the production build all pass. The e2e suite (CI, `LLM_STUB=1`) carries no nation-op assertions, so it stays green with the unchanged stub.
- **Deploy:** manual `docker compose up -d --build` on the host (no `down -v`).
- **Part of #16 — major nation changes (P4b).** This completes #16: `Fixes #16` on the merged PR closes the issue.
## v0.23.0 — 2026-10-14 12:58 ## v0.23.0 — 2026-10-14 12:58
- **Part 4a of #16: 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** (`GameState.pendingNationOps`) and only takes effect when the player **confirms** it (optionally editing the new name) or is dropped on **reject**. This is the first of the "major nation changes" the LLM may propose — the `NationOp` contract is stable now; **split** and **merge** are declared in the same union but are *accepted-and-stored, not applied* until P4b. - **Part 4a of #16: 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** (`GameState.pendingNationOps`) and only takes effect when the player **confirms** it (optionally editing the new name) or is dropped on **reject**. This is the first of the "major nation changes" the LLM may propose — the `NationOp` contract is stable now; **split** and **merge** are declared in the same union but are *accepted-and-stored, not applied* until P4b.
- **New `NationOp` union (`shared/protocol.ts`).** `rename` / `split` / `merge`, discriminated by `kind`. Each names its target via a `nationRef` the LLM may give as either a nation **id** or **name**. `TurnResult.nationOps` is a **required** `NationOp[]` (the model's proposals this turn); the confirm/reject request types (`NationOpConfirmRequest` `{index, edits?{name?}}`, `NationOpRejectRequest` `{index}`, response `{game}`) live here too. - **New `NationOp` union (`shared/protocol.ts`).** `rename` / `split` / `merge`, discriminated by `kind`. Each names its target via a `nationRef` the LLM may give as either a nation **id** or **name**. `TurnResult.nationOps` is a **required** `NationOp[]` (the model's proposals this turn); the confirm/reject request types (`NationOpConfirmRequest` `{index, edits?{name?}}`, `NationOpRejectRequest` `{index}`, response `{game}`) live here too.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "history", "name": "history",
"version": "0.23.0", "version": "0.24.0",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "AI-driven alternate-history simulator — the LLM is the world engine (Pax Historia style).", "description": "AI-driven alternate-history simulator — the LLM is the world engine (Pax Historia style).",