Behavior-preserving refactor of server/engine.ts: the 8 per-feature LLM abilities
(takeovers, wars, hybrid, economy, industry, nuclear, leaders, ideology)
are extracted into server/abilities/ as an ordered ABILITIES registry. Each ability
owns its own Ability<K> — field, default(), validate, apply, schemaLine, constraint, isMeaningful, zero-arg stub(), payload, and optional stateFields —
and engine.ts becomes a thin orchestrator that loops the registry instead of
hand-enumerating the eight fields.
Why it is safe (all verified)
Identical buildSystemPrompt (no-tool + tool) — byte-for-byte vs committed goldens.
Identical buildUserPrompt JSON — byte-for-byte vs committed goldens.
Deterministic stub.ts — deterministicTurn JSON byte-identical to origin/master.
All 522 unit tests green (16 files) + typecheck + lint + build.
server/engine.tsre-exports every name it used to, so server/index.ts and all
tests work with zero edits.
No new runtime deps; no @turf/@types/geojson in any .ts; shared/ stays
dependency-free.
Client (src/) untouched.
New tests
server/abilities/registry.test.ts pins the contract: 3 golden-prompt tests (byte-identical
system/user prompts, with a REGEN_GOLDENS=1 rewrite mode), registry order + distinct
fields, default() equality and freshness, isMeaningful, the stateFields set, registry
composition, and a backward-compatibility check that the engine still re-exports every
caller-imported name.
Notes
Ships as v0.40.0 (version bump + CHANGELOG.md entry included).
Not deployed — CI only. Production deploy happens manually after merge + tag, on request.
## What this does
Behavior-preserving refactor of `server/engine.ts`: the 8 per-feature LLM abilities
(`takeovers`, `wars`, `hybrid`, `economy`, `industry`, `nuclear`, `leaders`, `ideology`)
are extracted into `server/abilities/` as an **ordered `ABILITIES` registry**. Each ability
owns its own `Ability<K>` — `field`, `default()`, `validate`, `apply`, `schemaLine`,
`constraint`, `isMeaningful`, zero-arg `stub()`, `payload`, and optional `stateFields` —
and `engine.ts` becomes a thin orchestrator that loops the registry instead of
hand-enumerating the eight fields.
## Why it is safe (all verified)
- **Identical `buildSystemPrompt`** (no-tool + tool) — byte-for-byte vs committed goldens.
- **Identical `buildUserPrompt`** JSON — byte-for-byte vs committed goldens.
- **Identical `applyTurnResult`** world-state mutations (same load-bearing order).
- **Deterministic `stub.ts`** — `deterministicTurn` JSON byte-identical to `origin/master`.
- **All 522 unit tests green** (16 files) + `typecheck` + `lint` + `build`.
- `server/engine.ts` **re-exports every name** it used to, so `server/index.ts` and all
tests work with **zero edits**.
- **No new runtime deps**; no `@turf`/`@types/geojson` in any `.ts`; `shared/` stays
dependency-free.
- **Client (`src/`) untouched.**
## New tests
`server/abilities/registry.test.ts` pins the contract: 3 golden-prompt tests (byte-identical
system/user prompts, with a `REGEN_GOLDENS=1` rewrite mode), registry order + distinct
fields, `default()` equality and freshness, `isMeaningful`, the `stateFields` set, registry
composition, and a backward-compatibility check that the engine still re-exports every
caller-imported name.
## Notes
- Ships as **v0.40.0** (version bump + `CHANGELOG.md` entry included).
- **Not deployed** — CI only. Production deploy happens manually after merge + tag, on request.
Fixes #74
Adds deterministic golden fixtures (server/abilities/goldens/) pinning the
EXACT output of buildSystemPrompt (no-tool + tool modes) and buildUserPrompt
for a fixed wwii-1939 preset plus a crafted state that exercises every
optional payload branch (regions, frontier, frontierMilitary, takeoverRisks,
unowned, markers, wars, relations, trades, embargoes, resources, industry).
These are the target: the v0.40.0 modular ability registry (P2-P5) must
reproduce these byte-for-byte. Part of #74.
Move the 5 shared reference/id primitives (resolveNationRef, resolveRegionKey,
freshNationId, freshWarId, NEW_NATION_STATS) from engine.ts into the new
server/abilities/shared.ts (breaking the engine<->ability import cycle), and
add server/abilities/types.ts with the Ability<K> interface + AbilityCtx /
UserPromptCtx types that the 8 per-feature ability modules will implement.
engine.ts imports the helpers and re-exports them so existing callers
(server/index.ts, the unit tests) are untouched. Behavior is identical; all
511 unit tests, lint and typecheck stay green.
Each per-feature LLM ability is now a self-contained module in
server/abilities/ with its own Ability<K> const (schema line, constraint
bullet, validator, applier, isMeaningful, stub default, user-payload
field). The ordered ABILITIES registry (index.ts) assembles them in the
canonical order: takeovers → wars → hybrid → economy → industry → nuclear
→ leaders → ideology.
All function bodies are moved VERBATIM from engine.ts. The engine is
untouched in this commit — P4 will rewire it to use the registry.
Behavior-preserving rewire: every per-ability code path now iterates the
ordered ABILITIES registry instead of hand-enumerating the eight fields.
- engine.ts: buildSystemPrompt (schema lines + constraint bullets),
buildUserPrompt (payload keys at their historical positions),
validateTurnResult (per-field validate), applyTurnResult (ordered apply
block), isMeaningful, and the runTurn "unclear" fallback all drive off
ABILITIES. The eight moved ability bodies (~1,249 lines) are deleted;
the names the engine and its tests import are re-exported from
./abilities.
- abilities/types.ts: Ability.stub is a zero-arg method (the stub has no
GameState/AbilityCtx to pass; it needs none) — all eight implementations
were already arg-less.
- stub.ts: deterministicTurn fills the eight ability fields from the
registry (each contributes its inert default), between the core fields
and markers — JSON output stays byte-identical (verified against
origin/master).
- db.ts: the legacy-row migration for ability-owned GameState fields
(wars/trades/embargoes/industry) iterates the registry's stateFields,
only-if-undefined, instead of a hardcoded field list.
No client changes, no new deps, no shared/ runtime deps. typecheck + lint
+ 511 unit tests green; stub JSON byte-identical to origin/master.
Fixes#74
Adds server/abilities/registry.test.ts, the safety net for the refactor:
- GOLDEN PROMPTS: the refactored engine's system prompt (both tool
variants) and user prompt are asserted BYTE-IDENTICAL to the committed
fixtures in server/abilities/goldens/ (captured in P1 from the
pre-refactor engine). The state builder the goldens were captured from
is re-embedded here, with a REGEN_GOLDENS=1 mode to rewrite the
fixtures after an intentional prompt change (verified a no-op — the
refactored output is exactly stable).
- REGISTRY CONTRACT: ABILITIES is the canonical 8-ability order with 8
distinct TurnResult fields; each ability's default() equals the
canonical TurnResult default and is a FRESH value (never a shared
reference); the default is never "meaningful" while a populated value
is; the registry declares exactly the durable GameState fields the db
migration must default (wars/trades/embargoes/industry); the registry
composes (a hypothetical extra ability appends cleanly to every
uniform loop); and the engine still re-exports every name its callers
import from it (backward compatibility).
The throwaway _golden-capture.ts is deleted (its logic is now the
REGEN mode of this test).
Fixes#74
- package.json: 0.39.0 → 0.40.0.
- CHANGELOG.md: new v0.40.0 entry documenting the modular LLM-ability
system (the behavior-preserving extraction of the 8 per-feature
abilities into the ordered ABILITIES registry), with the guarantees
(byte-identical prompts/mutations/stub, zero client changes, no new
deps, shared/ stays dep-free) and the new registry/golden tests.
Verified: typecheck + lint + 522 unit tests + build all green.
Fixes#74
kamilkrzywda
scheduled this pull request to auto merge when all checks succeed 2026-10-16 09:27:41 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What this does
Behavior-preserving refactor of
server/engine.ts: the 8 per-feature LLM abilities(
takeovers,wars,hybrid,economy,industry,nuclear,leaders,ideology)are extracted into
server/abilities/as an orderedABILITIESregistry. Each abilityowns its own
Ability<K>—field,default(),validate,apply,schemaLine,constraint,isMeaningful, zero-argstub(),payload, and optionalstateFields—and
engine.tsbecomes a thin orchestrator that loops the registry instead ofhand-enumerating the eight fields.
Why it is safe (all verified)
buildSystemPrompt(no-tool + tool) — byte-for-byte vs committed goldens.buildUserPromptJSON — byte-for-byte vs committed goldens.applyTurnResultworld-state mutations (same load-bearing order).stub.ts—deterministicTurnJSON byte-identical toorigin/master.typecheck+lint+build.server/engine.tsre-exports every name it used to, soserver/index.tsand alltests work with zero edits.
@turf/@types/geojsonin any.ts;shared/staysdependency-free.
src/) untouched.New tests
server/abilities/registry.test.tspins the contract: 3 golden-prompt tests (byte-identicalsystem/user prompts, with a
REGEN_GOLDENS=1rewrite mode), registry order + distinctfields,
default()equality and freshness,isMeaningful, thestateFieldsset, registrycomposition, and a backward-compatibility check that the engine still re-exports every
caller-imported name.
Notes
CHANGELOG.mdentry included).Fixes #74