06 — AI Opponents
This document specifies the Staff: Every Last City's AI opponent, the framework that runs it, and the evidence that has to exist before any claim about its strength may be made. It owns the layered decision architecture and how search stays tractable; the evaluation function and its exact integer arithmetic; how an AI seat forms belief under fog and issues orders under commit-before-you-see; the difficulty ladder and the shipped personality roster; time, memory and determinism budgets for mobile, desktop and the headless farm; the plugin interface third-party AIs are written against; AI substitution for absent players; and the evaluation, anti-regression and adversarial-testing pipeline.
Advanced AI opponents are a headline feature of this product (00-overview.md pillar 6, OV-100 through OV-120). Most people who play a game of this shape will play it mostly alone, and for them the AI is not a substitute for the game — it is the game. Two commitments follow and they govern every requirement below:
The Staff sees exactly what a human in its seat sees, and gets nothing a human seat could not have. Difficulty is quality of play. §3.
A weak opponent plays a smaller game, not a broken one. Weakness is produced by a smaller world model and a slower re-plan, never by corrupting a decision the AI already made correctly. §13.
Status: Draft v0.2 · Owner: unassigned · Depends on: 00-overview.md, 01-game-rules.md,
02-units-and-industry.md, 03-architecture.md, 10-turn-model.md, 11-combat.md, 12-economy.md,
13-command.md, 14-victory.md, and — for plugin hosting, seat substitution and the platform that runs
the farm — 05-multiplayer.md, 07-modding-content.md, 08-services-platform.md. Binding non-requirement
sources: docs/design/00-direction.md, docs/design/01-decision-turn-model.md, and the four AI design
proposals docs/design/ai-architecture.md, ai-opponents.md, ai-learning.md, ai-redteam.md.
A note on the numbers. Every microsecond, millisecond and step figure in this document is a budget being proposed, not a measurement, with two exceptions that are measured and belong to 11-combat.md (CB-2590): the engine sustains ≥ 500 Clash resolutions per Cascade and ≥ 100,000 previews per second on reference hardware. Everything else is engineering estimate, and every requirement that rests on one says so. An AI specification that quotes invented timings as if they were data is exactly the over-promise pillar 6 is most at risk from.
1. What this document owns, and what it does not
Owns: the Staff's architecture, evaluation, belief model, budgets and determinism obligations; the AI seat contract; the difficulty ladder (Rank); the shipped personality roster; the AI plugin interface's decision surface; AI mandates for absent seats; the self-play, tuning and anti-regression pipeline; and the adversarial test roster.
Does not own, and cites rather than restates: the order vocabulary and the command layer (13-command.md); the turn structure and activation (10-turn-model.md); the combat predictor (11-combat.md); the economy's closed forms (12-economy.md); the Track abstraction and the endgame (14-victory.md); the fog model and the Contact Register (01-game-rules.md); the plugin container, sandbox, transport and SDK (07-modding-content.md); the WASM host, determinism CI and performance fixtures (03-architecture.md); the substitution transport and consent flow (05-multiplayer.md); the farm's operation as a service (08-services-platform.md).
AI-005 This document MUST NOT define a way for an AI seat to affect the world other than by submitting order records to the authoritative order log (10-turn-model.md TM-200, 13-command.md CM-2040). Any AI behaviour that is not expressible as a legal order record is a defect in this document or in 13-command.md, and MUST be resolved in one of those two places rather than by adding a private path.
2. Vocabulary
Seven named objects, one named score, and three words that had to be chosen carefully because the specification set has run short of good ones.
| Term | What it is |
|---|---|
| the Staff | The whole shipped AI system, named for a general staff: a few people deciding what the army is for, and a much larger machinery executing it. |
| the Slate | The Staff's belief state — Contacts and their ages, the threat field, force accounting, opponent hypotheses. Everything the Staff thinks it knows. |
| the Atlas | A bounded graph of named places — cities, Landmarks, Region capitals, chokepoints, Depots, formation anchors — with abstract inter-node distances per movement class. The map, reduced to something a plan can be written on. AI-515 defines it; AI-517 owns the bound and what happens when it binds. |
| the Docket | The turn's candidate Operations: dated, located, valued things worth doing. |
| the Sandtable | A cheap abstract forward model that advances the game a few turns at formation granularity, to rank plans. It exists to order candidates, never to predict the game. |
| the Assay | The evaluation function. An integer score for a position, denominated so that 100 points = one city. |
| Operation | One of exactly twelve named things a force can be assigned to do (§6.5). |
| Rank | An AI difficulty level. Six: Cadet, Lieutenant, Captain, Colonel, General, Marshal. |
| Personality | A named, published set of parameter offsets from a tuned baseline. Seven shipped. |
| Decisive Point | A unit or small group the Staff hand-orders as an explicit Task, outside the Posture vocabulary, within a per-Rank budget. |
| Intent Record | The per-turn audit artifact of an AI seat's reasoning; sibling to 13-command.md CM-1940's CommandDecisionRecord, and on the advisory stream (CM-190), never the rules hash. |
AI-015 Rank, Personality, Operation, Decisive Point and Intent Record MUST have exactly one meaning across this specification set (00-overview.md OV-080), and this document MUST NOT reuse a word another document owns for a different thing. Specifically, an AI difficulty level MUST NOT be called a Tier (02-units-and-industry.md US-810 owns Tier for the Industry ladder), a Grade (11-combat.md owns Grade for veterancy and 01-game-rules.md GR-1260 owns fidelity grade), a Class (02-units-and-industry.md US-090 owns unit class), or a Band (14-victory.md VC-2200 owns Rumour/Report/Alarm).
Rationale: an AI difficulty called "Veteran" would be a word already carrying a combat meaning in the same sentence a player reads it in — "a Veteran AI moved a Veteran battalion". The naming cost is paid once here; the confusion would be paid in every support thread and every design discussion forever.
3. The seat contract
The two hard constraints, and the seven rules that make them checkable rather than promised.
3.1 The Staff is a seat, not a subsystem
AI-010 An AI position MUST be a seat: its decisions MUST enter the simulation as ordinary order records in the authoritative order log, in the same envelope as any human order (03-architecture.md AR-260, 10-turn-model.md TM-200, TM-210), submitted during that seat's Orders phase, and resolved by the same Cascade. The Staff MUST NOT be part of the simulation core, MUST NOT be reachable from rules code, and MUST NOT be able to alter any state except by an order the authority accepts.
§3 was taken whole on 2026-09-23 and now stands at 10/10
(packages/client/test/seat-contract.test.ts). Every clause in this section is a statement about the
shape of an AI position rather than about how well it plays, which is why they are held
structurally: a contract you can only observe by playing is a contract nobody can check.
AI-010’s three prohibitions, each as its own row. The orders: a Floor-played turn’s journal
entries are ordinary order records carrying a seat, indistinguishable in kind from a player’s.
Not part of the core, not reachable from rules code: the whole of packages/core/src is walked
and no module imports the Staff or the client — the one prohibition that could be broken by an import
nobody notices, silently, because everything would still work. No state except by an order: no AI
module names AR-030’s mutation verbs, so the Staff submits and does not apply.
AI-070 is its complement and the one easy to get backwards, which is why the requirement states it normatively — "without this stated normatively, someone will read CM-110 as binding on the AI". The rows hold both halves: the submitted kinds are the ordinary vocabulary, never a private one, and the Staff does have a seeded stream of its own that is seeded rather than ambient.
AI-030 is held where it actually bites: not that handicaps exist — §4 says they may — but that no Rank names one. A Rank carrying a handicap would be a resource advantage shipped as a difficulty setting, which is the clause’s last words. AI-080 is held by the selector: an unmeasured Rank’s line carries its description and its blind spots and no win rate. AI-050 rests on the AI-1410 gate landed the same day. AI-090’s second half — "structural work … MUST NOT be blocked on that consistency" — gets a row too, because it is as load-bearing as the first and is the reason the planner and the belief filter exist at all.
Mutation pass: 5 of 5 killed — a Rank naming a handicap, the selector claiming an unmeasured win
rate, full vision on by default, the Staff calling applyOrder itself, and a core module importing the
Staff.
Rationale: this is 00-overview.md OV-100 and it is the design's strongest structural advantage, obtained without being designed for the AI's benefit. Because orders are persistent conditional policies evaluated at activation (10-turn-model.md TM-280) rather than a stream of keystrokes, an AI and a human write in the same language, into the same log, evaluated by the same code. An AI that cannot express something has found a gap in the player's vocabulary — which is a bug report about 13-command.md, not about the AI.
AI-020 The fairness constraint — information. An AI seat MUST receive exactly the per-player fog-filtered player view a human seat receives, and nothing else. It MUST NOT read true world state for any tile, unit, city or installation its owner cannot presently see and has not previously observed (13-command.md CM-130 states the same rule for the command layer); it MUST NOT read any other seat's view, orders, Contact Register, Dispatch, or internal state; and it MUST NOT read any value from which those can be recovered.
AI-030 The fairness constraint — resources. No shipped preset MAY grant an AI position a production, economic, movement, vision, combat, supply or victory-side advantage of any kind (00-overview.md OV-110). Every handicap that exists is an explicitly configured, disclosed, recorded setup option under §4, applies identically to human seats, and is never part of a difficulty Rank.
AI-040 The Staff MUST NOT read the global activation order, any opponent's position in it, or any opponent's exact Initiative. 10-turn-model.md TM-380 and TM-1120 withhold these from every player; an AI seat is a player. The Staff MAY compute its own units' relative activation order, because TM-380 grants that to every seat, and MAY estimate opponents' Initiative from the published Initiative terms (TM-900) applied to its own Contacts.
Rationale: this is the single most seductive leak available, because the frozen order is sitting in memory and reading it is easier than estimating it. TM-1120 makes withholding it a rule about players, and the Staff is bound by rules about players.
AI-050 The plausibility constraint. No Rank MAY produce behaviour that reads as broken rather than as weak. The Constraint Floor of §6.9 MUST run at full quality at every Rank, and a Floor violation MUST be a build gate rather than a tuning metric (AI-620, AI-1410).
Rationale: an opponent making blunders no human would make is unsatisfying even when it is easy to beat. A beginner who loses a city because they forgot it existed feels bad about themselves; a beginner who watches the AI forget a city feels bad about the game.
AI-055 An AI seat MUST always be declared. Setup, the player list, the Watch, the replay
header and the game's Terms MUST identify every AI seat as an AI seat, with its Rank and every active
handicap. Concealment of which personality an AI seat is running is permitted as the Opponents: Anonymous option of AI-756; concealment of the existence of an AI seat MUST NOT be offered.
Rationale: concealment requires the AI to fake human behaviour it does not have, starting with commit timing — a seat that commits in 40 ms every turn is identifiable within three turns unless artificial latency is added, which is a lie with a code path in it. It also corrupts the terms of the contest, which the rest of this set treats as the thing that must be public (14-victory.md publishes the Ebb schedule and the Landmark locations on exactly this principle), and it makes the Debrief (AI-754) impossible to offer, because offering it would reveal the seat.
3.2 What the Staff may do that a human may do
AI-060 The Staff MUST be permitted to issue explicit Tasks and per-unit orders exactly as a human
may (13-command.md CM-070, CM-270). Restricting an AI seat to Postures and Sanctions MUST NOT be
treated as a fairness measure or an elegance: it would be a handicap the human does not carry, and
every command-layer exploit in §18 would gain a free multiplier. The number of units an AI seat
hand-orders outside the Posture vocabulary per turn MUST be a declared per-Rank budget P (AI-610),
and MUST be recorded in the Intent Record (AI-750).
Partially held 2026-09-28: explicit orders are built and pass a single journaled door; what this row still owes is recording them in the Intent Record. The order kinds stand at packages/ai/src/search.ts (Reassign, Swap, Reinforce, Abandon); every AI order enters through the same applyOrder seam a human's do, pinned by packages/client/test/ai-seat-contract.test.ts, which also slips one deliberately illegal order and gets it refused with a typed rejection; and the strength knob P is declared per Rank in packages/ai/src/ranks.json (decisivePoints 0 to 16), carried through rank.ts's types and validation but read by no behaviour yet. What waits: a count of hand orders on the Intent Record — packages/client/test/legibility.test.ts's describe 'AI-750 — the Intent Record' pins the record's fields, and it has none for that.
AI-070 The Staff's order-selection process MAY consume a dedicated seeded stream (AI-390). The orders it submits MUST be ordinary orders and MUST be executed by the same deterministic command layer as any player's. 13-command.md CM-110 forbids the command layer from drawing randomness; it does not bind a seat's choice of what to submit, any more than it binds a human's.
Rationale: without this stated normatively, someone will read CM-110 as binding on the AI, and the AI will ship as a pure strategy in a simultaneous-move game — which is the definition of exploitable (§9.3). CM-110's rationale is that a player must be able to predict their own screen; it says nothing about whether an opponent should be able to predict a plan.
3.3 Evidence, not assertion
AI-080 No difficulty Rank, personality, or strength claim MAY ship without measurement by the pipeline of §17 and the gates of §20 (00-overview.md OV-120). A Rank whose claimed strength has not been measured MUST NOT appear in the difficulty selector; §13.5 specifies what the selector shows instead.
AI-090 Version-pinned dependencies. This document's weights, thresholds and evaluation features are functions of the constants in 11-combat.md, 12-economy.md and 02-units-and-industry.md. The AI build MUST record the exact document versions and content hashes it was tuned against, and weight tuning (§17) MUST NOT begin until those three documents are mutually consistent. Structural work — the planner, the belief filter, the Cascade rollout, the Antagonist harness (§18) — MUST NOT be blocked on that consistency, because none of it depends on the constants.
Rationale: self-play tuning against an inconsistent rules set produces confident, well-tested, wrong weights, and the confidence is the dangerous part — a tuned vector carries a promotion-gate p-value and a release note, so it looks like knowledge. The first tuning cycle would be thrown away, and with it 14-victory.md's length-estimate calibration (VC-3210) and the Antagonist calibration done against the old numbers.
4. Handicaps, and why every one of them is published
4.1 The disclosure rule
AI-100 Every advantage or disadvantage in force for any position — AI or human — MUST be disclosed in three places: the setup screen at the moment it is chosen, the in-game rules review, and the replay header. Disclosure MUST name the position, the handicap, its magnitude and its direction. No shipped configuration MAY grant any position a resource, production, combat, movement, vision or information advantage that is not disclosed in all three (04-ui-ux.md renders these).
Rationale: the shallow reason is fairness. The real reason is epistemic — a player who beats a handicapped AI and does not know it has been told something false about their own skill, and the game has spent their evening teaching it. Everything else in this design is built to make the player's model of the world correct: 11-combat.md's exact preview, 13-command.md's decision records, 14-victory.md's published Ebb schedule, 12-economy.md's visible Integration. A hidden AI bonus is the same sin as an invisible production tax (12-economy.md EC-010, EC-040), committed against the player's model of themselves rather than of the economy. Two practical reasons follow it: an undisclosed advantage is indistinguishable from a bug, so every unexplained AI success becomes a support ticket; and in any game with more than one human it is a rigged contest.
AI-110 A difficulty preset MAY bundle a Rank with handicap values, but the setup UI MUST expand the bundle into its disclosed components before the game starts, and the recorded game configuration MUST carry the components rather than the bundle name.
4.2 What is offered
AI-105 The handicap catalogue MUST be exactly the following. Each applies to any seat, in either direction where a range is signed, and each is chosen at setup and never changed during a game.
| Handicap | Range | Why it is legitimate |
|---|---|---|
| Extra starting cities | ±3 | Visible from turn 1, positional, and reasonable-about |
| Starting Industry | 1–3 | 02-units-and-industry.md US-810 already makes Industry a per-city rating; starting higher is a legible head start |
| Industry cap | 1–4 | The same lever from the other end: a veteran capped at a lower ceiling against a newcomer at the full one |
| Opening stock | ±400 Works, ±200 Manpower, once | A finite, spendable, visible head start |
| Cadre discount | 50–200‰ of the escalating cost 12-economy.md §11 defines | Dials the anti-snowball brake directly and visibly |
| Start-position quality | seeded and labelled | Map generation already ranks starts; saying so is free |
| Foreknowledge | on/off | AI-150 |
| Full vision | on/off, off by default | AI-330 |
Rationale for opening stock rather than a production-rate multiplier: a rate multiplier distorts every derived number in 12-economy.md — the 25-to-35-turn payback target of EC-570, the Cadre cost curve, the Works-to-Manpower ratio bands of EC-290 that determine the forced army mix — so a handicapped game is quietly playing different economics and none of that document's worked reasoning holds. A one-time stock leaves the arithmetic intact and can be understood as "they started with an army", which is a sentence a player can hold.
AI-108 The following MUST NOT be offered as handicaps: production-rate multipliers, movement-rate bonuses, vision-radius bonuses, and per-seat exemptions from any brake of 12-economy.md EC-030. All four are invisible in play and all four corrupt a derived system.
AI-112 A combat handicap, where one is offered at all, MUST be an ordinary force modifier that is visible to the predictor and enumerated in the attack preview's line items alongside every other contribution. It MUST NOT be applied after prediction, and MUST NOT be applied to resolution without being applied to the preview.
Rationale: 11-combat.md CB-2280 makes the preview a pure function of the Clash Specification and CB-2690 puts the binary-search prediction against a brute-force oracle in CI. A hidden combat handicap makes the preview lie and turns that CI gate into a false alarm. A visible one costs nothing and, by appearing in the panel every single time, means nobody forgets it is on.
AI-150 Foreknowledge MUST be offered as a labelled handicap granting a seat revealed terrain and city locations at turn 0 and no unit information at any time. It MUST NOT reveal, then or later, any foreign unit, any Contact the seat has not earned, or any city's garrison.
Rationale: this is the handicap most players are actually asking for when they ask for full vision — an opponent that does not spend twenty turns exploring. It is legible, it is equivalent to very good early scouting, and it changes nothing about how the seat behaves once contact is made.
4.3 The rule that keeps the ladder honest
AI-115 No handicap preset MAY ship, and no handicap MAY be offered as a difficulty step above the top Rank, until the un-handicapped top Rank's win rate against the human ladder has been measured and recorded (AI-080, AI-730).
Rationale: the day the top Rank loses to good players, the instinct will be to reach for the handicap levers, and the metrics will go green because the subsidised AI wins. That converts a strength problem into a fairness problem and hides the strength problem permanently. This requirement is the only thing standing between "difficulty comes from quality of play" and a slow, well-intentioned death.
5. What the Staff sees
5.1 The view surface
AI-280 An AI seat MUST be served exactly the player view of 00-overview.md §9.6: revealed terrain, current vision, its own units and cities in full, its own economy, and foreign entities only as Contacts from its own Contact Register (01-game-rules.md GR-1200, GR-1210) at the fidelity GR-1260 admits. The view an AI seat receives MUST be produced by the same projection a fog-enforced client receives (05-multiplayer.md MP-250), and MUST NOT be a second, more permissive projection built for convenience.
AI-285 The AI's input stream MUST be byte-identical to the compacted per-player observable event stream a network client receives for the same seat. The Staff MUST NOT consume the raw per-activation event stream, the unfiltered Cascade trace, or any intermediate the core produces while resolving. CI MUST assert this equality over the benchmark corpus (AI-1400); a divergence MUST be a release blocker.
Rationale: the Staff runs in-process, so the raw per-activation stream is right there and consuming it is easier than consuming the compacted one. 10-turn-model.md already closes this leak for network clients because activation gaps correlate with Initiative bands and therefore leak enemy composition. An AI that reads the raw stream is cheating by construction, in a way that is invisible to inspection and that would make every strength measurement in this project meaningless. It is also the cheapest anti-cheat proof available: one byte comparison in CI.
Partially held 2026-09-28: the input-stream discipline is a standing checker rule and the view surface is pinned; what waits is the byte comparison itself, in CI. The Staff consumes the compacted per-player stream, never resolution intermediates — that is the raw-event-stream rule of scripts/check-ai-fairness.mjs, pinned by scripts/test/ai-fairness.test.ts — and the §5 view surface stands at packages/client/test/what-the-staff-sees.test.ts. What waits: this block's own closing clause, one byte comparison in CI over the benchmark corpus as the anti-cheat proof; no test makes that comparison today.
AI-290 The Staff's economy view MUST expose exactly the derived economic values the owning human would see — Works and the Yard, Manpower, Integration, Unrest, garrison requirement and points present (12-economy.md EC-1230), supply state, production queues and completion estimates — and nothing about another player's economy beyond what Contacts and the published Track bands (14-victory.md VC-2200) reveal.
AI-300 The Staff MUST NOT be able to observe pending or future random results. Combat randomness is
drawn by the core's seeded rules streams at resolution (03-architecture.md AR-370) and MUST NOT be
exposed through any view or any AI-facing call. The Staff MUST NOT hold the game root Seed (AI-195).
AI-310 The Staff MUST be given the complete active rules configuration, the complete unit set (02-units-and-industry.md US-700, US-380, US-470), the enabled Tracks with their thresholds, Vigils and published schedules (14-victory.md VC-350), and the Ebb schedule. There is no fog over rules, only over the map (02-units-and-industry.md US-930 makes the encyclopedia complete from turn one for every player), so withholding any of this from an AI seat would be a handicap, not a fairness measure.
AI-320 The Contact Register MUST be the Staff's only source of knowledge about foreign units, cities and installations. The Staff MUST NOT maintain a parallel record populated by any other means, MUST NOT retain a Contact the Register has decayed beyond what GR-1260 admits, and MUST NOT infer a foreign entity's attributes from any field the Register does not carry.
§5 was taken whole on 2026-09-23 and now stands at 8/8. Four clauses joined
scripts/check-ai-fairness.mjs, which already held AI-020, AI-040, AI-340 and AI-1225 the same way;
the rest are in packages/client/test/what-the-staff-sees.test.ts. Every clause in this section is a
negative, which is why it was worth taking as a section: a leak that never happens looks exactly like
a leak nobody checked for.
New in the script. AI-285 — no AI module may name the resolution intermediates, because the
requirement’s own rationale says the Staff "runs in-process, so the raw stream is right there".
AI-195 and AI-300 — no AI module imports Seed from the core. Scoped to an import from the core
rather than to the word, because portfolio.ts has its own Seed for AI-550’s strategic seeds and a
gate that could not tell those apart would be switched off inside a week, taking the real checks with
it. Both were confirmed to fire by planting a violation.
A correction to an existing reading, which is the finding half of this slice. The fairness
script’s tests carried a row asserting that a module other than mixing.ts importing Seed is no
finding at all, on the reading that the seed is "conventionally shown at setup", so "the prohibition
is on the mixing stream deriving from it — not on the AI knowing it". That is right about AI-1225 and
overreaches: AI-300 is explicit that "the Staff MUST NOT hold the game root Seed (AI-195)", and
AI-195 gives the reason — what a plugin receives must be an AI-private one-way derived value "such that
a plugin holding it cannot reconstruct the Seed, any rules stream’s state, or any other seat’s
streams". A seat holding the root seed can reconstruct the streams whether or not it derives its salt
from them. The two rules stack rather than compete, and the row now says so.
In the test file. AI-280: the closer calls playerView and no convenience projection stands
beside it — the thing to look for, because it would never announce itself; it would be a helper with a
plausible name. A mutant that adds one is killed. AI-290: the seat’s own city carries its Works,
Yard, Integration, Unrest and Population, and a foreign city carries none of them — both directions,
because withholding is the opposite failure and a test that only checked for absences would pass on a
Staff that had been starved. AI-310: the unit set the Staff reads is STANDARD_UNIT_SET, a module
constant, so "there is no fog over rules" holds by construction rather than by a filter somebody has to
keep permissive.
AI-320 is held by an allow-list, and a mutation pass is why. The first draft named the fields a
parallel record might use — contacts, foreignUnits, sightings — and a mutant adding one under a
different name survived. A deny-list can only catch the leak somebody already thought of, which is
the wrong way round for a clause that says "populated by any other means". The seat memory’s keys
are now checked against the seven the requirements put there, so a new field needs a requirement behind
it; the row also asserts the aggregate integral is present, so it cannot pass by the memory being empty.
AI-330 A labelled option AI Full Vision MAY be offered per AI position, granting that
position an unfogged view. Where offered it MUST be labelled as a cheat, disclosed under AI-100,
recorded in the game configuration and therefore visible in replays, and off by default in every
shipped preset (00-overview.md OV-110). It MUST NOT be bundled into any difficulty Rank, and the
setup screen MUST state that it changes what kind of opponent the seat is, not how well it plays.
Rationale, and a recommendation this document makes against its own option: full vision does not make the Staff harder, it makes it a different and much less interesting opponent. An AI that knows where everything is does not scout, does not screen, does not misjudge, and never plans against a Contact that turns out to be stale — so every behaviour that makes it readable (§16) disappears. It also violates AI-050's shared-vocabulary premise outright: no player can express "march confidently through fog to the exact tile where the thing you cannot see is standing." OV-110 requires the option to exist as a labelled, disclosed, recorded, off-by-default handicap, and this requirement provides it; AI-150's Foreknowledge is what should be recommended in the UI instead, and Open question 3 asks whether the option should be withdrawn entirely at the next overview revision.
AI-340 AI instances MUST be information-isolated from one another: no shared memory, no messaging channel, no shared belief state, and no host call that reveals another AI seat's internals. Two AI seats of the same personality MUST NOT coordinate implicitly. This game has no diplomacy (00-overview.md §4), and an emergent AI coalition would be a diplomacy system nobody chose.
6. The Staff — the layered architecture
6.1 The shape, and the bet it is making
┌──────────────────────────────────────────────┐
player view ────►│ L0 PERCEPTION builds the Slate, │
(fog-filtered) │ the Atlas, the Docket │
└───────────────────┬──────────────────────────┘
│ belief · places · candidate Operations
┌───────────────────▼──────────────────────────┐
│ L1 STRATEGY Track portfolio, │ exhaustive
│ Empire Stance, │ over ~8
│ theatre & industry budget │ candidates
└───────────────────┬──────────────────────────┘
│ objective weights · force budget
┌───────────────────▼──────────────────────────┐
│ L2 OPERATIONS assign formations to │ portfolio
│ Operations; set Postures, │ + local
│ Sanctions, Postings, │ search over
│ Requisitions, Tasks │ the Sandtable
└───────────────────┬──────────────────────────┘
│ plan → compiled order delta → Rehearsal
┌───────────────────▼──────────────────────────┐
│ L3 EXECUTION Postures & Sanctions, │ no search
│ in the core, identical │
│ for every seat (CM-100) │
└──────────────────────────────────────────────┘
══════════ THE CONSTRAINT FLOOR ══════════════════════════════════
garrison sufficiency · Cadre routing · Depot chains · transport lift
· air endurance · production continuity · no idle unit · no stranded
formation · retreat degree. FULL QUALITY AT EVERY RANK. NEVER DEGRADED.
The bet, stated before the machinery so it can be judged on its own:
This design does not try to out-search a human. It tries to out-administer one, never make a mistake a competent player would not make, and reason about the same handful of objectives a human reasons about — in the same vocabulary the human uses.
That is a smaller claim than "superhuman 4X AI" and a much more achievable one, for a reason specific to this game. Of the eight ways opponents in this genre visibly lose, seven are not search problems: leaving cities ungarrisoned is arithmetic; stranding transports and running aircraft out of endurance is constraint satisfaction; idle cities and misdirected reinforcement is scheduling; walking a column into a garrison it could have inferred is belief; fighting at bad odds is a solved arithmetic problem because 11-combat.md CB-2250 gives an exact predictor; and never noticing that an opponent is three turns from winning is impossible when 14-victory.md publishes it (VC-2280). Only two are search: picking the wrong axis of advance, and mistiming a concentration. The plan is to be perfect at the seven and spend the whole compute budget on the two. §21 is honest about how strong that makes us and about the one place it could ship weak.
AI-500 The Staff MUST be organised as four layers over one Constraint Floor, with the responsibilities, inputs, outputs and methods of the table below. A layer MUST NOT reach past the layer beneath it, and no layer MUST re-derive a quantity an owning specification computes in closed form.
Held 2026-09-23 as packages/client/test/staff-layers.test.ts. "A layer MUST NOT reach past the
layer beneath it" is a statement about the import graph, and nothing checked it: the Staff is
thirty-nine modules in one flat directory, where an L2 module importing an L0 internal would work
perfectly, produce correct answers, and stay invisible until somebody tried to change L0. Every module
now carries a layer, and a new one with no layer fails the check — which is what makes adding one a
decision.
The check found a real thing on its first run, and it was a type in the wrong file. Seven modules
across L0, L1, L2 and the Floor imported MapShape from the Floor, so every one of them read as
reaching into it. The type is the map’s dimensions — "metadata every seat has, not privileged state" —
and it now lives in arith.ts with the rest of the shared arithmetic, re-exported from floor.ts so
no caller had to change. That is the whole value of a structural check: the violation was on paper
rather than in behaviour, and nothing else would ever have reported it.
The first draft also got the Floor's own position wrong, and the requirement's own words are the correction: "four layers over one Constraint Floor". The Floor is beneath, so every layer may reach down into it, and AI-560 gives the Floor’s own input as "Slate, own state" — which is L0. Reading the Floor as a sibling reported five perfectly correct imports as violations.
Four edges go the wrong way and are admitted by name, so a new one fails while these do not. Two
are the design’s own inversion of control — floor.ts imports staff.ts and assay.ts because the
Floor runs the seat and consults the layers, attaching their output to FloorPlan.drawn and
FloorPlan.assay; and floor.ts imports absence.ts because absence is a condition on the seat
rather than on the plan.
And two are findings, recorded for the spec owner. prospect.ts (L1) depends on sandtable.ts
(L2) for the Position type, and gunline.ts (Floor) depends on assign.ts (L2) for Force. Both
are dependencies on a shape rather than on behaviour, and in both cases the honest fix is the one
already applied to MapShape: the shape belongs below both its users. Recorded rather than moved,
because one type move is a slice and three is a refactor.
| Layer | Input | Output | Method | Cadence |
|---|---|---|---|---|
| L0 Perception | the player view (AI-280) | the Slate (§8), the Atlas (AI-515), the Docket (AI-525) | incremental update, no search | every turn |
| L1 Strategy | Slate, Docket, Track state | Empire Stance, per-Track weights w_T, theatre force budget, Industry and Cadre policy |
exhaustive over ~8 surviving candidates | every k turns (AI-610), plus triggers (AI-545) |
| L2 Operations | L1 output, Docket, Formations | an assignment of formations to Operations, plus Postures, Sanctions, Postings, Requisitions, Consignments and Decisive-Point Tasks | portfolio of K seeds + steepest-ascent local search, scored through the Sandtable (AI-565) |
every turn, subject to commitment lock C |
| L3 Execution | the committed order records | unit behaviour | no search. 13-command.md CM-100's command layer, in the core | every activation |
| Constraint Floor | Slate, own state | obligations that are solved, not scored | exact algorithms (AI-560) | every turn, every Rank |
AI-505 L0 MUST be a pure function of the player view and the Staff's own persisted advisory memory (AI-460). It MUST NOT consume the rules PRNG streams, and MUST NOT depend on wall-clock time, host locale, iteration order of a hash container, or object identity (03-architecture.md AR-090, AR-100, AR-110).
AI-575 L3 MUST NOT exist as AI code. Unit-level behaviour under a Posture — how a Screen picks its tile, how a Survey chooses where to look, when a unit withdraws, what happens when a plan meets an obstruction — MUST be the core's, identically for every seat (13-command.md CM-100). The Staff MUST NOT ship a movement engine, a pathing policy, or a per-unit tactical search of its own.
Rationale: this is the cleanest structural win in the design and it costs nothing. A unit ordered
Screen by the Staff runs the identical CM-460 candidate-scoring loop as a unit ordered Screen by a
human. There is no AI movement engine to keep in sync with the rules, no class of bug where the AI's
units path differently, and no way for an AI's units to execute better than a human's — which is
CM-050's fairness contract, obtained by construction rather than promised.
6.2 How search stays tractable — counted honestly
This is the design's central structural advantage and it deserves arithmetic rather than a gesture.
The raw action space is not merely large; it is the wrong space. On a 1,000 × 1,000 map with ~300
units of our own, a unit with movement 4 on mixed terrain reaches roughly 25–45 tiles; add up to four
attack targets and four Stances (10-turn-model.md TM-630) and call it ~160 distinct per-unit
commitments. Jointly that is 160^300 ≈ 10^661. Restricted to the ~30 units a human actually re-orders
in a turn it is still 160^30 ≈ 10^66. Neither number is interesting, because under Orders and the
Cascade the raw space is not the player's action space at all. A player does not choose destinations;
they choose order policies (13-command.md CM-340: a Posture is a policy evaluated at activation, never
a recorded sequence of steps). The raw space is what the engine explores while executing a policy.
The command space, counted properly. The seat's actual action space per turn is a change to some subset of its command objects:
| Object | Reference midgame | Late large |
|---|---|---|
| Groups (13-command.md CM-860, ≤ 5 Formations each) | 4 | 10 |
| Formations (≤ 8 units each) | 15 | 50 |
| Unattached units — garrisons, Cadre, Depots, scouts | ~25 | ~90 |
| Postings (CM-1130) | 5 | 12 |
| Cities — Consignment, Template, Industry | 20 | 60 |
(Reference midgame: 200 × 200 map, 48 cities, 4 players, turn ~120, ~90 units — 14-victory.md's reference configuration, used throughout so the numbers are comparable to that document's.)
Per command object: six Postures (three for a Group) × ~8 feasible geometries × 3 Sanction presets,
less illegal combinations ≈ 60 legal assignments. Jointly, 60^19 ≈ 6 × 10^33 at reference midgame
and 60^60 ≈ 5 × 10^106 late-large.
AI-510 The specification MUST NOT claim that the command abstraction makes the action space
searchable, and no budget, schedule or Rank definition MAY be justified on that claim. The abstraction
takes 10^661 to 10^106. Both are infinite for our purposes.
Rationale: the branching-factor argument is seductive, correct-sounding, and about to be used to justify a small planner budget. Postures collapse the branching factor of issuing orders. They do not collapse the branching factor of planning, because good plans live above the vocabulary (§6.7). An AI that searches only over Posture assignments is searching a small space of mostly-bad plans, and it will be fast and weak.
Built 2026-09-22 as packages/client/src/ai/compile.ts. Nothing issues what it produces, and that
is a coordination decision rather than a design one: the Constraint Floor’s orders are replayed in the
AR-960 corpus, so the turn an order actually leaves the Staff is the turn every expected hash in the
repository moves. That is an event to schedule with the other agents working in this tree, not one to
start unilaterally. This is the whole of the compilation that does not need the window; what remains is
small.
The loss is reported, which is the half of this clause a compiler most easily fails silently. The
requirement does not merely permit the loss, it prescribes the treatment — "the compilation MUST be
treated as lossy, and AI-585’s Rehearsal is what detects the loss" — so Compiled carries a list
of what could not be said, in the register prospect.ts reports the Assay terms a projection cannot
compute. A thing reported as absent is a thing nobody can pretend was considered; a synchrony group
dropped quietly is a planner whose whole richness went nowhere with nobody the wiser. All five of this
clause’s own list are reported — timing, synchrony, refusal of a named city, fixing, graph
properties — and each only where the plan actually contains the shape that would need it, because a
report that fires unconditionally is noise and a report nobody reads is the silent drop by another
route. A sixth is reported that the clause does not list and the Posting system forces: an Operation
against a region has no anchor, because CM-1130’s is a tile or a Formation — so nothing reinforces
it, which is a consequence rather than a missing field.
AI-559’s Posting weights, and the one thing that clause leaves open. "Proportional to committed
force × Operation value, normalised to [1, 10]" gives the proportion and the range and not the
mapping between them. Ruled: normalise against the heaviest objective, so the largest pull is
always 10. CM-1190 spends a deficit in the ratio of the weights, so a plan with one objective must
send everything to it; normalising against a fixed scale would make a seat with one modest Operation
reinforce it at weight 2 and hold the rest back for nothing. The floor is the requirement’s own and
is load-bearing: 1, not 0. A weight of zero is an Operation the round-robin never reaches — and the
plan named it, and may even have committed force to it — so a range beginning at one is the clause
saying that the smallest real objective still receives.
The leftovers needed no translation at all, which is worth noting because it is the exception that
shows the rule: AI-555’s placeLeftovers chooses Reserve, Screen or Survey and never Idle (CM-540),
and each of those three is already one of CM-330’s six Postures. The one part of the plan that survives
the compilation intact is the part that was written in the target language to begin with.
What the abstraction actually buys is four things, each of which is what makes the search work:
- Moves become meaningful, so local search becomes possible. In a raw action space one move is "a
unit steps one tile", which changes the position by almost nothing, so hill-climbing is noise. In
posture-and-objective space one move is "III Corps stops screening the river and marches on Ash
Ford", which changes six units' behaviour for five turns by a computable amount. A space of
10^106with a smooth, sparse-interaction neighbourhood is a space local search does well in; a space of10^661with a meaningless neighbourhood is not. - Operations are mostly separable. Two formations' assignments interact only when they compete for the same objective or share a theatre, so a change can be scored as an Assay delta over a handful of terms rather than a full re-evaluation (AI-1050).
- The bottom layer is free. L3 is the game's code, not the AI's (AI-575).
- The geometry parameter is bounded by the Atlas, not by the map. This is the mechanism that turns an unbounded parameter into a finite one, and it is specified next.
AI-515 The Atlas. The Staff MUST NOT search raw geometry. All Posture geometry parameters MUST
be selected from an Atlas: an abstract place graph, built at map load from the player view and
maintained incrementally, whose regions are exactly the map's Regions (14-victory.md VC-1030, count M
fixed at generation by VC-1031). Nodes MUST be drawn from: every known city; every Landmark
(14-victory.md VC-1080, public from turn 1); every Region capital (VC-1030); every derived chokepoint;
every friendly Depot; and each Formation's current anchor. Edges MUST carry an abstract integer path
cost per movement class, obtained from the hierarchical chunk-boundary pathfinder of 03-architecture.md
AR-750, and MUST be invalidated on terrain change, city capture, Depot change, or a supply-state change.
A node-to-node distance query MUST answer at a declared integer step cost (AI-405); nothing in this
document requires a dense all-pairs distance table, and an implementation MAY answer from any
oracle — hierarchical over AR-750's chunk graph, landmark-based, or cached on demand — that meets that
cost and is deterministic.
Built 2026-09-22 as packages/client/src/ai/oracle.ts, and it is the clause’s own escape hatch that
made it possible: AR-750’s hierarchical pathfinder is unbuilt, and the core’s stepCost takes a
WorldState and a UnitState the Staff must not have (AI-020). So the oracle is the one the view
can answer — a budgeted Dijkstra over view.explored at 01-game-rules.md GR-290’s real per-terrain
entry costs, through the engine’s own entryCostFor.
CM-140 costs the dark, and it is derived rather than written down. "For routing, an unrevealed tile MUST be treated as passable at an assumed movement cost equal to the cheapest cost the unit’s movement class pays on any terrain it may enter" — so the assumed cost is computed from the loaded terrain set, and a custom set (GR-370) moves it without touching the oracle. It is floored above zero: free dark would let a search cross a continent through it.
The query is bounded and says when it stopped. This clause requires an answer "at a declared integer step cost (AI-405)", so the search carries a budget and reports exhaustion as no answer rather than as a distance. A caller reading "too far to look" as "unreachable" is making a different mistake from one reading it the other way, so the result distinguishes them.
And building it found that every ETA the Staff had ever computed was wrong. AI-555’s eta divided
a straight-line tile count by the formation’s movementAllowance, as though the allowance were
tiles a turn. It is movement points. A unit with an allowance of 4 entering cost-4 Plains makes one
tile a turn, not four — so every ETA was three to four times too optimistic, AI-555’s timeliness was
correspondingly too generous, and distant objectives cleared the fit floor on an arithmetic error.
The correction is visible in the fixtures and worth recording as a behavioural change rather than a
bug fix: a city nine tiles away that the assignment used to commit to is now a nine-turn march scoring
below AI-555’s fit < 50 floor, and the Staff declines it. That is the requirement’s own arithmetic
applied to a correct distance, and it makes the Staff act locally at Cadet depth — which is what
AI-610’s ladder should produce. Whether the 80-per-turn timeliness slope is still the right one now
that it is fed a true ETA is a question for §21’s tuning run, and it now has the corrected number to
ask it with.
This retires the straight-line readings in three files at once — staff.ts’s eta and distance,
and it is the same graph table.ts’s Sandtable edges and §8.6’s frontier will read. Four of AI-525’s
ten missing Operation kinds wait on that frontier or on a route model, which is why this was worth
building before more kinds.
A Screen line is therefore "the frontage between node A and node B"; a March is "to node N"; a Survey
is "region R"; a Garrison is "city C". Of the map's M Regions — at most 24 under VC-1031 — only ~8 are
feasible for any one formation within the planning horizon, which is where the "~8 feasible geometries"
in the count above comes from.
AI-517 The Atlas is bounded, and the bound is derived rather than fixed here. The Atlas MUST carry a node bound. That bound MUST be derived per game from the active map tier (03-architecture.md AR-690) against two budgets this document already owns — the Rank's step budget (AI-405, AI-610), which Atlas maintenance and distance queries are charged against, and the working-set obligation of AI-435 — and MUST be reported alongside the budget-compliance measurements of AI-710 rather than asserted as a constant in this requirement.
The following MUST be retained as nodes whatever the bound is: every Landmark, every Region capital,
every derived chokepoint, every friendly Depot, every Formation anchor, and every known city within
horizon × pace of any friendly Formation — the same reach AI-525 generates Seize candidates
from, so every city the Docket can propose is nameable by a plan. Where the node set would still
exceed the bound, the reduction MUST be deterministic and replayable: remaining known cities MUST be
coalesced onto their nearest retained node in ascending order of city value, ties broken by lowest tile
index (matching AR-750's tie-break); a coalesced city MUST remain addressable as its retained node plus
its own city id, so that no city becomes silently unnameable; and the node bound in force together with
the count of coalesced places MUST appear in the Intent Record (AI-750).
Rationale, and why there is no constant here. The outcome the Atlas exists for is that the geometry
parameter is finite and meaningful — §6.2's fourth item — and that outcome is bought by the
abstraction, not by any particular ceiling. A fixed small ceiling fails on this project's own fixture:
03-architecture.md AR-760's reference game has 2,000 cities and 8 players, so "every known city" alone
exceeds a few hundred nodes by an order of magnitude once the map is explored, and a requirement that
mandates both the inclusion rule and a small fixed cap is unsatisfiable rather than strict — it forces
an implementer to invent an eviction rule the specification never states, differently each time, in the
one document whose whole argument is that planning happens in the space of named places. A ceiling of
that shape also drags a dense all-pairs distance table behind it, because a small fixed n is what
makes n² affordable and n² is then what keeps n small; AI-515 releases the table and this
requirement releases the constant, so a finer Atlas is priced against the step and working-set budgets
rather than forbidden by arithmetic nobody wrote down. The retention list is the part that must not be
released: it is what keeps the reduction from costing the Staff the ability to name the board it is
planning on, and publishing the coalesced count is what makes a reduction that went too far visible in
the Debrief (AI-754) rather than felt as an opponent that inexplicably ignores half the map.
AI-520 Chokepoints MUST be derived deterministically and cached: on the chunk graph, a tile is a chokepoint if making its 8-neighbourhood impassable increases the abstract distance between two chunk regions by more than 3×. Where 01-game-rules.md's map generator can supply chokepoints directly, the Staff MUST use those instead of deriving them.
AI-522 Through fog, the Atlas MUST use 13-command.md CM-140's routing prior verbatim: an unrevealed tile is passable at the cheapest cost the movement class pays, and contains no enemy. This prior MUST NOT reach threat evaluation (AI-1100). The Staff plans optimistically and discovers the truth, exactly as a human does.
AI-525 The Docket. Candidate Operation generation MUST be rule-based, bounded, deterministic and hard-capped per kind, so the Docket cannot grow with map size or contact count. The caps MUST be:
| Kind | Generated from | Cap |
|---|---|---|
Seize |
enemy and neutral cities within horizon × pace of any formation |
64 |
Relieve |
own cities where the 2-turn threat field exceeds garrison value | 32 |
Screen |
Atlas edges crossing a believed approach corridor | 24 |
Deny |
any opponent in the Alarm band — sites are published with live vision (14-victory.md VC-2280, VC-2290) | 16 |
Claim |
open and announced Warrants; upcoming Tallies | 8 |
Raid, Interdict |
enemy Depots, Cadre Contacts, high-Industry rear cities | 24 |
Survey |
fog-frontier regions with unrevealed area | 12 |
Escort, Establish, Integrate, Besiege |
lift routes; Depot sites; un-integrated cities; fortified places | 24 |
| Total before filtering | ≤ 204 |
The Docket MUST then be filtered to at most 40 entries by value × 1000 / (cost + 1),
cross-multiplied rather than divided, ties broken by lowest Atlas node index. That filtered Docket is
what L2 searches.
AI-527 An Operation MUST carry exactly: a kind from the closed twelve-value vocabulary
(Seize, Relieve, Screen, Raid, Interdict, Survey, Escort, Establish, Integrate,
Besiege, Deny, Claim); a target that is an Atlas node, an Atlas edge, or a region; an integer
requiredForce computed by the exact predictor (11-combat.md CB-2250) against believed defence; a frontage
(AI-570); a (earliest, latest) turn window; an integer value and an integer risk, both Assay
deltas; and a template — the Posture and Sanction tuple to project onto whoever is assigned. Adding a
thirteenth kind MUST be a specification change here, not an implementation choice.
Generation built 2026-09-22 (client/src/ai/generate.ts), for two of the twelve kinds — Seize
and Relieve, the game’s own loop. docket.ts had held the caps and the filter since 2026-09-11
with no generator at all, so every Operation this repository had assigned (AI-555) or searched over
(AI-557) was one a test built. The other ten are named in the module with what each waits on, because
a Docket that silently offered two kinds would be a Staff that had stopped considering ten — and this
requirement’s own argument is that the composition is what the caps protect.
requiredForce is the open question this answered, and it was answered by measuring rather than by
choosing. The predictor (CB-2250) says what is the probability for this attacker; the Docket needs
the inverse, how much force does this need. Two things shaped the built answer:
- It searches over the attacker’s strength, not over a count of attackers. The first attempt
added supporters until the probability cleared the threshold and the engine refused it by name:
11-combat.md CB-370 admits a lead and at most two supporters, so a count is a three-rung ladder
and cannot express "this city needs four times what I have". The built inversion scales a reference
attacker and reports
value × multiplier, which is the model AI-555’ssufficiencyratio already assumes when it divides one force value by another. - It is exact, because exact turned out to be cheap. Measured before designing, in case the
honest way was unaffordable: one
capturePermilleis 0.069 ms, one inversion is 12 probes / 0.855 ms, so the filtered Docket of forty costs 34 ms and the whole capped 204 costs 174 ms. There is therefore no cheaper reading to rule on. The probability is steep in force — at one defender, 20 attack reads 0, 30 reads 550, 40 reads 1,000 — so the threshold is an edge rather than a slope and the inversion lands on a real number.
Generation is deliberately not spent against AI-405’s step budget: that budget is the search’s
(AI-550 spends it inside LOCAL_SEARCH), and this requirement bounds the Docket by composition and
count instead — which is what keeps it fixed-size on a 1,000 × 1,000 map. A target no force could
take is reported at the ceiling rather than as impossible, because the assignment then prices it
as insufficient and the search abandons it: that is a decision about a plan, not a fact about a
target, and this requirement asks the field for the second.
One reading worth stating for the next reader of the works field: the city lists the generator is
handed are the view’s own, so works is 11-combat.md CB-1570’s fortification level in [0, 4] and
not the city’s economic Works. The first draft passed the second and the combat model refused it by
name — which is the good outcome, and the reason the field is documented at the boundary rather than
left for a reader to infer.
Corrected 2026-09-12: this requirement cited AI-1030 for "the exact predictor" and AI-1030 is
the Assay — the weighted integer sum of §7, which is the evaluation function and not a combat
predictor at all. The exact predictor is 11-combat.md CB-2250, which §6.1 names ("fighting at bad
odds is a solved arithmetic problem because 11-combat.md CB-2250 gives an exact predictor"), which
the AI interface exposes to a plugin as the predict host import (AI-200, with CB-2290
forbidding a cheaper approximation), and which §22's contract table names a third time. Three places in this document had
it right and this one had it wrong, and check-spec-refs passed both ways because AI-1030 exists.
This is the fourth wrong-but-resolvable citation found this month — after AI-190 for AI-750, §10.4
for §11.4, and 12-economy.md EC-810 for EC-1530 — and the pattern in all four is the same: a
plausible neighbour in the same document, cited from memory while writing a clause about something
else. The mechanical checker can only ask whether a target exists. What catches these is building
the thing and discovering the cited requirement does not answer the question — here, that an
evaluation function has nothing to say about how much force takes a city. Built at
client/src/ai/docket.ts, where the predictor is an injected value precisely because CB-2290 makes
it the core's and forbids a second one.
Rationale for Deny and Claim existing at all: they are the §6.2 argument made concrete. The Alarm
hands every player the address of the fight, so the correct response is not subtle — generate Deny
Operations against those exact sites at a value that dominates the Docket, because the alternative is
losing. Warrants and Tallies are announced with dates, so an AI that arrives at the Iron Gate on the
Tally turn, having started its march twelve turns earlier, reads as genuinely intelligent and is an ETA
lookup. The single hardest thing in strategic AI is deciding what matters, and 14-victory.md publishes
a calendar of it.
6.3 L1 — Strategy
AI-530 L1 MUST decide exactly three things every k turns and MUST evaluate them exhaustively
over the surviving candidates, because the space is small enough to:
(a) Track portfolio. An integer weight w_T per enabled Track, feeding Assay features A4 and A5.
The default schedule MUST key on the published visibility bands (14-victory.md VC-2200), so that the
Staff's strategic attention sharpens exactly when the game says it should and a watching player can
predict when it will:
| Highest band any player holds on that Track | w_T |
|---|---|
| Nobody above Rumour | 400‰ |
| Any player in Report | 1000‰ |
| Any player in Alarm, or any Vigil running | 2000‰ |
(b) Empire Stance. Exactly five values, which are 12-economy.md's central dilemma expressed as one enum:
| Stance | Effect on the Docket and the weights |
|---|---|
| Expand | Seize value ×1400‰; accepts un-integrated cities; Cadre purchases deferred |
| Consolidate | Integrate and Establish ×1600‰; Seize ×600‰; Cadre purchases prioritised |
| Strike | one theatre takes 70% of uncommitted force; Deny and Raid ×1500‰; cautious Sanction presets dropped |
| Defend | Relieve ×1800‰; Screen ×1400‰; Leashes halved; reserve fraction raised to 30% |
| Endure | play for Tenure: hold, garrison, deny, refuse engagements below 850‰ |
(c) Industry and Cadre policy, both of which MUST be computed as arithmetic rather than searched, using the owning documents' own closed forms: 02-units-and-industry.md's Industry payback for the upgrade decision, and 12-economy.md §11's Cadre cost curve for the next-Cadre decision. The Staff performs the sums those documents already state; it MUST NOT restate or approximate them.
no-test-needed: L1 does not exist as code in this tree — no module produces a Track-portfolio weight or an Empire Stance for it to schedule — so its k-turn decision cadence has nothing to fire against; staff-layers.test.ts's header records the clause at its own requirement rather than citing it there. It becomes testable when L1 lands.
AI-533 Cadre assignment MUST be scheduled by rate, not by total value:
priority = integratedValueGain × 1000 / (turnsToIntegrate + travelTurns), cross-multiplied.
Rationale: greedy-by-value sends every Cadre to a Metropolis and spends the longest integration time per Cadre, when in the same window several smaller cities would have been fully digested. This one expression probably beats any hand-tuned weight this subsystem will otherwise receive, and it is the kind of thing self-play discovers slowly and a designer discovers instantly.
Built 2026-09-22, as the valuation of the Integrate Operations generate.ts now offers — the fourth
of AI-525’s twelve kinds, and the first about the seat’s own empire rather than about something to
acquire.
The numerator comes from a term the Assay already charges. A13 prices occupation drag at
(100 − integration) × baseWorks on every city outside the seat’s homeland — the term measured at
AI-570 as the one that scales with the prize, so that a captured Metropolis is a liability until it is
digested. Nothing in the Docket had ever proposed doing anything about it. An Integrate is worth
the drag it retires, which makes it commensurate with a Seize by construction rather than by a
constant somebody tuned.
And this clause caught the first draft doing exactly what it forbids. That draft used the gain as
the value outright, and a test row had been written asserting that a Metropolis is worth more to fix
than a Town — which is greedy-by-value, and is the mistake this rationale names in as many words. The
rate is the rule: EC-930’s own cadreRateFor supplies the denominator, and it is 10 a turn in a
Town, 6 in a City and 2 in a Metropolis, so the largest prize is five times the slowest to digest.
Measured after the correction, a City at 20% integration scores 642 against a Metropolis at 10%
scoring 400: the smaller city is finished while the larger one is still starting, and a Cadre freed
sooner is a Cadre somewhere else.
travelTurns is zero at generation and that is a split rather than an omission: the Docket has no
formations, so there is nothing to measure a journey from. The term arrives at assignment, where
AI-555’s timeliness discounts an Operation by exactly eta − earliest — the same quantity, applied by
the stage that knows it.
AI-535 Strategic variables MUST carry hysteresis. Retasking a command object MUST cost the Assay's A27 term, and changing Empire Stance MUST additionally cost a flat penalty, so that a Stance flips only when the alternative is decisively better rather than marginally. An opponent whose army reverses direction every turn is both weaker and infuriating to play against.
Partially held 2026-09-28: the retask cost is declared, consumed and round-tripped; the Stance-change penalty waits on a layer that does not exist yet. The A27 term stands in packages/ai/src/manifest.ts (a27.perRetask with its range), pinned by packages/ai/test/assay.test.ts, and its hysteresis baseline — retaskedLastTurn in packages/ai/src/memory.ts — survives save and reload under the AI-460 pin at packages/client/test/ai-memory-save.test.ts. What waits: the flat penalty for changing Empire Stance; no module produces an Empire Stance yet (AI-530's L1 is unbuilt), so that half of the row has no subject.
AI-540 The Staff MUST condition its strategy on the enabled Tracks and on nothing else: it MUST NOT carry a hard-coded objective, and adding a Track to 14-victory.md's catalogue MUST give the Staff that Track's progress function and threshold for free through the uniform Track shape (VC-350). Under a Track keyed on a Seat, the Staff protects its own and threatens others'; under a threshold Track it races, denies and times against the Vigil clock; under a Tenure Track it plays the Ebb schedule backwards from the Horizon. Multiple simultaneous Tracks MUST be scored jointly.
AI-545 Re-planning MUST be trigger-driven, not unconditional. A full L1+L2 re-plan MUST run
when any of the following fires, and a cheap refinement otherwise: a city taken or lost; any Track
entering Report or Alarm, or a Vigil starting or breaking; a Formation destroyed or under strength; the
unaccounted-force estimate moving by ≥ 15%; a Warrant opening or a Tally six turns out; an Operation
completing or becoming Impossible (13-command.md CM-1340); the Assay falling by more than the Rank's
gradient threshold since the last plan; and unconditionally every 8 turns as an anti-staleness
floor.
Rationale: the triggers already exist and are free. 13-command.md CM-1610 requires the core to emit Attention Events deterministically, per player, from that player's fog-filtered view (CM-1850), which is a deterministic, fog-honest change detector the Staff can read at no cost. It does not depend on them for behaviour — CM-1630 forbids that, and AI-1310 restates the prohibition on borrowing the Dispatch's ranking — it uses them as a signal that something changed.
6.4 L2 — Operations: the search
AI-550 L2 MUST use portfolio search with steepest-ascent local search, in this order, with deterministic tie-breaks throughout:
for each of K strategic seeds: -- K by Rank (AI-610)
plan ← GREEDY_ASSIGN(Docket, Formations, seed weights)
plan ← LOCAL_SEARCH(plan, step budget / K)
for each opponent hypothesis H (1..Y by Rank):
outcome[plan][H] ← SANDTABLE(plan, H, depth D) → Assay at leaf
score[plan] ← RISK_WEIGHT(outcome[plan][*]) -- AI-1200
commit argmax score, ties by lowest plan seed index -- then AI-1220 mixing
Strategic seeds MUST be qualitatively different emphasis vectors over the same Docket — Seize-heavy, Defend-heavy, Deny-heavy, Economy-heavy, Tempo-heavy, and the L1 Stance's own weighting — not perturbations of one vector. Rationale: a portfolio of qualitatively different plans, each locally optimised, beats one plan optimised harder, because the seeds converge to genuinely different local optima. This is portfolio search and it is the right technique here for the same reason it is right in real-time strategy research: the space is too big to search but candidate strategies are few and enumerable.
Built 2026-09-22 as packages/client/src/ai/portfolio.ts, and wired into the Staff: the search that
had been running once per turn now runs K times from K different starting points.
A seed is a weight per Operation kind, applied to the Docket’s own value. That is the smallest
thing that makes this clause’s six emphases expressible, and it is enough: assign orders by value
descending and scores fit on it, so a Seize-heavy seed and a Defend-heavy seed genuinely disagree
about which Operations matter and start from different assignments. A measured consequence worth
stating, because it is invisible: the emphasis reaches the plan entirely through the greedy
assignment. Neither movesFor nor applyTo reads an Operation’s value, so the local search is
value-blind and simply optimises whatever it was started from — which is exactly "each locally
optimised" and is correct, but it means a seed’s whole influence is its starting point.
The budget is divided K ways, and that is load-bearing. "LOCAL_SEARCH(plan, step budget / K)" —
a portfolio that gave every seed the whole budget would make a Marshal twelve times more expensive than
AI-405’s bound says it is, and the bound would mean nothing. beginSearch therefore takes an optional
share; absent, a search outside a portfolio gets the whole of its Rank’s budget, which is what every
caller before this one got.
And a gap between this clause and AI-610, found by building the loop and ruled here. This clause
names six emphases. AI-610’s seeds knob runs 2, 3, 4, 6, 8, 12 — so from Colonel upward the
ladder asks for more seeds than there are distinct ones to be. It cannot be closed by inventing six
more, because this clause forbids exactly that: six further vectors over the same twelve Operation
kinds would be perturbations or they would be nothing.
Ruled: K is bounded by the emphases that exist. A Colonel and a Marshal both weigh all six, and
the ladder still separates them on the knobs it also raises — passes and depth, which it does. What a
seventh emphasis would have to be, stated so that the knob is reachable rather than merely capped: a
genuinely different axis rather than another vector on the same one — a per-theatre weighting once
§8.6’s frontier carries theatres, or a per-opponent one once AI-1200’s hypotheses exist. Neither does
yet, and until one does the knob’s upper half is a number with nothing behind it.
The inner loop of this clause’s pseudocode — the fan over Y opponent hypotheses and AI-1200’s
RISK_WEIGHT over their outcomes — is not built. Y is 1 for Cadet and Lieutenant, so a
single-hypothesis portfolio is the specification’s own value at those Ranks rather than a stub; above
them it is 2 or 3 and this does not yet fan out, because a second hypothesis needs a model of what the
opponent would do and nothing has one. AI-570’s exchange now produces the outcome band RISK_WEIGHT
would read, so that half of it is waiting rather than missing.
AI-555 Greedy assignment MUST use integer, cross-multiplied fitness:
sufficiency = clamp(forceValue(F) × 1000 / requiredForce(O), 0, 1500)
timeliness = clamp(1000 − 80 × max(0, eta(F,O) − window.earliest(O)), 100, 1000)
fit(F,O) = value(O) × sufficiency × timeliness / 1000000
sufficiency MUST be capped at 1500‰ so that over-committing to an easy objective scores no better
than committing adequately — this is the anti-doom-stack term, and it works with AI-570. Assignment
MUST proceed by Docket value descending, ties by lowest Atlas node index; MUST skip any pairing with
fit < 200; MUST allow a second formation onto an under-strength Operation only up to that
Operation's frontage; and MUST place leftover formations as Reserve, then Screen on the nearest
uncovered frontage, then Survey — never Idle (13-command.md CM-540).
Two further readings, 2026-09-22, from running §6.4 against a real board.
forceValue(F) is a fighting value, not a scoreboard value. This clause weighs forceValue(F)
against requiredForce(O) and does not define the first; AI-1032 defines cv(u) for the Assay,
pinned to worksPaid so that a healthy army is worth the same there as on 14-victory.md’s
scoreboard — and VC-540 reads a granted starting unit as worksPaid: 0. Taken into the planner
that means a seat’s whole opening army has a force value of zero, every sufficiency is zero, and
nothing can be assigned to anything until the seat has built something it paid for. So a plan
prices a unit at what its class costs to field (classBase) where nothing was paid for it, through
the same strength-and-disorder expression. The Assay keeps cv(u), because there the agreement with
VC-540 is the point; the planner asks a different question and needs a different answer.
A unit under no Formation is an echelon of one. Wiring this clause up found that an AI seat forms no Formations at all — the Constraint Floor issues unit-level orders — so the assignment had objectives and no subjects and every plan came back empty. Having the Staff issue formation orders is AI-559’s commit and moves every hash in the replay corpus, so the reading is 13-command.md CM-200’s instead: the command stack is three levels and a unit is the lowest of them, so a loose unit is an echelon and this clause assigns echelons. Nothing about it issues an order, and the corpus and the reference hash are unchanged — which is how it is checked.
The id space carries which is which: a positive id is a Formation’s and a negative one is the unit
-id - 1 planning as itself, offset so that unit 0 does not read as Formation 0. AI-559’s commit
will need that distinction to know what order to issue; a plan that had flattened it would have to
guess. One consequence is stated rather than left to be found: this clause’s "lowest formation id"
tie-break therefore prefers a lone unit to a Formation at equal delta, which is arbitrary but total.
Amended 2026-09-22, by wiring §6.4 to a real position (client/src/ai/staff.ts). The fit < 200
floor cannot be right on AI-1000’s scale, and is ruled at 50. A city is 100 points (the Assay’s
a1.cityPoints), so a city taken by an adequate force arriving exactly on time scores
100 × 1000 × 1000 / 1000000 = 100, and the most any single city can ever score is
104 × 1500 × 1000 / 1000000 = 156 — under the stated floor. A Staff holding to 200 refuses every
Seize the Docket can generate, which is the one thing this game is about. Fifty is derived rather
than chosen: it admits a plain city taken adequately and arriving a couple of turns late
(100 × 1000 × 840 / 1000000 = 84) and still refuses a pairing whose target is worth less than a
third of a city at full sufficiency and perfect timing. The floor’s purpose — do not make a pairing
that is not worth making — is unchanged; only the number moves, and it moves because the two
requirements were written on different scales.
A second correction came with it, and it is the Docket’s rather than this clause’s: a Seize’s
value is the Assay pair, not the single term. Taking a city that somebody holds moves A1 (this seat
gains it) and A5/A6 (its owner loses it), so the Operation is worth 2 × cityPoints + works; a
neutral city is the single term, because nobody loses it. Recorded at AI-527 where the generator
reads it.
And a finding worth keeping rather than fixing: against a fortified city (works 4) the
inversion of CB-2250 reports a requiredForce around twenty times a single reference attacker, so no
formation a young seat holds clears the fit floor and the assignment refuses the Operation. That
is correct behaviour rather than a defect — a Staff with no siege machinery should not march on a
fortress — and it names the next kind worth generating: Besiege, which is one of the ten AI-525’s
generator does not offer yet.
Built 2026-09-17…2026-09-22 (client/src/ai/assign.ts), and three things the building settled.
First, the fitness divides once. value × sufficiency × timeliness / 1000000 is one expression
and it is implemented as one: a per-term divide floors three times, and there are reachable pairings
where that turns a fit of 2 into a fit of 1 — value 2 against a 1499‰ sufficiency one turn late is
the row. This is the same ruling §7 recorded for the Assay (accumulate, then divide once), and it is
recorded again here because the expression is stated per term in the pseudocode above.
Second, "a second formation" reads as "a further formation", bounded by frontage. Taken as a
hard count of two, the frontage half of the sentence does nothing — an Operation with frontage for
five formations would still take two, and AI-570 states its bound in members (φ(n) × stackLimit)
precisely so the number of formations follows from the ground rather than from a number in this
clause. So reinforcement continues while the Operation is under strength and the engagement bound
still admits the members. The doom-stack pressure is then held by two independent things, which is
the intent: the 1500‰ sufficiency cap makes over-committing worth nothing, and the frontage makes it
impossible.
Third, the ruling this clause needed: what bounds the Reserve. The sentence gives an order — Reserve, then Screen, then Survey — and no size, and an unbounded Reserve makes the other two arms dead: every leftover would be a Reserve and no formation would ever screen a frontage. The bound is derived from the one requirement that reads the Reserve, AI-557: the local search may "reinforce an under-strength Operation from the reserve", so a Reserve is worth exactly the reinforcement the plan could still want. Formations are held back, strongest first, until they cover the plan’s total shortfall; everything past that screens the nearest uncovered frontage, and then surveys. Strongest first because the shortfall is a force value, so the fewest formations that cover it is the placement that frees the most for the frontage — and because a Reserve of many small formations is one the frontage bound may refuse to spend anyway.
What is built is the assignment alone. AI-525’s candidate generation is unbuilt, so the Docket
this assigns from is one a test builds rather than one a game generates — the same shape docket.ts
itself landed in, and deliberate for the same reason: the arithmetic here is exactly specified and
can be proven now, while generation needs requiredForce from CB-2250’s predictor and frontage
from the Atlas, for twelve kinds. AI-557’s local search, AI-565’s Sandtable and AI-550’s portfolio
loop are the rest of §6.4 and are not built.
AI-557 Local search MUST use exactly these five move types, scored by Assay delta rather than by full Assay, with ties broken by lowest formation id then lowest Operation id: reassign one formation to a different feasible Operation; swap two formations' Operations; reinforce an under-strength Operation from the reserve; abandon an Operation, freeing its formations; escalate or de-escalate one assignment's Sanction preset. Estimated neighbourhood per pass: ~335 moves at reference midgame, ~1,905 late-large.
Built 2026-09-22 (client/src/ai/search.ts), over AI-555’s assignment, and the first caller
client/src/ai/budget.ts has ever had — that module’s beginSearch/applyMove/commit were
written for this and had stood since 2026-09-11 with only their own test. Four things it settled.
First, the neighbourhood is larger than the estimate above, and the difference is a reading.
Measured at this requirement’s own reference shape — 19 command objects against a full 40-Operation
Docket — a pass offers 933 moves: 741 reassignments, 162 swaps, 10 abandons and 20 Sanction steps
over 10 running Operations; late-large is 4,170 against the stated ~1,905. The gap is not
arithmetic. A reassignment is offered here to every Operation in the Docket whose frontage admits
the formation, and ~335 is close to what restricting it to the Operations the plan is already
running would give (19 × 9 + 162 + 10 + 20 = 363). The wider reading is the one built, because a
local search that could only move formations between Operations it was already running could never
start one — Abandon would have no counterpart, and a Docket entry no formation happened to be
assigned to at greedy time could never be reached. The estimates should be re-derived against the
built neighbourhood or the narrower reading stated; the numbers above are what a pass actually costs.
Second, the tie-break is ordered, so a move naming no formation cannot win a tie against one that
does. "Lowest formation id then lowest Operation id" leaves Abandon and Sanction — which name an
Operation and no formation — without the first key, and the reading taken is that a missing first key
does not beat a present one. The comparator is exported (moveBeats) for the reason docket.ts
exports scoreBeats: the neighbourhood is generated in an order that already agrees with the key, so
a row that only ran the search could not tell the rule from the generation order.
Third, a Reinforce is offered where AI-555’s fit floor refused an assignment, and that asymmetry
is deliberate. After a greedy pass an Operation is short for one of three reasons — its frontage
filled, the formations ran out, or the rest scored below fit < 200 — and in that last case the
formations sit in the Reserve. The local search scores a move by its Assay delta, so a bad
reinforcement is refused by the scorer rather than by a threshold this stage does not own; applying
the assignment’s floor here would make a formation unreachable to the search because it was
unattractive to the greedy.
*Fourth, AI-610’s L is honoured as well as Σ: the search takes at most the Rank’s pass count,
so a Cadet gets one pass however much budget is left. Without it the ladder collapses into a single
knob — a Cadet with a generous oracle would search as deep as a Marshal and only the step budget
would separate them. Every considered move costs one assay-delta, checked before it is scored, so a
pass that runs out mid-neighbourhood stops there with the best move it has found (AI-405).*
The scorer is injected. A move’s Assay delta is a claim about the position the plan would reach,
which is the Sandtable’s (AI-565) and is unbuilt — so this takes a delta oracle, proves the
machinery against it, and leaves the oracle to the slice that builds one. AI-550’s portfolio loop and
AI-525’s candidate generation are likewise not built.
Half of that is now answered (2026-09-22): packages/client/src/ai/prospect.ts scores a Sandtable
Position and prospectDelta subtracts two of them. AI-550’s portfolio loop and the wiring of this
search to it are still not built — the wiring is held back for a reason given below, not for want of
a turn.
One scale, and it must be the Assay’s own. Every weight is read from ASSAY_MANIFEST through the
same parameterOf the Assay uses, and the result is the Assay’s own AssayResult. This is not
tidiness: AI-527 prices an Operation in Assay points, this clause compares a move’s delta against that
price, and a second set of weights — even a well-chosen one — would leave the two incomparable while
looking as though they agreed. A finding that follows from it, and the more useful half: every weight
in the shipped manifest is 1000, because §21’s tuning run has not happened, so points equals raw
everywhere and a scorer that ignored the weight entirely would pass every ordinary test. The weighting
is therefore held against a deliberately altered manifest, so that the tuning run is not the thing
that discovers the weights were never connected.
Twenty-three of AI-1030’s thirty features are unreachable from a position, and they are reported
rather than scored as zero. The Assay already carries the vocabulary — a Limit with "the one thing it
waits on, in a sentence a reader can act on" — and it is exactly right here. The difference is not
cosmetic: a term reported as zero is one the search will trade away for a point elsewhere, and a term
reported as a limit is one nobody can pretend was considered. Seven contribute — A1, A7, A10, A13, A14,
A15, A19 — and three of those seven are additionally partial: A14 and A15 are priced in a
formation’s aggregate fighting value where the Assay sums cv(u) and a Works cost over units (the
two agree on a bought army and part company on a granted one, per this section’s own earlier ruling),
and A19 cannot price a city’s Industry, so a threatened Metropolis and a threatened Town cost the same.
A15 also reaches CB-1650’s four combat tiers from 12-economy.md’s three supply states, mapped at the
ends — Isolated is Cut, Extended is Strained, and Supplied spans both unpenalised tiers, which is why
the mapping loses nothing there.
A13’s "foreign cities" takes two tests, and the Sandtable must carry both. The Assay arrived at this by measurement and it must not be rediscovered: EC-1450’s claimant list is the economy’s own notion of whose homeland a city is, and it is empty on a founding city, so a claimants-only test makes a seat an occupier of its own capital — measured, a turn-one seat came out foreign to all four of its own cities, and only Integration sitting at 100 kept the charge at zero. So AI-565’s city record carries the founding owner beside the claimant list, and A13 asks both. EC-1440’s capture row keeps reading the claimant list alone, because there the economy’s notion is the right one; they are two different questions that happen to share a word.
Why the wiring is held back. Running this search on this scorer needs a position whose foreign
cities carry a garrison value, because a projection in which nothing can be taken produces a delta of
zero for every move and a search that finds nothing. The number to hand is AI-527’s requiredForce,
and using it would reinstate precisely the circularity AI-565’s build just ruled out — requiredForce
is the Docket’s own price for the target, so every Operation the Docket thought affordable would
succeed by construction. The honest source is the believed defence AI-525’s generation already derives
from AI-1100’s belief, exposed as a query of its own. That is the next increment, and it is a
requirement about where a number comes from rather than a convenience.
Built the same day. The search now runs on this scorer inside the Staff’s own drawUp, which is the
first time AI-557’s machinery has run on anything but an injected oracle. The readings follow.
The believed defence is a query of its own (AI-1132), and it reads the undecayed value. A foreign
city’s garrison in a projection is what the seat believes is there, summed over the Contacts
standing on its tile — never AI-527’s requiredForce, for the reason recorded above. Two further
things fall out of building it. First, AI-1110’s staleness curve is the right weighting for a threat
and the wrong one for a garrison: a city does not get easier to take because nobody has looked at
it lately, and reading the decayed value would make exactly that mistake in the optimistic direction.
Second, a city carrying no Contact at all is not an empty city — it answers AI-1132’s class-typical
garrison at full strength, the same fallback the generation already makes, because a zero there would
make every unscouted city a free capture and send the search at the furthest one on the map.
The horizon is the plan’s own longest ETA, and both sides of a delta are scored at the same one. AI-565 fixes no depth, and a constant would be wrong in both directions: too short and a plan is judged before its Operations land, so every plan looks like the turn it starts; too long and the economy dominates, so the Staff ranks plans by whose captured cities have had longest to integrate. The turn the last Operation resolves is the answer to both. And it must be the horizon of the plan before the move: letting each side choose its own would let a move win by being measured over a longer run — sending a formation somewhere distant lengthens that plan’s horizon, the extra turns buy the seat’s own cities more Integration, and the search would learn to waste a formation on the most distant objective it could find. Floored at one so a plan of immediate arrivals is still stepped, and capped, because a formation that cannot arrive reports AI-565’s horizon value rather than a number.
A foreign city is read at the floor of everything the fog does not admit, with one exception. No
Industry beyond US-820’s own minimum of 1 (which baseWorks enforces — level 0 is rejected outright,
so "no Industry built" means level one), no expansion, no war damage. The exception is Integration,
read at 100 rather than 0, and it is the reading that claims least rather than the one that looks
least: a city its owner holds is integrated for them, and reading it as unintegrated would understate
what the opponent gets from it and — since A13 charges drag on what is not integrated — make taking one
look like relieving the opponent of a burden.
A known artifact, named rather than left to be met. The Sandtable’s flip is positional: a
formation standing on a foreign city with more value than its garrison takes it, whether or not the
plan sent it there. In a real game that position cannot arise — a force on an enemy city’s tile has
already taken it — but it can be constructed, and a fixture that constructs it makes AI-557’s
Abandon move look like it changes nothing. The Sandtable has no notion of a formation declining to
attack; AI-570’s aggregate exchange is where one would live if it is ever wanted.
Two measurements, recorded because they are the sort of thing the tuning run of §21 will move. A seat sees no foreign cities at all at turn 1, so every test written against a created game’s view was vacuous on the foreign half — deleting that whole branch passed all of them — and the foreign readings are held against constructed views instead. And, on two equidistant cities one formation could take either of: the Metropolis beats the Town at every horizon from the turn of capture out to twenty, by between 8 and 12 points, never reversing. That was measured rather than assumed, and the opposite was expected: A13’s drag scales with the prize, so a large city taken and not held is a real liability, and EC-1020 makes a Metropolis wait longer at capture Unrest 70 before EC-930 will integrate it at all. It is simply not enough to outrun A1 and A10 together at these weights — at these weights being the whole caveat.
AI-559 The plan's commit MUST also write, in the same order delta: Posting anchors and weights
(13-command.md CM-1130) proportional to committed force × Operation value, normalised to [1, 10], so
that CM-1190's deficit round-robin delivers the reinforcement ratio without further AI involvement;
Requisition rows derived from the Operations assigned to each theatre; Consignments for cities
near a theatre; and one Doctrine per role archetype (CM-770, CM-790) so that newly produced units
arrive correctly ordered without a decision (CM-080).
Rationale: this is a genuinely large amount of AI work that simply does not have to exist. The Staff sets a ratio and the core delivers it, using the same scheduler a human's empire uses.
6.5 The Sandtable
AI-565 The Sandtable MUST be an abstract forward model at formation and city granularity — never per-tile, never per-unit — carrying per formation: Atlas node or edge with a progress fraction, aggregate attack and defence value, pace, supply state, current Operation, ETA; per city: owner, base Works, Integration, Unrest, garrison value, incoming threat; per player: Works and Manpower rates, Standing, Tenure, per-Track progress. It MUST step by: move along Atlas routes; resolve contested nodes and edges as one aggregate exchange (AI-570); flip cities and apply 12-economy.md's capture consequences exactly; advance the economy using 12-economy.md's closed forms; and recompute Track progress, Standing and Tenure at an abstract Seal using 14-victory.md's own functions.
Blocked, 2026-09-22, and recorded here so that the next reader finds a named dependency rather than an
unexplained gap. This step cannot be built as written: computeStanding and trackProgress take a
GameState and a WorldState, not a hypothetical position, so "14-victory.md’s own functions" cannot
be called on a Sandtable at all. §22’s own integration table already records the debt — 14-victory.md
owes the AI "per-Track progress and threshold functions evaluable at a hypothetical state, which
VC-350’s uniform shape already promises the AI evaluator". Until that surface exists the Seal can only
be built by restating the victory layer at a coarser granularity, and unlike EC-1440’s capture rows
there would be nothing to pin the restatement against, because the engine’s own function takes the
state this model does not have. The four steps that could be built are built; A4, A5 and A6 name this
as what they wait on.
AI-567 The Sandtable MUST NOT consume the rules PRNG. Aggregate exchanges MUST be resolved at fixed draws — the median for the mean case and the extremes for the risk band — so that the Sandtable is a deterministic function of the plan and the hypothesis.
Rationale: the Sandtable's economy is not an approximation at all — it is exact, because 12-economy.md's Works, Levy, Integration, Unrest and Cadre-cost functions are already integer and already closed-form (EC-060 forbids that document the PRNG entirely). That is a real and unusual advantage: most abstract forward models in this genre are wrong about the economy on the first step.
Built 2026-09-22 as packages/client/src/ai/sandtable.ts, in the first of its increments: the three
record shapes, and three of the five steps — move, flip with the capture consequences, and advance
the economy. AI-570’s aggregate exchange and the abstract Seal are not built, and are named in the
module rather than left to be discovered. The readings the build made follow; each is binding.
The economy step is exact, and the rationale above is the reason rather than the claim. It is
exact because 12-economy.md’s formulas were already written as pure functions over a view rather
than over the game state, so the Sandtable calls integrationDelta, unrestTerms, baseWorks and
effectiveWorks themselves. There is no second implementation of the economy inside the forward
model to drift from the first, and a row holds each step against a direct call of the same function.
Three terms are held false and each is a reading rather than an omission: the Emergency Levy’s +25 and
the razing shock’s +30 resolve at the Ledger’s own steps and are false in a projection under held
conditions — which is exactly what EC-1820’s "current delta" means and what the player-facing
Digestion forecast already does — and the Academy aura is an EC-460 adjacency this model has no board
to evaluate. A Cadre’s presence is held false for the same reason and is the one that costs something:
EC-930’s step 4 gives a Cadred city its class rate and an uncadred one a flat 1 below Unrest 30, so
the model systematically under-states how fast a well-administered empire integrates. It is
recorded here rather than papered over, and it is the first thing to fix if AI-1490’s plan-ranking
measurement finds the Sandtable ranking economic plans badly.
The model MUST NOT score a plan using the plan’s own estimate. The obvious way to decide whether
an Operation succeeds in projection is to compare the committed force against the Operation’s
requiredForce, because AI-555’s assignment has computed both already. It is circular:
requiredForce is the Docket’s own price for the target, so every Operation the Docket thought
affordable would succeed by construction, each plan would project its own optimism back at itself,
and the ranking signal AI-572 asks for would carry no information whatever. An arrival is therefore
decided against the position’s numbers — the city’s garrison value, which AI-565 lists — and never
against the plan’s. Until AI-570 is built the decision is the sign of attack − garrison, with a tie
left to the defender: that is the exchange’s outcome collapsed to its sign, and it costs neither side
value, which is the single largest way this increment flatters an attacker.
The progress fraction is an integer in thousandths. "A progress fraction" reads as a float, and
a float is the wrong answer here for AI-1010’s reason: a projection that depends on floating-point
association is a projection two hosts can disagree about. Progress runs 0..1000 along its edge and
every advance is one intMulDivFloor. A consequence, found by a test rather than by reasoning: the
ETA MUST be computed in those same units, by dividing the progress still to cover by the progress one
turn covers. The first draft converted back to tiles and floored, which dropped the part-tile at the
end of an edge and reported an arrival a turn before the move would deliver one — and AI-555 times a
plan on the ETA, so the model would have ranked a late Operation as punctual.
A city’s record is this clause’s six fields and its identity, kept apart. AI-565 names six things
that vary per city, and a seventh varying field would be a quantity the Staff began scoring on without
a requirement saying it may — the discipline AI-527 is built with. But the closed forms read more than
six, and the rest is not state the model steps: it is the city’s class, traits, homeland claimants and
loyalty, fixed for the length of a projection. Those sit in a separate record that no step ever writes.
Base Works is the case that makes the split worth drawing: it is a closed form of five of those fixed
fields, and this clause lists it as carried state, so it is computed once through the engine’s own
baseWorks by whoever builds the position and read from the record thereafter. Computing it inside the
step would put one number in two places.
The capture consequences are the half that exists at this granularity, and they are pinned. The
engine splits a capture in two — settleCityCapture writes the owner, applyEconomyCapture applies
the consequences — and at formation-and-city granularity the flip and its consequences are one step.
Of the consequences, this model applies EC-1440’s two Integration rows, EC-1020’s Unrest with the
Citadel bump, and EC-630’s one-phase stop, which survives because EC-930’s step 1 reads it. The rest
of applyEconomyCapture — the emptied yard, the destroyed queue and Awaiting Levy hulls, the reset
Consignment and surplus rule, the zeroed naturalisation, the war-damage step — is below this
granularity and has nowhere to land. A restatement of a rule is a copy waiting to go stale, so a
named row runs both against one real created game and holds them equal on every field the Sandtable
carries. A later change to EC-1440 fails that row rather than quietly leaving the Staff reasoning
from the old rule.
The Atlas carries no routes and no pace model, so movement is straight-line at the formation’s
slowest pace, taken from the caller — the same reading AI-555’s adapter already makes for its eta,
so that the two are replaced together when the Atlas grows routes.
AI-570 Frontage limitation. An aggregate exchange MUST engage at most φ(n) × stackLimit
members, where φ(n) is the Operation target's frontage — the number of tiles from which the
target may legally be engaged — derived once from the map and stored on the Atlas node. Support
contributions MUST use the same aggregate shape 11-combat.md defines, so the Sandtable's arithmetic
and the real thing's do not diverge.
Rationale: the obvious way to fight two formations is to sum attack and defence and resolve one exchange, and that is wrong in a specific and dangerous way — it makes concentration free, so the AI learns to build a doom-stack, which is precisely the behaviour 11-combat.md and 13-command.md are designed to prevent. Frontage does two jobs at once: it makes the model honest about concentration, and it gives the Staff a reason to value chokepoints that falls out of the arithmetic rather than being asserted as a heuristic. An AI that defends a defile because the defile makes its numbers count is reasoning, not following a rule.
Built 2026-09-22, as exchangeStep and flipStep in packages/client/src/ai/sandtable.ts. With it,
four of AI-565’s five steps exist and only the abstract Seal is absent.
It is 11-combat.md’s own preview, because the only way to keep two arithmetics from diverging is
to have one. This clause requires that "the Sandtable’s arithmetic and the real thing’s do not
diverge", and the engine already exports the evaluation of a ClashSpec. And that function turns out
to be exactly what AI-567 describes: CB-2160 resolves the exchange at r = 0, 500 and 999 and bands
the results, which is "the median for the mean case and the extremes for the risk band" in the
predictor’s own words. The Sandtable needs no PRNG here not because it avoids one but because the
predictor the requirement points at never had one. The median reading decides the outcome and the band
is carried for AI-527’s risk to read.
Losses MUST land on the records the next exchange reads, or the model has no sieges in it. Writing
a loss only onto the aggregate value leaves both CombatantRecords untouched, so a second turn of the
same fight resolves identically to the first, and a third, for ever: a city that holds once holds
against that force permanently, however long it is invested. Wearing the strength down is what makes a
fight the attacker loses on turn one a fight it can win on turn four — and it is equally what makes a
fight the attacker cannot afford visible, because its own strength falls in the same arithmetic.
Frontage is half of what stops a doom-stack; CB-370 and CB-650 are the other half. Measured on a frontage wide enough to admit everybody: the second echelon raises the attack’s win probability, the third raises it again, and the fourth, fifth and sixth change it by nothing at all — because an Assault admits a lead and at most two supporters (CB-370) and the stack bonus is capped (CB-650). Concentration saturates inside the combat rules, before the frontage bound is even reached, and a Staff reading its own arithmetic has a reason to stop sending echelons that is not a rule anybody told it.
The terrain range, measured on one echelon against AI-1132’s class-typical garrison, in win per-mille: Open 1000, Hills 856, Marsh 602, Forest 553, Urban 514, Mountain 117. That is this clause’s rationale coming true — "an AI that defends a defile because the defile makes its numbers count is reasoning, not following a rule" — and it is a number that falls out of the engine’s own predictor rather than a heuristic anybody asserted.
A consequence for AI-557’s horizon, which had to change. Before this step existed, an Operation
resolved the instant a strong enough force reached its target, so arrival was completion and a plan
was rightly judged at its longest ETA. An exchange takes turns. Judged at the turn of arrival, every
assault therefore scores as a pure loss — the attacker has paid for one round of fighting and taken
nothing — and the search would learn never to attack at all. The horizon is now the longest arrival
plus the width of AI-527’s own (earliest, latest) window, which is the Docket’s own statement of
how long an Operation is allowed to take, rather than a constant somebody picked.
What the exchange costs, measured — and a correction to the first reading of it. With fighting costing both sides value, the question is whether conquest still pays. It does, across the whole force range the roster can produce, and the numbers are the delta of abandoning an Operation that would have succeeded, projected over the plan's own window:
| echelon value | abandon a Metropolis | abandon a Town |
|---|---|---|
| 15 (a Militia) | −109 | −101 |
| 93 (heavy armour) | −98 | −90 |
| 186 | −85 | −77 |
| 372 | −59 | −51 |
| 900 | +14 | +22 |
The first version of this note reported the last row as the finding — that no capture pays for the
fighting it costs, because a city is +100 on AI-1000's scale while thirteen per cent of a 900-value
formation is −117. The arithmetic was right and the 900 was not: it was a number invented for a test
fixture. combatValue divides by a14.divisor of 40,000, so a unit is worth its Works cost over
four — heavy armour 93, armour 39, engineer 33, infantry 15, militia 10 — and no echelon the game can
field approaches 900. The corrected reading is the more useful one: the crossover at which this scorer
would begin preferring to preserve force over taking cities sits near 600 per echelon, which is
outside the reachable range, and A1 and A14 are commensurate after all at the shipped weights. It is
still a number §21's tuning run should know, because both weights that decide it are things the run
moves.
AI-572 The Sandtable's output MUST be treated as a ranking signal only. The Staff MUST NOT
carry a Sandtable projection forward as knowledge between turns; plans MUST be re-derived from the
current view at every trigger (AI-545), and the only thing carried between turns MUST be the switching
cost baseline (A27) and the commitment lock C.
Rationale, stated plainly because it is this design's largest single risk: the Sandtable has no terrain below the Atlas, no per-unit losses, no reactions, no interruptions, and no knowledge of what the opponent actually ordered. It will be wrong. §21.3's plan-ranking measurement (AI-1490) decides whether it is wrong in a way that still ranks correctly, and §21.3 names the fallback if it is not.
6.6 The Rehearsal
AI-585 After compiling a plan to orders and before committing them, the Staff MUST rehearse: run the compiled order set forward through a forked Cascade against the chosen opponent hypothesis and evaluate the result from its own seat's view. If the rehearsal shows the compilation did not express the plan — the concentration did not arrive together, the Screen did not cover the corridor, a Sanction declined the fight the plan required — the Staff MUST choose a different plan or a different compilation rather than commit a compiled shadow of a plan it liked better.
AI-587 The Rehearsal's opponent model MUST read only the acting seat's knowledge view. Where a hypothesis requires simulating an opponent's decisions, the opponent's modelled view MUST be constructed from the Staff's own Slate: the opponent is assumed to see what the Staff's units are within the vision radius of the Staff's own Contacts on them, to hold the force the Staff's force-accounting estimate says they hold (AI-1120), and to know the same published information. The Staff MUST NOT read true state to build an opponent model, at any Rank, under any option except AI-330.
Rationale: this is the one place where cheating would be invisible and catastrophic to trust — modelling an opponent's decisions requires modelling their view, and their view is not ours to read. AI-1400's fog-leak gate is what keeps a hundred small opponent-modelling changes honest over a project's lifetime.
Rationale for rehearsing at all: the Orders-and-Cascade model makes a turn a pure function of committed orders and frozen Initiative (10-turn-model.md TM-040, TM-150), so the Staff can simulate its own turn before committing it. Most strategy AIs cannot do this. The cost estimate this rests on — that a copy-on-write fork with activation memoisation brings a marginal Cascade to a small fraction of a full one — is the single load-bearing unmeasured assumption in this document and is Open question 5.
Built 2026-09-22 as packages/client/src/ai/rehearse.ts — the half of this clause that needs no
simulation, which turns out to be most of what it names.
All three named failures are checkable from the plan and its compilation alone. Each is a
disagreement between what the plan intended and what the orders say, not a fact about how a turn would
unfold: an arrival is the ETA AI-555 already computed, coverage is a Screen’s tile against the frontage
that needed covering, and a declined fight is CM-750’s preset and CM-740’s Posture row read through the
engine’s own applyPreset. This file reports what the vocabulary carried wrongly; AI-580’s
lost list reports what it cannot carry at all. Two different questions, and only the first is a
defect.
And the third check immediately found a live one. The Docket’s own Seize template was
March/Balanced. CM-740’s March row is engagement: Defend, seize: Undefended, and CM-750’s
Balanced row is the defaults unchanged — so a Seize as generated would have walked to the city,
declined to attack it, and stopped at the garrison. Generation priced the capture, assignment
committed force to it, AI-570’s exchange resolved a fight for it and AI-550’s portfolio ranked plans by
it; the orders at the end forbade it. Nothing else in the chain could have caught that, because every
other stage reasons about the plan and this is the only one that reads what the plan compiled to.
The fix is the clause’s own instruction — "choose a different plan or a different compilation" —
and it is not a choice between imperfect options: Aggressive on March gives seize: Free and a
Favourable engagement at 600‰, which is exactly AI-525’s TAKE_THRESHOLD. CM-750’s boldest row and
the bar at which the Docket calls a target takeable are the same number, so a Seize is expressible at
precisely the price it was generated at. Held as a test against the engine’s tables rather than against
the literal, so a change to either number fails loudly instead of making the Staff decline again.
A distinction the third check needs, and getting it wrong would make the Rehearsal cry wolf. It
fires only for the kinds that require the seat to start a fight — Seize, Besiege, Raid,
Interdict. A Relieve is generated under a Hold Posture at a Careful Sanction, which engages only
when attacked, and that is the right order rather than a declined fight: the plan is to be there and
hold, and the opponent supplies the attack. An Integrate garrisons a city the seat already owns and a
Survey exists precisely to avoid a fight. The Operations whose caution is the point must not be
reported for being cautious.
What is not built, and why it is not stubbed. This clause also says to "run the compiled order set
forward through a forked Cascade". The Staff has no state to fork — AI-020 gives it a PlayerView
and nothing else, which is what check-ai-fairness exists to enforce. AI-587 says how that gap must be
closed when somebody closes it: the opponent’s modelled view "MUST be constructed from the Staff’s own
Slate", never from true state, "at any Rank, under any option except AI-330". So a hypothetical-state
builder is a slice of its own with a fog-leak gate of its own, and reaching for real state inside this
file would be exactly the failure AI-587’s rationale calls "the one place where cheating would be
invisible and catastrophic to trust". What a Cascade would add over what is here is the dynamic
disagreements — a reaction that intercepts, an interruption that splits a concentration which was going
to arrive together on paper.
Partially held 2026-09-28: "never read true state" is enforced and pinned; building an opponent model from own Slate waits on a hypothetical-state builder. The negative half stands at the fairness checker's true-world-type rule (scripts/check-ai-fairness.mjs) and packages/client/test/fog-leak.test.ts — its it 'an enemy unit the seat cannot see does not change a byte' under describe 'AI-1405 — the fog-leak gate'. And packages/ai/test/rehearse.test.ts pins this row as a boundary stated in an argument count: rehearse reads no state, and its comment notes that the forked Cascade of AI-585 is its own slice with its own fog-leak gate. What waits: the opponent model built from the Staff's own Slate rather than true state — it needs the hypothetical-state builder.
6.7 The planner's language is richer than the order vocabulary
AI-580 The Staff's plan representation MUST be separate from its order encoding. The planner MUST be able to represent, at minimum: objectives with deadlines; corridors and frontages; fixing forces; synchrony groups (these units strike in this Cascade or none of them do); retreat-degree constraints; refusal of a specific named city; and Operations valued by what they remove from an opponent rather than by what they gain. Each turn the Staff MUST compile the current plan down to Postures, Sanctions, Tasks, Postings and Requisitions, so that every unit ends the compilation holding ordinary orders indistinguishable from a human's, executed by the same command layer (13-command.md CM-100, CM-2040). The compilation MUST be treated as lossy, and AI-585's Rehearsal is what detects the loss.
Rationale, and it is the sharpest correction the red team made to this design. The Posture and Sanction vocabulary was sized for a unit panel: 13-command.md CM-330 fixes exactly six Postures and CM-230 fixes the stack at three levels, both with the stated reason that a deeper structure cannot be explained on a panel. Those are correct decisions for a human interface and terrible ones for a planner. Five things a good player conceives cannot be said in that vocabulary at all — simultaneity (CM-1010's cohesion radius is proximity, not synchrony); refusal of one specific city (CM-700's Seize is a policy over a class of cities); feints and fixing attacks, and crucially the ability to recognise one, since a demonstration is indistinguishable from an attack that has not happened yet; graph properties such as retreat degree, Depot-chain articulation, and corridor control, which are properties of the position graph rather than of any tile or unit; and timing, since no Posture takes a turn number and 14-victory.md's published Tally calendar exists precisely to make "be at the Iron Gate on turn 96" a plan. If the planner shares the order vocabulary it cannot conceive any of these, and the AI is fast and weak.
6.8 Playing any unit set
AI-590 All unit reasoning MUST derive from 02-units-and-industry.md's declared capability fields and closed flag vocabulary (US-380, US-470) and from the class enumeration (US-090). The Staff MUST NOT branch on a unit's identity, name or index, and MUST play any valid unit set competently. A unit whose capabilities give it no usable role in the active set MUST be garrisoned or escorted, never left without a Posture and never a planning crash.
Partially held 2026-09-28: capability-field identity is pinned under neighbouring titles; this row lacks its own pin for the garrisoned-or-escorted guarantee. The identity discipline stands at packages/client/test/staff-layers.test.ts's describe 'AI-600 — capability data is enough, hint data is a bonus' (its its: 'the Staff reads capability fields rather than class names', and 'a roster lookup rather than a branch'); roster-priced generation holds where EC-930 is pinned in packages/client/test/generate.test.ts; role-less units fall to placeLeftovers in packages/ai/src/assign.ts; and the Floor's no-idle row answers at packages/client/test/floor-gate.test.ts. What waits: a pin under this id for the guarantee that a unit with no usable role ends garrisoned or escorted rather than left without a Posture.
AI-595 Setup MUST match an AI plugin's declared unit-set support against the selected set: a plugin
declaring any is always offered; a plugin declaring an explicit list is offered only for those sets,
and selecting it elsewhere is refused with an explanation rather than silently degrading.
no-test-needed: plugin unit-set matching presupposes plugins — none exist in this tree (§12's host cluster is unbuilt), so nothing declares a supported set, and there is no offering to refuse with an explanation; staff-layers.test.ts's header records the clause at its own requirement rather than citing it there. It becomes testable when the plugin host lands.
AI-600 A custom unit set MUST NOT be able to carry executable code (02-units-and-industry.md US-2060, 07-modding-content.md MOD-300), so the Staff's declarative capability reasoning is always sufficient. A set author MAY ship AI hint data alongside a set — role annotations and weight overrides — and the Staff SHOULD consume it; a set with no hint data MUST still be played competently from capability data alone.
6.9 The Constraint Floor
AI-560 The following obligations MUST be solved, not scored, MUST run at full quality at every Rank, and MUST NOT yield to the step budget. Each MUST be exact arithmetic against the owning specification.
| Obligation | Method | Bound |
|---|---|---|
| Garrison sufficiency | 12-economy.md EC-1170 gives the requirement by class and Integration and EC-1200 the test. Compute required − present per city; deficits become Requisition shortfalls (13-command.md CM-1250, EC-1220) and Relieve Operations |
O(cities) |
| Cadre routing | AI-533's rate priority over the exact 12-economy.md formulas; escort mandatory wherever the route's threat field is non-zero | O(cadre × cities) |
| Depot chains | Set-cover maximising supplied tiles along the intended axis, subject to every Depot's own path home remaining intact; candidates capped at 64 tiles per axis | ≤ 64 per axis |
| Transport lift | 13-command.md CM-530's Ferry loop modes execute shuttles with no per-turn input; the Staff picks route endpoints and capacity only | O(routes) |
| Air endurance | A hard filter, never a scored decision: no sortie is generated whose return leg exceeds remaining endurance against a reachable base or carrier. An aircraft is never considered for a one-way trip | O(air × bases) |
| Production continuity | Every city always carries a Consignment or Template (13-command.md CM-1160, CM-1310); a newly captured city defaults to Auto and is therefore never idle |
O(1) per city |
| No idle unit | 13-command.md CM-540 raises an Idle event every turn. The Staff MUST treat any Idle as a hard defect and assign a Posture in the same turn |
O(events) |
| No stranded formation | CM-1440's Stalled detection is core-side and fog-honest. Two consecutive non-progressing turns MUST force the Operation to be abandoned and re-planned |
O(events) |
| Retreat degree | AI-1160 | incremental |
Implementation note, 2026-09-11, updated 2026-09-12. All nine are met, across
client/src/ai/floor.ts, retreat.ts, threat.ts and depot.ts. The rationale's claim is now
testable rather than aspirational — "a shipped AI that gets all nine right at every Rank will read as
far stronger than its search budget suggests" — and what each row cost is recorded at the row:
The four files moved to packages/ai/src/ on 2026-09-27, when the Staff left the client package; the paths above are where they were built.
- Cadre routing is met as of 2026-09-12, both clauses, once §8.2's threat field existed — which is
what it was waiting on when this note said it was not. AI-533's rate priority runs over EC-540's own
effective-Works arithmetic, cross-multiplied so nothing divides; the escort clause reads AI-1112's
chunk grid along the route, which is what the grid is for and is not the city-level use the
requirement forbids. Two things were measured building it. AI-533's denominator is undefined for a
city that is not integrating, and the commonest cause is an unmet garrison — EC-930's step 1–5 stops
include the garrison requirement, so a city nobody holds does not integrate however many
Administrators walk into it; that couples this row to the garrison row above and makes the Floor's
order (garrison first, Cadre after) the correct one rather than an accident. And the two
substitutions that make Assay feature A8 dishonest are harmless here: a per-city supply state the
projection does not carry, read as Supplied, and EC-460's Academy aura read as absent are uniform
across every candidate and cancel out of a ranking, where they would inflate a score. EC-950's
hostile-within-3 term is not uniform — a frontier city has hostiles near it — so that one is computed
from the seat's own Contacts.
- Depot chains is met as of 2026-09-12. The greedy maximum coverage the row names, in
client/src/ai/depot.ts: axes from every held city toward every objective (and toward the frontier
where it knows of none), candidates capped at the stated 64 per axis and sampled rather than
truncated — the lazy reading of the cap turns a candidate bound into a range limit, so a chain
could never be aimed past 64 tiles from a city on a Vast map — and every station scored against the
union the earlier stations already supply. Greedy rather than exact because maximum coverage is
NP-hard and because AI-1260 asks for "a small deterministic set cover" in as many words; every
tie breaks on the lowest tile index.
The defect it fixes is specific: the rule it replaces placed one Depot at a time and could not see what another already covered, so two Depots on one axis stationed inside each other's Reach and the second bought nothing. No per-Depot rule can see that, which is why the requirement asks for a cover.*
Two measurements the implementation forced, both recorded. The path-home constraint clusters the chain rather than spreading it, and that is correct: a station must itself be supplied, so the chain grows about one Reach per emplacement instead of leaping to the far end of the axis — a test row that first asserted the stations were a Reach apart failed for that reason and now asserts the property that matters, which is that the second Depot buys something. And the coverage proxy has to read fog the way AI-522 says: with unrevealed ground counted impassable, a turn-one seat's whole revealed world fits inside its capital's own Reach 10, every candidate scored a gain of zero, and the cover correctly declined to move a Depot for the entire opening — the row met in code and silent in play. AI-522 settles it ("an unrevealed tile is passable at the cheapest cost … the Staff plans optimistically and discovers the truth"), and the direction is the opposite of AI-1160's for a good reason: AI-522 is about where a seat can go and should be wrong toward trying, AI-1160 is about whether a stack can get out and should be wrong toward believing itself trapped. AI-522's own second sentence draws the same line from the other side — "this prior MUST NOT reach threat evaluation".*
Four of the seven became possible only on the day they were built, and the blocker was 13-command.md rather than this document: garrison deficits need Requisitions (CM-1250), production continuity needs Consignments (CM-1160), transport lift needs Ferry loops (CM-530), and the Formation half of "no stranded formation" needs Formations. (Corrected 2026-09-16: transport lift was met on paper only. The loop modes existed, but CM-535's loading and unloading at the terminals did not, so a Transport the Floor set shuttling crossed empty until CM-535 was built that day.) §7 and §8 landed the same day, and the Floor's own module documentation had been naming each absence for weeks — which is the argument for naming an absence where the reader will be standing when it stops being one.
Measured and built 2026-09-16 (Claude Code): the stranding the Register could see coming. "No stranded formation" re-plans a unit after two turns without progress, and the re-plan excluded only the city it had stranded on. Over 150 turns of one Standard/4 game, 278 of 682 real fallbacks (10-turn-model.md TM-1440) were MoveTos with no route in the seat's own knowledge while the ground connected. Nearly all were aimed at a city neutral Militia held after a revolt. A hostile occupant refuses the entry (01-game-rules.md GR-560), and the Floor issues no order that attacks a city, so every such march was a stranding the seat's own Contact Register predicted. So the Floor no longer marches on a city where its Register records a unit. Lift demand still reads the terrain alone. A stranded unit with nowhere left to go now drops its Task, so its Posture resumes (13-command.md CM-370), rather than the Task falling back every turn. Measured on four Standard/4 games, filter off and on: the stance-fallback rate fell in every game, from 1,367 of 15,288 activations (8.94%) to 761 of 14,208 (5.36%). CityTaken events rose from 135 to 140, and cities held by seats at the end from 52 to 58. One of the four games is still above TM-1440's 8% band (seed 303: 12.63% to 8.72%), so 09-roadmap.md §2.4's corpus clause is not met yet. Taking defended cities remains the Staff's work (§8), not an obligation of this table. Later the same day, after 13-command.md CM-210's ruling on a Reflex whose objective is already met, all four games were inside the band: 452 of 14,353 activations (3.15%), the highest seed 303 at 5.09%. Four AI-only games are evidence, not the clause's corpus of real games, which Phase 3's playtests supply.
Rationale: most of what makes an opponent look intelligent is not intelligence. It is never doing the nine things in that table wrong. A shipped AI that gets all nine right at every Rank will read as far stronger than its search budget suggests, and one that gets any of them wrong will read as broken regardless of how good its planning is. This is also exactly 13-command.md CM-020's ledger split: Execution-ledger work is arithmetic and is always performed correctly; Commitment-ledger judgement is where an AI is allowed to be worse.
7. The Assay — the evaluation function
7.1 Scale and arithmetic
AI-1000 The Assay MUST be denominated so that one city = 100 points, on the same scale as 14-victory.md's Standing, so that every weight is interpretable — "a Cadre on station is worth nine-tenths of a city" — and the Assay and the game's own score cannot drift apart.
AI-1010 All Assay arithmetic MUST be exact integer arithmetic. Weights MUST be integers with a declared scale, expressed as per-mille multipliers on integer quantities, defaulting to 1000‰ so that the untuned origin is neutral. Ratio comparisons MUST be performed by cross-multiplication, never by division. No floating-point value MAY appear anywhere in the Assay, in the Sandtable, in the Docket, in local search, or in any weight file at rest or in memory. Where a divide is unavoidable it MUST be floor division toward negative infinity, matching 11-combat.md CB-070.
Rationale: this is deliberately stricter than what binds plugins in general — AI-385 requires only
that no float cross the ABI and 07-modding-content.md MOD-600 permits float arithmetic inside a guest.
The shipped Staff gives that permission up anyway, for a specific reason: the utility sum is the one
place an evaluator naturally reaches for floats, and it is also the value the self-play pipeline
reproduces across tens of thousands of farm games. A promotion result (AI-660) has to mean the same
thing on every machine that re-runs it, including the farm reproduction that adjudicates a dispute
three years from now. A tuned weight of 0.734 is a different number on two decimal-to-binary paths; a
tuned weight of 734 per-mille is the same number everywhere, forever.
Reading recorded 2026-09-11, when §7 was implemented: accumulate the numerator, divide once.
Several features in AI-1030's table write a division inside their sum. Read per element, A15's
− Σ_units cost × penalty‰ / 400000 gives a unit of worksPaid 200 at Cut supply
floor(200 × 400 / 400000) = zero, and so does every unit at every cost the roster carries — the
feature would be identically nothing in every reachable position, which is not a smaller number but no
number at all. So each feature accumulates an integer numerator across its sum and divides once at the
end. The two readings agree wherever the per-element quotient is exact and differ only by the discarded
remainders everywhere else, and this clause's own "no floating-point value MAY appear anywhere" is
satisfied identically either way.
AI-1020 Every number the Staff uses to make a choice MUST live in a versioned parameter manifest — named integers with declared ranges, a declared scale, and a content hash — and MUST NOT be a constant in code. An AI whose thresholds are code constants cannot be tuned, cannot be A/B tested, cannot ship two Ranks from one binary, and cannot have a promotion gate, because there is nothing to promote.
7.2 The features
AI-1030 The Assay MUST be a weighted integer sum over the following feature set. press(p) is the
shared convexity function that makes progress near a threshold worth disproportionately more, with p
in per-mille of the threshold:
press(p) = p for p < 500
500 + 2 × (p − 500) for 500 ≤ p < 900
1300 + 6 × (p − 900) for p ≥ 900
| # | Feature | Definition | Default weight |
|---|---|---|---|
| Territory and victory | |||
| A1 | Cities held | 100 × cities |
1000‰ |
| A2 | Landmarks | 150 × landmarks |
1000‰ |
| A3 | Seats | 200 × seats held |
1000‰ |
| A4 | Track pressure | Σ_T w_T × press(p_T) / 1000 |
1000‰ |
| A5 | Track denial | − Σ_T Σ_opp 3 × w_T × press(p_opp,T) / 4000 |
1000‰ |
| A6 | Vigil equity | +400 × held/required (mine), −600 × held/required (theirs) |
1000‰ |
| Economy | |||
| A7 | Integration stock | 3 × Σ_cities (baseWorks × integration / 100) |
1000‰ |
| A8 | Integration slope | 2 × Σ_cities (projected Δintegration over 10 turns × baseWorks) / 100 |
1000‰ |
| A9 | Levy rate | 20 × manpowerPerTurn |
1000‰ |
| A10 | Production rate | 8 × worksPerTurn |
1000‰ |
| A11 | Industry depth | 40 × Σ_cities (industry − 1) |
600‰ |
| A12 | Cadre on station | 90 × cadre stationed where unrest < 50 |
1000‰ |
| A13 | Occupation drag | − Σ_foreign cities (100 − integration) × baseWorks / 100 |
1000‰ |
| Force | |||
| A14 | Force value | Σ_units cv(u) × mobility‰ / 1000, where cv(u) = cost × max(0, 100×strength − 50×disorder) / 40000; mobility 1000‰ mobile, 500‰ immobile |
1000‰ |
| A15 | Supply state | − Σ_units cost × penalty‰ / 4000 (amended 2026-09-11, was 400000 — see below) |
1000‰ |
| A16 | Veterancy | + Σ_units grade × cost / 200 |
600‰ |
| A17 | Lift capacity | + 30 × min(transportSlots, unitsNeedingLift) |
800‰ |
| Position | |||
| A18 | Garrison shortfall | − 200 × Σ_cities max(0, required − present) |
1000‰ |
| A19 | City threat | − Σ_cities threat[city][2] × cityValue / 4000 |
1000‰ |
| A20 | Seat exposure | − 800 where the 3-turn threat at a Seat meets or exceeds its garrison value |
1000‰ |
| A21 | Frontier opportunity | + Σ_takeable cities cityValue × takeProb‰ / 4000 |
800‰ |
| A22 | Reach coverage | + 2 × supplied tiles within 6 of the contested band |
400‰ |
| A23 | Chokepoint control | + 60 × held chokepoints on an active axis |
1000‰ |
| A24 | Observation | + 1 × currently-observed tiles in the contested band |
200‰ |
| A25 | Unaccounted enemy force | − unaccounted Works / 8 |
1000‰ |
| A28 | Retreat degree | − 150 × own stacks at retreat degree ≤ 1; + full target stack value where an attack drives an enemy stack to degree 0 |
1000‰ |
| A29 | Strategic-asset exposure | − replacementCost × P(loss within 6 turns) / 1000 over Cadre, Depots and high-Industry rear cities |
1000‰ |
| A30 | Trend | signed 20-turn deltas of mean Industry, total Works, Cadre count and integrated-city count, each scaled | 1000‰ |
| Hygiene | |||
| A26 | Idle or stalled | − 40 × (idle formations + stalled formations) |
1000‰ |
| A27 | Switching cost | − 25 × command objects retasked this turn |
1000‰ |
Amendment ruled 2026-09-11 by measurement, and binding: A15's divisor is 4000, not 400000.
The published number makes the feature inert. Measured across the roster's cost range, a completely unsupplied thirty-unit army of average cost 200 comes to 6 points against a force value (A14) of 1500 — four-tenths of one percent — while 11-combat.md CB-1650 makes Cut supply a 400‰ cut in combat power, the difference between winning a fight and losing it. An evaluator that priced a whole army out of supply at 0.4% of its own value would march it out of supply happily, which is precisely the class of blindness AI-560's rationale is about.
The corrected divisor is not a guess. AI-1032 fixes cv(u) = cost / 4, so a p-per-mille haircut on
a unit's own force value is cost × p / 4000 exactly — and at that divisor A15 takes 40% off a Cut
unit and 15% off a Strained one, per-mille for per-mille with CB-1650. 4000 is also the divisor A5, A19
and A21 all use; 400000 is the only number of its kind in the table, which is the shape of a
hand-written zero rather than a decision. penalty‰ itself reads as CB-1650's supply modifier taken as
a magnitude where it is negative — Cut 400, Strained 150, nothing for Adequate or Full — because this
document names no supply penalty of its own and CB-1650 is the only per-mille one in the specification
set. The parameter manifest keeps 400000 inside a15.divisor's declared range, so the amendment costs
nothing to reverse.
Reading recorded 2026-09-11 on the opponent terms: A5 reads the pessimistic end of every band.
AI-1040 confines opponent standing to A5, A6 and A25, and A5 needs press(p_opp,T) — a per-mille
ratio the projection does not publish at full fidelity. 14-victory.md VC-2200 gives three tiers: an
Alarm publishes the exact ratio and count, a Report publishes either a natural-unit count
(VC-2240, thresholds of eight or fewer — also exact) or a ratio quantised downward, and a Rumour
publishes only a word from VC-2210's five-step vocabulary. A5 therefore takes the top of every
interval: 'advanced' is read as 499‰ rather than 375‰, and a quantised Report as q + quantum − 1.
A5 is a denial term, so under-estimating a rival's progress is the dangerous mistake, and this is the
same direction AI-1160's retreat degree errs in and for the same reason.
One term came out better than expected: A6 is exact on both sides. VC-2200 puts a Track into Alarm when the ratio reaches 900 or the Vigil counter reaches 1, and an Alarm publishes the counter and its requirement exactly — so a rival's Vigil is always visible when it exists, and there is no band to be pessimistic about.
Reading recorded 2026-09-11 on A26: its "idle" is broader than 13-command.md CM-540's, and the two are different states. CM-540's idle unit has no standing order at all, and the Constraint Floor already gives every such unit a Posture (AI-560). A26 is a hygiene term about wasted capacity, and a unit sitting on a default Hold with no Task is wasted capacity — it has somewhere to be and has not been told where. So A26 counts command objects carrying no Task, which is also what makes the term move when the Staff sends its army somewhere.
AI-1032 cv(u) MUST equal cost / 4 at full strength and zero disorder, matching 14-victory.md's
military Standing term, so that a healthy army is worth the same in the Assay as on the scoreboard.
Disorder MUST cost half as much per point as missing strength, mirroring 11-combat.md's resolve
arithmetic inverted — their disorder helps the evaluating seat and its own hurts it.
AI-1034 Features A28, A29 and A30 MUST NOT be omitted at any Rank at or above the Rank that introduces them (§13.2), and A28's defensive half MUST additionally be enforced by the Constraint Floor at every Rank (AI-1160).
Implementation note on A21, 2026-09-12. takeProb‰ is 11-combat.md's own capturePermille
(CB-2150) over a CB-300 Clash Specification the seat builds from its projection — the engine's
arithmetic rather than a second combat model, which is what AI-1040's discipline is for. The Contact
Register's assessed block is already exactly a CombatantRecord, which is why GR-1250 puts those
fields there.
A specification is built directly rather than through buildClashSpec, and the reason is the AI-115
boundary: that function takes a view whose units are whole UnitStates, and the seat has never seen
a whole enemy unit — going through it would mean fabricating two dozen fields it has no evidence for,
where a CombatantRecord is thirteen the Register actually carries. Every remaining term is computed
from the projection or defaulted with its reason at the call site: flankers is 0 because an estimate
that assumed a flank it has not arranged would read its own optimism back as a probability;
attackerEdgeMods and defenderExtraMods are 0 because CB-590's and CB-600's sums come from a route
nobody has chosen, which makes A21 optimistic about difficult ground — an assault across a river
will go worse than this says, and that is the same direction AI-522 errs in and for the same reason.
hashKey is a stated sentinel, not a fabricated seed: capturePermille is threshold arithmetic and
never consumes CB-800's positional draw, so the seat needs no seed — and a plausible-looking key would
imply it had one, which under AI-115 it does not. A test asserts the sentinel survives the call, which
makes "never draws" checked rather than asserted.
The fidelity split rides into the decomposition as a partial: an estimate against an assessed Contact and one against AI-1132's class-typical garrison are different claims, and flattening them would present a guess with the authority of a reading. AI-1130's answer to the second kind is a reconnaissance Operation "valued at the swing in the pivot", consuming CB-2330's full-domain evaluation and CB-2340's pivot analysis — neither of which exists in the engine, so the split is reported and no reconnaissance is generated.
Rationale for the three additions to the original 27: each closes a failure this design's own systems create. Retreat degree (A28) is a property of the position graph, not of any tile or unit, so a tile-and-unit evaluator is blind to it in both directions — it neither notices its own stacks losing their last exit nor notices that one move would take an enemy stack's — and 11-combat.md CB-050 makes that the largest single swing in tactical play. Strategic-asset exposure (A29) exists because 12-economy.md's Administrative Cadre is simultaneously the anti-snowball engine and the softest target in the game, and rear-area security is exactly the region of a per-turn utility landscape that is systematically under-served. Trend (A30) exists because several of this game's worst outcomes are monotone ratchets — a captured city drops an Industry level and recapture does not restore it — and a level-only evaluator sees each event as a small negative while twelve of them are a collapsed industrial base.
Partially held 2026-09-28: all three features are built, declared and consumed, with A28's defensive half in the Floor; what waits is the Rank gating itself — no code enforces absence below an introducing Rank. A28 stands at packages/ai/src/assay.ts — its defensive half scores retreat degree against a28.degreeCeiling from parameters declared in packages/ai/src/manifest.ts (a28., a29.) — A29 is scored beside it, and A30's trend (trendOf in packages/ai/src/memory.ts) is read by the Assay and pinned at packages/client/test/belief.test.ts under describe 'A30 — the trend', whose comment names this row. The defensive half of A28 additionally runs from the Constraint Floor: packages/ai/src/floor.ts's 'Retreat degree (AI-1160, the defensive half of A28)' section with packages/ai/src/retreat.ts. What waits: the MUST-NOT-omit gate over Ranks — ranks.json's per-Rank feature sets declare which Ranks carry each term and rank.test.ts pins them cumulatively under describe 'AI-612', but no code or test enforces that a lower Rank lacks what it has not introduced, and no test title names this row.
AI-1040 The Staff MUST NOT compare its own Assay to an estimate of an opponent's Assay. Opponent standing MUST enter only through A5, A6 and A25, in the evaluating seat's own currency.
Rationale: cross-player evaluation comparison is a classic source of nonsense in this genre — my 4,000 and your 4,000 are not commensurable when we hold different things under different fog — and refusing to do it costs nothing.
AI-1050 The Assay MUST be decomposable into independently computable term groups keyed by entity, so that a local-search move can be scored as a delta over the few terms it touches. Full Assays MUST be computed only at Sandtable leaves. Estimated budgets, labelled as estimates: full Assay ≤ 15 µs at reference midgame and ≤ 60 µs late-large; one delta ≤ 3 µs.
AI-1060 Every committed plan MUST emit an Assay decomposition into the Intent Record (AI-750): the total, every non-zero feature's contribution, the chosen seed, and the top three rejected alternatives with their scores and the terms that separated them.
Implementation note, 2026-09-11. The Constraint Floor emits one every turn, carried on its plan and summarised in one Dispatch sentence. The rejected alternatives are absent rather than empty, and the distinction matters: there is no search — §6.4's Operations does not exist and the Floor commits the only plan it has — so an empty alternatives array would read as "the Staff weighed other plans and none scored" instead of "the Staff had one plan". The note says how many features scored and how many are waiting on machinery, which is the true statement available today.
The decomposition is of the position the plan was chosen against, taken before the turn's orders resolve. A seat that scored the post-order position would be reporting its own optimism, and a test holds the difference.
Twenty of the thirty features are computed exactly from the fog-filtered projection. The ten that are not are reported as limits, each naming the one thing it waits on, because a feature returning zero is indistinguishable from a feature that has nothing to say: A8 (a per-city supply state and EC-950's hostile term on true world state), A19 and A20 (the threat field), A21 (a take probability), A22 and A24 (a supply envelope, and the contested band — a term this document uses in both features and defines nowhere), A23 (§8.6's graph properties), A25 (§8.3's Slate), A29 (a loss probability), A30 (§10.4's cross-turn memory), and A2, which was absent for a reason that was not the AI's and was fixed on 2026-09-12: the player view carried no Landmark at all, so VC-510 paid +150 per held Landmark into a Standing the seat could read while nothing told it which ones it held. The projection now carries the public list (view v12, the reading is at 03-architecture.md AR-430) and A2 is computed by composition — a Landmark is held iff its marked city is in the seat's own cities. Two more are computed with a clause missing and are named partial rather than left to read as complete: A3 (a captured rival's Seat scores +200 and no other player's Seat mark is in the projection) and A28 (the offensive half is a property of a candidate attack, so it rides with the move generator).
8. Belief — the Slate
8.1 Two priors, because one is wrong in one of its two jobs
AI-1100 The Staff MUST maintain two distinct priors over unrevealed space, and MUST NOT use one for both jobs:
Built 2026-09-23. The routing half was already oracle.ts’s darkCost (CM-140); the pessimistic
occupancy prior is new, and it spreads the unaccounted force by reachability weight rather than
stacking it — a chunk far from every Contact and every believed city carries almost none, one beside
both carries most. The two are named side by side in one exported pair, because "MUST NOT use one for
both jobs" is exactly the clause that goes wrong quietly when the two live in different modules under
different names.
The prior loses nothing to rounding, and that is the requirement rather than tidiness: a pessimistic prior that quietly floors force away is an optimistic prior with extra steps. The remainder goes to the heaviest chunk, ties by the lowest chunk index. And every named chunk gets an entry, including an unreachable one at zero — a chunk missing from the map is a chunk the threat field reads as empty, which is the rationale’s own failure: "an AI that asks ‘is my flank safe?’ over a knowledge view in which unknown means empty concludes that its flank is always safe."*
(a) Routing MUST use 13-command.md CM-140's optimistic prior — an unrevealed tile is passable at the cheapest cost and contains no enemy (AI-522).
(b) Threat evaluation MUST use a pessimistic occupancy prior: unrevealed space MUST be treated as potentially containing the unaccounted enemy force of AI-1120, distributed over unobserved chunks weighted by reachability from last-known Contact positions and from believed enemy cities.
Rationale: CM-140 is a good requirement with a good rationale — pessimistic routing produces units that refuse to cross unexplored ground, which is behaviour no player would ever choose. The danger is that the same assumption reaches evaluation, where it means every unexplored region is a free approach corridor in the AI's model and an actual approach corridor in reality. An AI that asks "is my flank safe?" over a knowledge view in which unknown means empty concludes that its flank is always safe. This is expected to be the source of most "the AI got flanked by an army that walked through the obvious gap" reports, and it is expected to be misdiagnosed as a pathfinding or scouting bug rather than as a belief-representation one.
8.2 Contacts are discs, not points
AI-1110 A Contact of age k MUST be treated in threat evaluation as a disc of radius
k × lastKnownMovement, capped at a horizon, rather than as a point. Contact weight MUST decay with
age on a published integer curve — the default is clamp(1000 − 120 × age, 200, 1000) per-mille, so a
four-turn-old Contact counts at 52% and a six-turn-old at 28%, floored at 20% — and a Contact MUST NOT
be deleted merely for being old, only aged, within whatever 01-game-rules.md's own decay rules leave in
the Register.
Rationale: this is the single most valuable belief decision available and it costs one integer per Contact and a radius comparison. Both naive alternatives are bad in recognisable ways. Markers that never expire produce an AI that defends against ghosts, pinning units against an enemy that left thirty turns ago — paranoia, and a garrison sink that compounds with the turtling failure of §18. Markers that expire immediately produce an AI repeatedly surprised by forces it has already seen. A disc changes the shape of the AI's defence from "a unit at every remembered contact" to "a reserve that can reach any of them," which is also what a good human does.
AI-1114 The contested band MUST be the union of the reach discs of the Staff's believed hostile Contacts at the 2-turn horizon — that is, the tiles a Contact could occupy within two turns under AI-1110's disc, taken over every Contact the Slate holds.
Ruled 2026-09-12. The term was used in two Assay feature definitions — A22's "supplied tiles within 6 of the contested band" and A24's "currently-observed tiles in the contested band" — and defined nowhere in this document: not in §2's vocabulary, not in §8's belief section, not in §6.4.
Defined from the hostile side alone, which is what keeps it non-circular. The obvious alternative — the band where both sides' reach overlaps, or the band of supplied frontier — makes A22 read "supplied tiles within 6 of supplied tiles", which measures nothing. From the hostile side the two features read as they plainly want to: A22 is how much of the seat's supply sits near where the enemy can be, and A24 is how much of where the enemy can be the seat is watching. Both are questions about the seat's posture toward a threat, which is what a contested band is for.
The 2-turn horizon rather than another: it is the horizon AI-1030's A19 already reads for city threat
and the one §6.3's Relieve trigger names, so the band and the threat term describe the same tactical
present. It is also computed from machinery that exists — the discs of AI-1110 — rather than from the
supply envelope A22 and A24 are separately waiting on, so this clause unblocks the definition without
pretending to unblock the features.
Built 2026-09-17 (client/src/ai/threat.ts, inContestedBand), and A24 with it — the feature
this clause was ruled for had still been reporting "the same undefined contested band as A22", a
reason that stopped being true the day this ruling landed. Two things the implementation is worth
recording for.
It is the union of reaches, so it is AI-1112’s own reaches over every disc — no new geometry.
That is deliberate: a band computed any other way would be a second answer to "where can they be"
beside the threat field’s, and this clause’s own argument is that the two "describe the same tactical
present". It also means the band is never materialized as a tile set, which on a Vast map with
eight seats would be the largest object the Staff builds, for a question every caller asks one tile
at a time.
"Believed hostile" holds by construction rather than by a filter: the discs come from the Contact Register, and a register holds what a seat has observed of other players, never its own units. A Contact whose fidelity admitted no owner (GR-1290) is still not this seat’s and still somewhere force can be, so it bands — filtering by owner would drop exactly the contacts a seat knows least about, which is the wrong direction for a band that exists to say where to look.
A22 is still absent, and its reason is now the half that is true: it wants a supply envelope ("+2 × supplied tiles within 6 of the band") and the projection carries no per-tile supply state. The Assay’s rows pin that sentence as well as the list it appears in — a reason nobody checks is a reason that goes stale, which is what happened here for five days.
AI-1112 The Staff's threat field MUST be a per-chunk integer grid — 64 × 64-tile chunks, one
plane per horizon h ∈ {1, 2, 4, 8} turns, optionally one set per opposing player — summing reachable
Contacts' combat value scaled by age decay, plus the spread of unaccounted force. City-level decisions
MUST NOT use the chunk grid; they MUST use an exact per-city query over Contacts reachable within h
turns along the Atlas.
Readings recorded 2026-09-12, when §8.2 was implemented. Three, plus one clause named absent.
1. The disc and the horizon compose additively. AI-1110 gives a disc of age × move — where the
Contact might be now — and this clause asks for Contacts "reachable within h turns", which is
h × move further from wherever that is. So the reach at horizon h is (age + h) × move. The
plausible alternative, taking the larger of the two, would say a six-turn-old Contact is no more
dangerous at horizon 4 than at horizon 1 — false of the only thing the field measures, and pinned by a
test that fails under it.
2. {1, 2, 4, 8} constrains the grid's planes, not the exact query. The horizon set is attached
to the chunk grid and the last sentence leaves the per-city query to take "h turns" unqualified —
which is how AI-1030's A19 can ask for horizon 2, A20 for horizon 3, and §6.3's
Relieve trigger for the "2-turn threat field", without any of the three being a mistake. A19's 2 and
the Relieve trigger's 2 are the same number; A20's 3 is also 13-command.md CM-1750's own reach.
3. The radius stops growing where the weight stops shrinking. AI-1110 says the disc is "capped at
a horizon" and names no number; the decay curve names one for it. clamp(1000 − 120 × age, 200, 1000)
first reaches its floor at age 7 (1000 − 840 = 160, clamped to 200), so past age 7 an older Contact
is not believed any less — and growing its radius past that point adds area without adding information,
which is how a seat ends up defending against a disc the size of the map. The cap is 7, derived rather
than chosen.
Absent and named: the "plus the spread of unaccounted force" clause. That is AI-1120's estimate,
whose believedProduction(P) is "Σ over turns of believedWorks(P, turn), carried incrementally" — and
the Staff has no memory across turns, being reconstructed from the projection each turn (§10.4 is where
that would come from, and it is the same blocker as Assay feature A30). Every other input of AI-1120 is
available today, so the blocker is the carry rather than the arithmetic. Named at the feature rather
than substituted for, because a threat field that quietly omitted the unseen would be the most
dangerous kind of confident.
One reading on the count, recorded at AI-1110. GR-1260's bands are read asymmetrically, on purpose:
few is 2–4 and is taken at its top, because a bounded band has a top and under-counting a threat
is the dangerous direction, while many is 5-and-up and is taken at its floor, because an
unbounded band has no top and inventing a ceiling for it would put a number in the field that nobody
could check against the rules. count is exact from fidelity 2, so the bands only decide anything at
fidelity 1.
8.3 Reasoning about what has never been seen
AI-1120 The Staff MUST maintain an integer unaccounted force estimate per opposing player, derived only from information the game publishes to every seat and from the Staff's own observations:
believedCities(P) from the published band (14-victory.md VC-2200, VC-2250), band midpoint ± width
believedWorks(P, t) = believedCities(P, t) × own observed Works per city
believedProduction(P) = Σ over turns of believedWorks(P, turn), carried incrementally
militarySpend(P) = believedProduction(P) × militaryShare‰ − believedCadreSpend − believedIndustrySpend
unaccounted(P) = max(0, militarySpend(P) − observedAliveValue(P) − confirmedDestroyedValue(P))
The estimate MUST feed Assay feature A25 and MUST bias Sanction selection: a high unaccounted estimate
pushes the Staff toward cautious presets, larger reserves, Screen over March, and shorter Leashes.
Rationale: this is what a strong human does with the same information, and it is fog-honest by construction because every input is either published (VC-2200's bands, the public city count) or observed. It produces the correct relationship — the AI becomes cautious because it is ignorant — and that is legible: a player can watch the AI stop expanding after a scouting raid revealed a rival's army and understand exactly why. A hostile note against it: a human who understands this estimator can inflate the AI's fear by holding cities they cannot defend, or deflate it by hiding production. That is a bluffing surface rather than a defect, it is symmetric, and it is far better than an AI with no model of the unseen at all — but AI-1450 measures whether the equilibrium is interesting or degenerate.
Readings recorded 2026-09-12, when §8.3 was implemented. Three, plus a note on the second obligation.
1. believedCities is exact here, not banded. The formula says "from the published band … band
midpoint ± width", which presumes the city count reaches a seat only through VC-2200's banding. In this
implementation it does not: SeatView.cities carries "cities held at the Seal snapshot (VC-520's
count) — public by the recorded reading", per seat, in full. So the band arithmetic is for a
fidelity this projection does not impose, and using it would deliberately blur a number the game hands
over. The interval collapses to a point and the estimate is better than the requirement assumes.
2. believedCadreSpend and believedIndustrySpend are estimated by symmetry with the seat's own
spend. Neither is observable — a rival's Administrator purchases and Industry upgrades happen out of
sight — and this clause gives no estimator for them. Zeroing them inflates militarySpend and makes
the seat permanently timid; inventing a share for each is two more numbers nobody can check. So the
seat assumes a rival develops as it does, computed from the published cost curves over what it actually
holds (EC-1320's escalating Administrator price, EC's Industry rungs) — which is the assumption
believedWorks already makes when it uses "own observed Works per city", so one consistent
assumption runs through all three terms. It is also the assumption a human makes.
3. observedAliveValue reads the undecayed value. AI-1110's age decay prices where a Contact
might be, not whether it exists — a unit seen six turns ago is still a unit, and the Register ages a
sighting rather than deleting it precisely because it is still out there. Decaying it here would
double-count the uncertainty and, worse, would make the unaccounted estimate grow as sightings
aged: a seat that stopped scouting would become more afraid of an army it had already counted.
Note on the second obligation, which is the half that makes this worth building: "a high unaccounted
estimate pushes the Staff toward cautious presets, larger reserves, Screen over March, and shorter
Leashes." The Constraint Floor implements the reserve and none of the other three, because it has no
Sanction vocabulary to bias — presets, Stances and Leashes are §6.3's Strategy, which does not exist.
The reserve is expressible and is implemented: above the threshold the Floor holds the lowest-rated unit
on each held city instead of sending it at an objective, and says so in one sentence, which is the
legibility this clause's rationale asks for. The threshold is relative — per-mille of the seat's own
force value — because an absolute number would mean something different on turn 10 and turn 200.
8.4 Turning an unknown into a reconnaissance objective
AI-1130 Where an Operation's outcome pivots on a single unobserved field, the Staff MUST generate a reconnaissance Operation against that specific unknown, valued at the swing in the pivot, rather than guessing. 11-combat.md CB-2330 evaluates over the full domain of an unobserved input and CB-2340 performs pivot analysis over each unobserved field with an enumerable domain; the Staff MUST consume that pivot output directly.
Built 2026-09-22 in packages/client/src/ai/generate.ts as surveyCandidates. Survey is the third of
AI-525’s twelve kinds to have a generator, and the first to be generated against a question rather
than against a place.
CB-2340 answers in two pieces, and reading only the first offers nothing. The pivots array carries
the unobserved fields with an enumerable domain, ordered by span, largest first (CB-2350). Strength
is not among them and cannot be, because its domain is continuous — so CB-2340 reports it separately
through strengthDominant, and is explicit that where strength is unobserved it is the dominant
field. The first draft of this generator read pivots[0] alone and offered nothing at all on a real
turn-one view: the fogged preview came back provisional with strengthDominant true and pivots
empty. An unscouted city’s garrison has an unknown strength, and that is precisely the target a Staff
most needs to go and look at. Both branches measure the swing the same way — the span of the field’s own
defenderBreaks range, which is the quantity CB-2340 orders its pivots by — so a strength swing and an
entrenchment swing are comparable.
The valuation is derived, because a reconnaissance value picked by hand would be the guess this
clause exists to replace. A pivot’s span is the swing in break probability the unknown induces, in
per mille; a Survey buys that swing on the Operation it would inform; so it is worth
seizeValue × span / 1000. Two consequences fall out and both are wanted: a look at a target nobody
would attack is worth nothing, and a look at a rich target whose defence is a complete mystery is worth
the whole of it. The last is not a rounding error — when the swing is the full thousand, not knowing
takes the outcome from certain defeat to certain victory, so the look really does resolve the entire
question.
And the behaviour that falls out of the Docket’s own filter, which is the point of the clause: a Survey asks for one unit where a Seize asks for many, and AI-525 prices a candidate by value over cost. A city nothing is known about is therefore looked at before it is stormed, without anything anywhere having been told to scout first.
A measurement worth recording, because it bounds what this clause currently buys. End to end the
swing is only ever 1000 or 0. Strength dominates whenever it is unobserved, and once strength is
known no other enumerable field flips the break verdict against AI-1132’s class-typical garrison —
measured across grade, stance and entrenchment, with a Scout attacker and with heavy armour. So today
the Staff looks where it has never looked, and does not look again. The graded reading is built
because the clause asks for it and is held by rows of its own; it will begin to matter when a defender
is something richer than a class-typical militia. The supply state is a related fact about the
Register rather than about this clause: CB-1650’s neutral row means no Contact has ever carried one, so
supply is unobserved for every target and a fogged preview from this generator is provisional by
construction.
This is also the countermeasure §21’s red-team row E1 names. "The Blindfold" is one cheap Recon held at range disarming screens and deadlocking surveys permanently, because an unobserved strength predicts conservatively (13-command.md CM-160, 11-combat.md CB-2260), a conservative prediction fails the Cautious test (CM-690), and a Survey excludes the whole region. A Staff that prices the unknown and goes to look at it does not deadlock — and the measurement above says the price is at its highest exactly where the blindfold is most complete.
Readings recorded 2026-09-12, when Assay feature A21 was implemented against this clause's class-typical rule. Three.
1. A partial assessed block is an ordinary Contact, not a broken one. 01-game-rules.md GR-1250
admits "exactly the attributes admitted at the observation's fidelity", so a fidelity-1 sighting can
carry a class key and a role and no atk at all — it knows what it saw before it knows how hard that
hits. The first draft of the seat's record builder read the block as all-or-nothing and produced NaN
from exactly that Contact. The fallback is the class the entry names, which the ladder admits before
the stats and so is available precisely when the numbers are not; only then the roster default. A field
missing at this fidelity is a field the seat has not measured, never a field worth zero.
2. An unmeasured defender is at full strength. This is an offensive estimate, and assuming a damaged garrison is the optimistic mistake — the same direction AI-1110's decay reading errs in, and the opposite of the direction A21's terrain defaults err in (see AI-1030's note on A21).
3. A class the loaded set does not carry still yields a legal record. The loader would reject such a unit, but a Contact Register is written from observations and a mod can retire a class between one session and the next. AI-465's standard — "never a crash, never a refusal" — is what the seat is held to everywhere, and it applies to a stale sighting as much as to a stale memory blob.
AI-1132 A Contact that has been continuously visible for 3 turns without its fidelity improving MUST be treated as class-typical rather than as unknown for avoidance purposes, and MUST raise a reconnaissance obligation rather than an avoidance response.
Rationale: without this the Staff deadlocks by construction. Scouts avoid unidentified Contacts; Contacts stay unidentified because scouts avoid them; and AI-522's optimistic routing prior then tells the Staff that the unexplored region is empty. The AI ends up confidently blind precisely where the enemy is, and the behaviour reads as caution rather than as a bug. §18 E1 gives the exploit this closes and AI-1400's tripwire table gives the counters that detect it.
8.5 Step changes in information
AI-1140 A transition into or out of the Alarm band — the Staff's own or any opponent's — MUST be an unconditional full re-plan trigger (AI-545).
Built 2026-09-23 beside AI-1180, and it joins the trigger half to AI-1303’s pricing half landed the same day. Both directions and every seat, which is the whole content: a narrower reading that watched only its own band, or only entries, misses the case the requirement is written about — an opponent entering Alarm publishes its Bellwethers, and that is a board the Staff has never seen before. Re-pricing its own hold is narrower still: entry only, and only its own, because leaving does not un-publish what was published and VC-2290’s vision persists two turns past the clear. When an opponent enters Alarm, 14-victory.md VC-2290 grants every player live vision of the Bellwether sites for the duration plus two turns; the Staff MUST re-plan immediately rather than drifting toward the new optimum over ten turns and arriving after the grace period expired. When the Staff itself enters Alarm, it MUST re-price its own hold probability to account for its garrisons now being public.
8.6 Graph properties the Staff must hold explicitly
AI-1160 The Staff MUST maintain an integer retreat degree per friendly stack — the count of tiles that pass 11-combat.md's retreat-destination filter (CB-1310, CB-1120) as computed from the Staff's own knowledge view (CB-1360) — updated incrementally when an adjacent tile's occupancy changes. Retreat degree ≤ 1 MUST raise a Constraint-Floor obligation (AI-560) rather than merely contributing a score, and an attack that drives an enemy stack's retreat degree to 0 MUST be valued at the full value of that stack rather than at expected losses (A28).
Readings recorded 2026-09-11, when the Constraint Floor's half of this was implemented. Two.
1. An unrevealed tile is not an exit. This clause says the count is "as computed from the Staff's own knowledge view (CB-1360)", and 13-command.md CM-140 makes an unrevealed tile passable at the cheapest cost the mover's class pays — so a literal reading of the view would count unexplored ground as an escape route. That is the optimistic reading and it is the wrong one for a defensive obligation: a seat that counted what it had never looked at would think itself safe behind a blank map. So the Floor counts only revealed tiles, and errs toward believing it is trapped. The cost is stated plainly: the seat is more cautious near the edge of its knowledge than it strictly needs to be, which is the direction a floor should err in and the opposite of the direction that loses a stack.
2. The count is per tile, not per unit, because CB-1120 is. "The entire retreating stack" needs stacking room, so a tile holding four units needs four spaces and one holding one needs one. A seat piling units up is therefore in danger by that act, which is what 11-combat.md's own rationale calls "the sharpest edge in the document" — and counting per unit would hide it completely.
Note on the budget. This requirement asks for the count "updated incrementally when an adjacent tile's occupancy changes", and AI-560's bound column says "incremental". The Floor recomputes it per stack per turn: an eight-neighbour scan over a seat's own stacks is already inside any budget that would admit an incremental cache, and the cache is the optimisation while the count is the obligation. Doing the obligation exactly and the optimisation not at all is the right order to build them in; a cache that drifted would be worse than no cache, because a wrong retreat degree is a lost stack.
AI-1165 The Staff MUST maintain a stack liability term: the total value in a friendly stack multiplied by the probability that its retreat degree reaches 0 within the horizon. Stack liability MUST be consulted both defensively, before stacking, and offensively, before declining an attack on a stack whose top defender is unfavourable but whose exits are few.
Built 2026-09-23 as client/src/ai/liability.ts, on top of retreat.ts’s AI-1160 degrees. The
second consult is the interesting half and it is §18’s E8, the Gravedigger, read from the
attacking side: "retreat destinations are a published deterministic formula shown in the preview
(CB-1310, CB-1370), so pre-placing one unit on the last exit converts a marginal attack into the
destruction of a whole stack." The same arithmetic that makes that work against you tells you when to
run it yourself, and a Staff consulting liability only defensively would decline exactly the attacks
worth making.
The probability is over what an opponent can arrange, because the arrangement is cheap — one unit
per exit is the Gravedigger’s whole economy. So it is closers / (degree × horizon), cross-multiplied
and capped: a stack with no exits is already there and carries its whole value; a stack with more exits
than the opponent has units in reach carries almost none. A stack that cannot retreat at all (CB-1340)
carries none by this mechanism — its danger is a different clause’s, and folding it in here would
double-count the garrison the Floor is obliged to leave in place.
The defensive consult counts the exit the newcomer costs, not only the value it adds: CB-1120 needs room for the whole retreating stack, so a tile with four units needs four spaces and joining can remove a way out as well as adding something to lose. The offensive consult weights the prize by the chance of actually breaking in and against what the attacker stakes — which is what stops it becoming "attack every cornered stack", the Gravedigger run against oneself.
A reading recorded for the spec owner: AI-1165 names a horizon and does not fix its length. Three turns is taken here, and not arbitrarily — it is the window in which a closer can actually walk onto an exit; one further off arrives to find the stack somewhere else.
Mutation pass: 11 of 11 killed.
A note on how this slice was found, because it is the tooling working as documented. pnpm built-untested surfaced AI-1165 as "claimed by source and named by no test" — and the source claiming
it was exploits.ts, the AI-1300 register written earlier the same day, not an implementation. That is
exactly the caution AGENTS.md gives about that query: "a citation can be a signpost rather than an
implementation claim, so read the site before believing it." The signpost was right about the gap and
wrong about the reason, which is the useful half. With this landed the register is down to the three
§18.4 farm rows and nothing else.
AI-1170 The Staff MUST have access to an incremental supply oracle — ⚠ flagged gap, 2026-09-23: this is an ask on 12-economy.md, which §22 records; the clause itself says what to do until then — “where an implementation cannot provide one, the Staff MUST NOT evaluate supply-cut Operations at all rather than evaluate them from stale data”, and it does not —: the ability to answer "what does Reach look like if this tile changes hands" without a full re-propagation of 12-economy.md's flood fill (EC-1560). Where an implementation cannot provide one, the Staff MUST NOT evaluate supply-cut Operations at all rather than evaluate them from stale data.
*The second clause is held as of 2026-09-24 (client/test/supply-oracle.test.ts):
- The Docket is composed from exactly the five built generators, none of which targets a route.
- Raid and Interdict are listed in
generate.ts'sMISSING, each waiting on a route model. - No Staff module names core's supply propagation.
The day a supply-cut Operation arrives, it arrives through that test.*
Rationale: cutting a supply chain at an articulation point is one of this game's strongest moves, and a naive implementation that recomputes propagation per candidate is either unusably slow or quietly supply-blind. Supply-blind is far worse and far more likely, because it is invisible — the AI simply never plans an encirclement and nobody can say why. Refusing to evaluate is honest; evaluating from stale data is not.
AI-1180 The Staff MUST maintain a horizon estimate — expected remaining turns — computed from 14-victory.md's published Ebb schedule and the Horizon, and MUST price every option whose correctness depends on remaining game length against it. An option that repays over a longer window than the horizon estimate MUST NOT be taken, and an option that is a deliberately bad trade taken only when losing MUST NOT be taken while the Staff leads.
Built 2026-09-23 as client/src/ai/horizon.ts, closing §18’s E16 — "loser’s buttons pressed while
winning: options whose correctness depends on remaining game length or on losing". That "or" is why
the implementation is two tests rather than one score: an emergency measure that repays next turn
sails past any horizon check, and a single number satisfies neither prohibition cleanly. The refusals
are named rather than counted, and the horizon test runs first, so an emergency measure that is also
too slow is refused for being too slow — saying "loser’s button" would be a true sentence about the
wrong thing, and the Debrief shows the sentence.
The estimate is the smaller of two published facts: the turns to the Horizon, and the turns until the seat believes a Track becomes decisive after the next Ebb step. VC-830 publishes the schedule "to every player from turn 1 as an evaluable function" and VC-850 makes it the same for everybody, so there is no fog over the calendar and this estimate costs nothing.
Rationale: this class of decision — "am I about to lose?" and "how long will this game last?" — has no term in a naive evaluator, and 14-victory.md has already handed the answer over: the Ebb schedule is published from turn 1 and is a closed form, the Horizon is a hard backstop, and VC-2600 through VC-2620 make the elimination arithmetic exact. An AI that presses a loser's button while winning reads to a player as broken.
9. Commit before you see
The Staff issues orders blind, against the board as the previous Reckoning left it (10-turn-model.md
TM-030, TM-340), exactly like every other seat. docs/design/01-decision-turn-model.md names the
conditional vocabulary as the model's largest execution risk. For the Staff it is also the largest
opportunity, because unlike a human it can afford to actually evaluate the branches.
9.1 Hypotheses and risk
AI-1200 The Staff MUST evaluate each candidate plan against Y opponent hypotheses (Y by Rank)
and MUST combine them by an integer risk-weighted score:
mean = Σ_H outcome[H] × prior[H] / 1000
worst = min over H of outcome[H]
score = mean − k × (mean − worst) / 1000
The hypothesis set MUST be drawn from: Passive (every Contact holds; production continues; prior 250‰), Continuation (every Contact keeps its last-observed heading and pace, and unaccounted force appears on the shortest axis toward the Staff's weakest city; prior 500‰), and Best reply (a shallow Staff run for the opponent over the modelled view AI-587 constructs; prior 250‰). Best reply MUST be available only at the top Ranks.
AI-1210 k is the caution coefficient, an integer per-mille, and MUST be a personality dial
(§14). At k = 0 the Staff is an expected-value maximiser that will bet the game; at k = 1000 it is
a pure minimaxer that never attacks. The shipped baseline MUST be 500‰.
Rationale, because this single integer is doing the strategic work 12-economy.md was built to create.
A blitzing position — many cities, most of them barely integrated, thin garrisons, a long amber supply
frontier — evaluates higher on the Assay than a consolidated one, and that is correct: it is worth
more. The mortgage does not show up as a lower level. It shows up as spread. Under the Passive
hypothesis "keep expanding" scores brilliantly; under Continuation with a large unaccounted force it
scores badly, because un-integrated cities have thin garrisons, a broken Reach chain isolates a whole
axis, and 12-economy.md's Loyalty memory means every one of those cities snaps back to full value the
moment its original owner walks in. So a Staff at k = 300‰ keeps expanding and sometimes gets away
with it; at k = 750‰ it consolidates; and at k = 500‰ it expands while the unaccounted-force
estimate is low and consolidates when it rises. That third behaviour is the one we want, it is emergent
rather than scripted, and it is legible.
AI-1215 City valuation MUST include the value a capture would hand back to its original owner on recapture:
value = expectedIntegratedWorks × P(hold ≥ N turns)
− garrisonCost − cadreOpportunityCost
− recaptureValueToOriginalOwner × P(they retake it)
and the Staff MUST be able to decide not to take a specific city — which requires the planner language of AI-580, because 13-command.md CM-700's Seize Sanction expresses a policy over a class of cities and cannot name one.
9.2 Mixing, because a pure strategy is a solved strategy
AI-1220 The Staff MUST mix over plans, never over actions. Given candidate plans scored as
descending integers with best = score[0]:
keep i where score[i] × 1000 ≥ best × (1000 − EPS) -- EPS default 50 (5%)
weight[i] = score[i] − floor(best × (1000 − EPS) / 1000) + 1 -- integer, ≥ 1
r = hash64(salt, turn, playerId, "plan") mod Σ weight[i]
select the first i whose running weight sum exceeds r
The draw MUST be positionally addressed on (salt, turn, playerId, slot) rather than taken from a
stream cursor, so that it is unaffected by the order in which the Staff happens to evaluate things.
AI-1225 The mixing salt MUST be a separate secret value generated at game creation, stored in
the save so that replay and farm reproduction are exact, and never displayed. It MUST NOT be
derived from the game Seed, which is conventionally shown at setup.
Rationale: this is the load-bearing detail and it is easy to get wrong. If the mixing stream derives from the visible seed, a determined opponent holding a copy of the binary can compute the Staff's plan choice offline and the mixing has bought nothing. In ranked AI-versus-human play, where replays must verify, the salt needs a commit-reveal — its hash published at game start, the salt itself at game end — which is Open question 8.
AI-1230 Mixing MUST NOT reach: retreat-degree checks, supply state, garrison thresholds, Cadre escort decisions, air endurance, or anything else in the Constraint Floor (AI-560). The Staff MUST mix over aggression and axis of advance, never over survival.
Rationale: a player who watches an AI randomly abandon a garrison concludes the AI is broken, and they are right. Against a non-adaptive opponent mixing is a pure loss — the Staff sometimes takes its second-best plan. The case for it is that a human learns the AI's policy across turns and across games while the AI does not learn theirs within a game, and Orders and the Cascade makes each turn a simultaneous-move game in which a pure strategy known to the opponent is dominated by the best reply. That is not a heuristic worry; it is the definition of exploitability.
AI-1235 The mixing width EPS MAY be adaptive within a game, raised when a cheap regret signal
fires (repeated net-loss outcomes from plans of the same class) and lowered while the Staff is winning.
This MUST be the only online adaptation the Staff performs, and it MUST change how much the Staff
mixes, never what it believes (AI-680).
9.3 Strategic route selection
AI-1240 The Staff MUST randomise among near-cost-equal strategic routes: it MUST enumerate Atlas routes within 110% of the cheapest and choose by seeded draw from its own stream, keeping the choice replay-exact.
Rationale: 03-architecture.md AR-750 requires deterministic pathfinding with tie-breaks on lowest tile index, and 14-victory.md publishes the Tally calendar 24 turns ahead. Together those hand an opponent three things — where the Staff must be, when, and by which route, computable to the tile — and pre-placing entrenched reaction units on that corridor is a free ambush. This is the clearest case in the design where determinism of the engine and predictability of the agent must be separated: the engine must be deterministic for replay; the agent must not be predictable to an opponent holding the same rules.
9.4 Using the activation order the Staff is entitled to
AI-1250 The Staff MUST attach a condition to every committed order whose target is contestable, and its measured stance-fallback rate MUST be at or below the human median (AI-1420). The Staff MAY assign contingent Operations preferentially to low-Initiative formations, whose orders are evaluated against more of the turn (10-turn-model.md TM-280), and races for fixed objectives to high-Initiative formations; and MAY select Formation commanders for activation freshness under 13-command.md CM-960. All of this MUST use only its own units' relative order (AI-040).
Rationale: an AI can afford to enumerate conditions and a human cannot. If the Staff's stance-fallback rate is above the human median, the conditional vocabulary is buying the AI nothing and the central bet of the turn-model decision has failed on the AI side specifically — which is a finding worth having early, and AI-1420 is what surfaces it.
Partially held 2026-09-28: the own-orders half stands; conditional orders do not exist yet, so their stance-fallback rate has nothing to measure against a human median. The committed-order surface carries no condition — none of the order kinds in packages/ai/src/search.ts has one and nothing attaches it — and packages/client/test/mixing.test.ts disclaims this row rather than covering it (its file-level opt-out list names both AI-1250 and AI-1260). What waits: the condition itself, and behind it the measurement this row defines — AI-1420 is what would surface a too-high rate.
AI-1260 The Staff SHOULD solve a small deterministic set cover for reaction screens: for each believed approach corridor into its territory, place at least one unit in a Stance that carries a live reaction adjacent to it. Candidates MUST be bounded — at most 64 corridors and 256 candidate tiles — with deterministic tie-breaks.
Rationale: this costs almost nothing and produces the single most "how did it know" behaviour available in this turn model — a picket line stopping a raid it could not have seen coming, because it was covering the corridor rather than the unit.
no-test-needed: there is no set-cover solver in this tree to pin — 'corridor' in packages/ai/src belongs to Rehearsal's coverage checks and to position-graph properties (rehearse.ts, compile.ts), gunline.ts answers AI-1304's concentration mechanic rather than reaction-screen placement, and mixing.test.ts disclaims the row. It becomes testable when — if ever — a solver lands.
10. Budgets — work units, not wall clock
10.1 The rule everything else rests on
AI-405 The Staff's internal budget MUST be an integer step counter, never a clock. A step is one unit of declared work — one Assay delta, one Sandtable turn, one greedy marginal, one exact combat preview — each with a declared integer cost. A Rank's budget MUST be a fixed integer step count, and the search MUST be structured so that a complete legal plan exists after greedy assignment and every subsequent local-search move is optional, so that truncation at the step count always yields a committable plan.
AI-415 The Staff's decision procedure MUST be a total function of (player view, configuration, Rank, salt). Elapsed time MUST NOT be an input. There MUST be no iterative deepening cut off by a clock, no "spend the remaining milliseconds", and no adaptive work count. A slow device MUST take longer; it MUST NOT play worse.
Rationale, and the reconciliation of two proposals that appeared to disagree: the search is anytime in structure — it can stop at any checkpoint and commit — but the stopping point is a fixed integer step count, not a deadline. Three things fall out, and they answer three separate questions at once. Phone versus desktop versus farm becomes one knob: same code, same algorithm, different integer step budgets, so there is no mobile AI and no farm AI, only one AI with a number. Determinism survives time-boxing, because truncation happens at the same step on every machine — a wall-clock budget would make a faster computer play a stronger game, which is a fairness defect in multiplayer and makes the self-play farm irreproducible since the same experiment would explore a different search on every host. And difficulty stays honest, because difficulty is the step budget plus knowledge discipline rather than a hidden bonus. Wall clock survives only as the fault watchdog of AI-420.
AI-417 A device that cannot complete a Rank's step budget within its watchdog MUST NOT offer that Rank, and the setup screen MUST say so ("this device supports up to Colonel"). It MUST NOT silently run a weaker version of a Rank. Desktop headroom buys more Ranks, never a secretly stronger Captain.
10.2 The budget table
AI-400 The host MUST enforce a per-turn budget with a soft target and a hard cap, configured per context, with every value disclosed in the lobby and recorded in the game configuration:
| Context | Soft target (default) | Hard cap (default) | Enforced quantity |
|---|---|---|---|
| Local interactive game | the Rank's step budget; wall-clock calibration target 2 s + 10 ms × own-unit count, max 15 s | 60 s watchdog | steps for the shipped Staff; fuel for a plugin where AI-410 requires it, otherwise the AI-420 watchdog |
| Real-time multiplayer, server-run | lobby-configured, default 10 s calibration target | 30 s watchdog | steps; fuel for plugins |
| Correspondence / cloud turn | 60 s calibration target | 300 s watchdog | steps; fuel for plugins |
| Simulation farm and tuning | experiment-configured | experiment-configured | steps; fuel for plugins |
The seconds in this table are calibration targets on REF-HW (03-architecture.md AR-700), not the enforced quantity. The enforced quantity is the Rank's step budget for the shipped Staff and the fuel figure of AI-410 for a plugin; it is that figure, with the transform id and version, that the recorded game configuration carries (03-architecture.md AR-857).
AI-610's step budgets, and estimated wall-clock cost on REF-HW headless. Multiply by 2× for a browser tab and 2× again for a 2019-class ARM device, per AR-700. Every millisecond figure is an estimate.
| Rank | Step budget | Est. ms, reference midgame | Est. ms, late large |
|---|---|---|---|
| Cadet | 1,500 | 3 | 12 |
| Lieutenant | 6,000 | 10 | 40 |
| Captain (default) | 25,000 | 35 | 140 |
| Colonel | 80,000 | 100 | 400 |
| General | 160,000 | 200 | 800 |
| Marshal | 250,000 | 300 | 1,200 |
AI-407 The latency that mostly is not there. The Orders phase is concurrent and the board is stable throughout it (10-turn-model.md TM-340), so the Staff MUST begin planning as soon as the new views are published and MUST run off the UI thread (AI-440). In solo play the Staff's latency is therefore hidden behind the human's own thinking and behind Cascade playback. The Staff MAY additionally use 10-turn-model.md TM-2550's rolling planning on the same terms a human client does: once a unit has activated in turn T's Cascade, planning for T+1 may begin, subject to TM-2570 — nothing reaches the core until the next Orders phase opens.
10.3 Degrading without a cliff
AI-457 Where a platform cannot afford a Rank's full search, the quality knob MUST be the breadth of the plan portfolio, never the depth of the Constraint Floor and never the belief model. The Staff MUST degrade by evaluating fewer strategic seeds with shallower Sandtable depth, and MUST NOT degrade by dropping opponent hypotheses to one, dropping the belief filter, or dropping rollout to zero.
AI-458 Plan continuation MUST be the default. The Staff MUST keep the previous turn's plan and re-validate it cheaply, running a full re-plan only on an AI-545 trigger. The measured full-re-plan rate MUST sit at or below 20% in steady state (AI-1430).
Rationale: under budget pressure a team cuts in order of visibility rather than value — rollout depth first, then hypothesis count, then opponent modelling, then belief maintenance, which is the least visible of all. The first two are phase changes, not degradations: at rollout depth 0 the Staff is a greedy per-unit scorer and every exploit in §18 becomes available at once, and at one hypothesis it is a pure strategy and therefore maximally exploitable. So the honest answer to "does it degrade gracefully on a phone" is no, not unless the knob is changed — and changing the knob to portfolio breadth makes the mobile AI narrower, not dumber, with a floor of "a competent operational plan, executed coherently" rather than "greedy per-unit scoring." AI-1460 turns that into a shipping gate.
Partially held 2026-09-28: the trigger discipline that limits re-plans stands, and the 20% ceiling is a constant; plan continuation itself does not exist yet. The triggers are pinned at packages/client/test/triage.test.ts's describe 'AI-545 and CM-1630' — its it 'the triggers are AI-545's own list' asserts, among other things, that a single lost unit does not re-plan — and the ceiling stands as REPLAN_RATE_CEILING_PERMILLE in packages/ai/src/budget.ts, pinned under packages/ai/test/budget.test.ts's describe 'AI-457, AI-458 — degrading without a cliff'. What waits: continuation itself; drawUp takes no previous plan (the row's own flagged-gap note names it), so today every turn re-plans at 100% and the ceiling bounds only its absence.
10.4 Memory
AI-435 The Staff's working set MUST be dominated by structures whose size is independent of unit count and nearly independent of map size — the Atlas and its cached distances, the chunk threat field, the Contact ledger, the Docket, the plan store, and the Sandtable's working state — and MUST remain under 4 MiB at every scale and every Rank. Estimated composition at the largest map: Atlas nodes plus all-pairs distances across three movement classes ≈ 384 KB; threat field ≈ 32 KB; Contact ledger ≈ 13 KB; Docket ≈ 13 KB; plan store ≈ 17 KB; Sandtable working state ≈ 200 KB.
Rationale: this is the happy consequence of the abstraction, and it arrived before any of the others. The Staff reasons about formations and places, not units and tiles, so its memory is structural rather than proportional.
AI-430 An AI plugin instance's linear memory MUST be limited by the declared maximum of its
WebAssembly.Memory, so a growth attempt past it traps in the engine rather than being discovered by
host bookkeeping.
⚠ Flagged gap, 2026-09-23: there is no plugin ABI. §12 stands at 9% and is honestly unbuilt;
"the declared maximum of its WebAssembly.Memory" describes a host that instantiates WASM modules,
and this one does not yet. Recorded rather than faked. Defaults and caps:
| Platform class | Default per instance | Manifest-requestable cap | Aggregate, all AI instances |
|---|---|---|---|
| Server (Node, REF-HW class) | 256 MiB | 1 GiB | 2 GiB |
| Desktop client (Tauri web view or browser tab) | 256 MiB | 1 GiB | 2 GiB |
| Mobile client (Tauri web view, 2019-class ARM) | 128 MiB | 512 MiB | 768 MiB |
AI-instance memory is additional to 03-architecture.md's simulation-state budgets and MUST be accounted separately. A manifest request above the platform cap MUST NOT be a load failure: the host MUST grant the cap, warn at setup, and let the plugin degrade. Exceeding the granted limit is a fault under AI-240. On every client platform the AI aggregate shares one process with the whole UI and the AI-235 core replica, so the host MUST treat the aggregate cap as a hard client-wide budget and serialise AI turns rather than approach it. These numbers are engineering estimates and Open question 11 tracks their measurement.
no-test-needed: an instance's linear memory is bounded by its declared WebAssembly.Memory maximum only when instances exist — none can, in this tree (WebAssembly appears nowhere under any package's src), so no growth attempt can trap; packages/client/test/ai-budgets.test.ts records the clause as a flagged gap for exactly this reason. It becomes testable when the §12 host lands.
AI-440 AI computation MUST run off the UI thread — a Web Worker in the client, a worker_threads
worker on the server — and the client MUST remain responsive and MAY show progress. Worker isolation is
an enforcement mechanism, not only a responsiveness measure: it is what makes AI-420's interrupt
possible at all, and what keeps a runaway plugin from freezing the client's render and input loop. The
worker MUST hold the plugin instance and, where one exists, the AI-235 core replica, and the host MUST
be able to terminate it unilaterally at any point and recover under AI-240.
AI-450 Parallel evaluation MUST be a host service rather than plugin threads — ⚠ flagged gap, 2026-09-23: the same reason as AI-430, and it names "the plugin’s pure evaluation export", which is an ABI surface that does not exist —: a batch call that runs each task by invoking the plugin's pure evaluation export on isolated instance copies across available workers, returning results in submission order. Results MUST be bit-identical regardless of worker count or scheduling, and batch fuel cost MUST be the deterministic sum of task fuel costs. Plugins MUST NOT be given raw threading primitives, and the service MUST NOT be implemented with WASM threads or shared memory (AI-380(a); also, that would force cross-origin isolation on the web client). Where workers are unavailable (03-architecture.md AR-860), the batch MUST execute sequentially in submission order with identical results and identical fuel accounting; only wall-clock time differs.
no-test-needed: parallel evaluation as a host service presupposes plugin threads to be ruled out and batch calls to exist — neither does, in this tree (the row's own flagged-gap note says so), and packages/client/test/ai-budgets.test.ts records it alongside AI-430. It becomes testable with the §12 host.
10.5 Headless operation
AI-470 The Staff and every shipped AI MUST run fully headless — no UI, GPU, DOM or platform-service
dependency — under Node on Linux servers against @everylastcity/core, in the simulation farm
(03-architecture.md AR-970), in CI, and in a browser worker with no DOM access.
Held 2026-09-23 as the headless rule in scripts/check-ai-fairness.mjs, the ninth rule in that
gate. The Staff lives inside packages/client, which is full of DOM code — so the only thing
between it and a document reference is that nobody has written one yet, which is precisely what a
scan is for and a test is not. Confirmed to fire against planted document, window and
localStorage references.
window. and document. carry their dot in the forbidden list, because three modules discuss an
Operation's time window in prose and a bare word would make the documentation the violation — the
third time in one day a structural guard had to be taught the difference between a rule and a sentence
about it. The plugin host MUST be
a single implementation across all of those; the only permitted environment-specific code is the worker
primitive of AI-440 and the module-loading path, both behind one interface.
Rationale: the farm and the client must exercise the same host, or the farm's determinism evidence says nothing about what players run.
Partially held 2026-09-28: headless operation is real and its checker rule has proven teeth; what waits is the row's host-side half — one implementation for every environment. The Staff lives inside packages/client, which is full of DOM code, and the only thing between it and a document reference is the headless rule of scripts/check-ai-fairness.mjs (the ninth in that gate), worded 'AI-470 — the Staff runs under Node, in CI, in the farm, and in a worker with no DOM' and pinned by scripts/test/ai-fairness.test.ts; this block's own Held note of 2026-09-23 confirms it fires against planted document, window and localStorage references. What waits: the plugin host as one implementation across all of those environments — §12's cluster, unbuilt.
AI-475 The CLI MUST provide the farm drivers this document's pipeline needs — ⚠ flagged gap, 2026-09-23: a head-to-head driver, a gauntlet and a tuning driver are a farm, and §18.4’s three rows wait on the same thing — on top of 03-architecture.md AR-810's existing verbs: a head-to-head match driver supporting fixed-N and sequential testing; a gauntlet driver scoring a candidate against an anchor pool; and a tuning driver that emits matches and consumes results under a named optimiser. Every job MUST be content-addressed by a hash of its own inputs — map spec, game seed, ruleset, victory terms, per-seat agent and manifest and model hashes, pair index, hashing policy — so that a disputed result is re-runnable by anyone with the same build.
no-test-needed: the farm drivers do not exist — the CLI ships exactly new, play, run-ai, apply, save, replay and hash (packages/cli/src/main.ts), so a head-to-head driver, a gauntlet and a tuning driver have nothing to be tested against; §18.4's three rows wait on the same thing, as this row's flagged-gap note records. It becomes testable when the first driver lands.
11. Determinism and replay integrity
11.1 What is already settled, and what it buys
AI-350 Every order an AI seat submits MUST be appended to the authoritative order log exactly as a human's is, in the same envelope (03-architecture.md AR-250, AR-260). A replay MUST NOT re-invoke AI code (10-turn-model.md TM-2420); replays and desync checks derive purely from the order log and are exact regardless of AI determinism. No AI-produced datum MAY exist only in the derived event stream, because events are a re-derivable cache and never authoritative (AR-250).
Rationale, and the consequence worth naming: this decouples AI determinism from replay exactness, which
means an experimental AI cannot corrupt a saved game, and it means the AI can improve every release for
the life of the product without invalidating a single replay. Contrast 13-command.md CM-2050, where
changing how a Screen picks its tile is a rules change with a commandLogicVersion bump. Command-layer
changes are expensive and rare; AI changes are cheap and continuous, and whoever schedules the work
should know that.
AI-352 A change to the Staff's weights, evaluation model or planner MUST NOT bump
commandLogicVersion (13-command.md CM-2050) and MUST NOT be a RulesVersion change
(03-architecture.md AR-880). The command layer decides what a Posture does; the Staff only decides
which Posture it sets, and setting a Posture is an ordinary order any human could issue.
11.2 Where AI determinism is required anyway
AI-360 Every shipped AI MUST be strictly deterministic: given the same view sequence, options, advisory memory, salt and seed, it MUST produce an identical order sequence on every platform. Shipped AIs MUST take all randomness from AI-390's streams. The only budget observation any plugin may make is fuel, which is deterministic, so a shipped AI MAY shape decision content on it; there is no wall-clock observation in the ABI to depend on (AI-210).
§11 was taken on 2026-09-23 and stands at 10/12
(packages/client/test/ai-determinism.test.ts); the two still missing are AI-370 and AI-380, both of
which are about a plugin ABI that does not exist — see their own flagged notes.
AI-352 is held the way the clause is shaped: no AI module names commandLogicVersion or the
RulesVersion at all, so a Staff change cannot bump either by construction — an invariant fifteen AI
slices landed the same day relied on without anything asserting it. The second row gives the reason
rather than the assertion: what the Staff emits is the ordinary order vocabulary, read out of
OrderInput's union rather than listed in the test, because an enumeration in a test is a second copy
of the catalogue AR-270 owns — and a first draft that listed it got the list wrong twice.
AI-360’s two halves: the same view and options produce the same plan twice, and no AI module draws
ambient randomness or reads a clock. check-ai-fairness.mjs already forbids the same calls as a
fairness rule; the row here states it as the determinism one, because they are different
requirements that happen to forbid the same thing.
AI-362 AI determinism is required for four things replay exactness does not give: farm reproducibility, desync arbitration, bug reproduction, and the fog-leak gate of AI-1400. It is required for nothing else.
AI-370 A third-party plugin MUST declare deterministic truthfully in its manifest. A
non-deterministic plugin remains playable — AI-350 keeps replays exact — but MUST be excluded from every
context that re-executes AI decisions for verification: farm reproduction runs, desync arbitration, and
any ranked mode 05-multiplayer.md marks verification-required. The host SHOULD spot-check a determinism
claim by occasionally re-running a turn from the pre-turn state and comparing order streams; a mismatch
flags the plugin.
⚠ Flagged gap, 2026-09-23: this is about a plugin manifest, and there is no plugin ABI. §12, the AI interface, stands at 4% and is honestly unbuilt. The clause is recordable and not testable here, and naming its id in a test would move §11 to 12/12 on the strength of something the repository cannot do — the trap the AI-1430/AI-1440 slice hit the same morning. It becomes testable with the ABI.
Note: with no clock and no ambient entropy in the import set (AI-210), deterministic: false declares
an internal property of the module — a NaN-payload-derived value, reliance on host-observable memory
layout, or an engine-dependent construct module validation does not reach — and never a capability the
host granted. A plugin cannot become non-deterministic by reading something; only by computing
something.
no-test-needed: truthfulness in a plugin manifest is recordable and not testable here — this row's own flagged-gap note says it plainly, there is no plugin ABI in this tree (WebAssembly appears nowhere under any package's src), so no third-party declaration exists for the CI of AI-375 to check against. It becomes testable with the first non-shipped plugin.
AI-375 CI MUST assert that the same recorded player view, replayed on machines of materially different speed, produces byte-identical order sets. A difference is an AI-415 violation and a release blocker.
11.3 Arithmetic at the boundary
AI-380 WASM arithmetic is bit-exact across compliant engines with three named exceptions that are not configurable when the engine belongs to the platform (AI-125). The host MUST enforce them by module validation before instantiation, refusing to load a module that: (a) declares or imports shared memory or the threads feature, or uses atomic instructions; (b) uses relaxed SIMD instructions — deterministic SIMD is permitted and, for an integer inference kernel, is the one place in this project where SIMD is both permitted and worth having; (c) imports any transcendental or otherwise engine-supplied math function. Such functions MUST be compiled into the module, so their implementation is fixed by the plugin binary rather than by whichever engine is running it. Refusal MUST name the offending feature or import in a user-visible message. Beyond validation, a plugin MUST NOT let NaN bit patterns influence any value it returns; that obligation is 07-modding-content.md MOD-600's and sits on the plugin. Shipped AIs MUST NOT produce a NaN in any computation whose result reaches an order, and MUST assert on one in debug builds.
Partially held 2026-09-28: the shipped-AI half stands where bit-exactness is pinned; host enforcement of clauses (a)–(c) waits on an engine to enforce in. What actually ships cannot leak a float across the boundary: packages/client/test/ai-determinism.test.ts walks every numeric leaf, however deep, under its describe 'AI-385 — no floating-point value crosses the AI ABI' — fractional and non-finite values alike fail there. What waits: clauses (a) through (c), host-side enforcement against engines whose arithmetic cannot be configured; no WASM engine is exercised in any package's src today, so there is nothing to enforce them in.
AI-385 No floating-point value crosses the AI ABI.
Held 2026-09-23 over what the Staff emits, and that is a correction a mutation pass forced. The
first draft walked the journal — and the journal holds the orders the authority accepted. A
fractional field makes an order the engine refuses, so it never arrives there, and the row could not
tell "no float crossed" from "a float was rejected". AI-385 is about what crosses the boundary, and
constraintFloor's output is the boundary. Every numeric leaf of every emitted order is walked, to
any depth, and checked for both fractional and non-finite values — the second because
Number(undefined) is NaN, which is the conversion this clause forbids and a trap this repository has
met more than once.
One citation had to be followed rather than trusted: AI-385 names MOD-600's rule, and MOD-600 is
07-modding-content.md’s determinism clause — the float rule is MOD-580’s "No floating point across
the ABI". That document’s own §5 note records that this pointer has been re-resolved once already —
its float-discipline paragraph formerly cited an identifier that has since been retired, and both
pointers were re-resolved together — so the test holds
the rule by its words rather than the number, and this note records that AI-385’s pointer is one
the modding document has moved under it. For the spec owner. Every field of every submitted order MUST be
an integer, identifier or enum of the order catalog (03-architecture.md AR-270), and every numeric field
the view namespace returns MUST be delivered in the core's own integer or fixed-point representation
(AR-080 owns that representation and this document does not restate it) in the canonical encoding, never
rescaled, normalised or passed through a number conversion by the host's marshalling layer. This is the
AI-surface instance of 07-modding-content.md MOD-600's rule and the two MUST agree. Inside a plugin,
float arithmetic is permitted and this document does not ban it; the shipped Staff gives that permission
up under AI-1010, and a third-party plugin MAY do the same but is not required to.
Rationale: the host is written in a language with one numeric type, so a well-meaning marshalling helper that divides a per-mille value by 1000 to present a friendlier number is a live hazard. Keeping the boundary integer-only makes that class of bug a type error rather than a rounding difference that surfaces as a desync six months later. The boundary is also where the whole hazard lives: a float that never leaves the guest cannot desync a game, because what crosses into the log is an order, and an order is integers.
AI-390 The host MUST serve every AI random draw from a deterministic AI-private stream belonging to the calling seat alone. 03-architecture.md AR-370 owns the derivation normatively and this document MUST NOT restate it; what the AI framework guarantees on top of it is:
(a) Private per seat — no plugin can read, replay or influence another seat's draws (AI-340), and no
draw permits recovery of the game Seed, of any rules stream, or of another seat's stream (AI-195).
(b) Successive draws differ — repeated draws on the same key within one turn yield independent
values, so a plugin may draw as often as its budget allows without special-casing the key.
(c) Reproducible — the same AI, at the same seat and turn, taking the same sequence of draws, receives
the same values on every platform and every re-run. This is what makes AI-360 testable.
(d) Outcome-neutral — AI streams are independent of the rules streams that resolve combat and
production, so AI consumption never perturbs game outcomes, and AI stream state is not simulation state.
AI-195 The game root Seed MUST NOT be handed to any AI plugin, and no value passed to a plugin may
permit its recovery. The seed field a plugin receives at creation MUST be an AI-private seed: a
64-bit value one-way derived by the host from the game Seed, the seat id and the RNG version — a keyed
or cryptographic hash, not a rules-stream expansion — such that a plugin holding it cannot reconstruct
the Seed, any rules stream's state, or any other seat's streams. The same non-recoverability rule MUST
hold for the worldgen seed a generator plugin receives (03-architecture.md AR-400), otherwise an author
could recover the Seed by shipping a paired worldgen plugin and reading it out of the generation
request.
Rationale: AR-370 publishes the exact stream construction, so the root seed is equivalent to read access to every future combat roll and to unexplored map content. Handing it over would silently defeat AI-020, AI-030 and AI-300. The AI-private seed exists only so that a plugin preferring its own internal PRNG stays deterministic.
11.4 Advisory memory
AI-460 An AI seat's persisted memory blob MUST be stored as a separate save-container entry
outside hashed simulation state — ai/<playerId>.<OrderSeq>.bin, versioned and size-capped at the
instance memory limit — so that resuming a saved game restores AI intent: standing plans, target lists,
the force-accounting integrals, the switching-cost baseline. AI blobs MUST NOT be embedded in the initial
state, the order log or snapshots, MUST NOT contribute to any turn or state hash, and MUST be excluded
from snapshot verification (03-architecture.md AR-570). They are covered by the ordinary per-entry
integrity table like any other container entry.
AI-465 AI blobs MUST be droppable: a container stripped of its ai/ entries — for replay
distribution, bug-report bundles, or fog-safe transfer — MUST still verify and replay exactly, and
readers MUST treat an absent, unreadable or version-mismatched blob as "no AI memory" rather than as
corruption. Blobs are an optimisation, never authoritative: if restore fails or the blob is absent, the
Staff MUST continue from the current view alone, degraded but legal and competent — never a crash, never
a refusal. A writer MUST retain at least the newest entry per AI seat.
Implementation note, 2026-09-12, revised 2026-09-17. AI-460’s contents are built —
client/src/ai/memory.ts carries the force-accounting integrals, A30’s 20-turn window and AI-535’s
switching-cost baseline, versioned and capped — and since 2026-09-17 the memory survives a save:
serialize and restore (the two functions §12.3’s table already named) write it beside the browser
save’s record, and opening a game hands every AI seat its own memory back. Before that it lived in a
useRef for the length of a session, so reopening a save was amnesia — defensible under AI-465, and
not what a player who saved their game is asking for.
Three properties of that home are worth stating, because each is a requirement read literally. It is not in the snapshot (03-architecture.md AR-660): AI-460 says a blob "MUST NOT be embedded in the initial state, the order log or snapshots", so it sits beside the record instead — which also means a save whose snapshot is missing still restores its memory, and a snapshot copied between machines carries no seat’s private reasoning. Every refusal is total: an absent, unreadable, version-mismatched, oversized, duplicated-seat or malformed blob answers "no memory" and never throws, which is AI-465’s "never a crash, never a refusal" made a property of the function rather than an argument about the guards being complete. And a version that is not this one is discarded, never migrated, which is what the version number is for.
Server side, 2026-09-29. Until today the §11.4 memory had one more home that did not keep it: the server’s designated seats ran in-process and remembered between turns but lost everything at a restart — AI-465’s degraded path was the only path a restarted service took (recorded as the gap under MP-160 on 2026-09-28). packages/server/src/seat-memory.ts now carries one versioned blob per game in its own file (seat-memory.json) beside the durable container: identity-keyed by the game’s gameId and this build’s unit set (US-2100 names the set before replaying anything against it), rewritten after every plan, and read exactly once when a host is constructed for that game’s identity — so a resumed game replays its orders exactly and hands each designated seat back what it had measured. The blob enters no AR-260 chain, no stateHash and no integrity table of the container (AI-460 read literally: another file with its own name), one bad side costs exactly that half, and every way the file can be wrong degrades to no memory rather than an error (AI-465) — a Finished successor overwriting the same slot fails identity and starts amnesic. Pinned in packages/server/test/seat-memory-durable.test.ts: byte-identical plans after a simulated restart, host-level continuation across crash-and-restart beside a real durable log, and every degraded read total.
The container entry ai/<playerId>.<OrderSeq>.bin is still unbuilt, and the reason has changed:
it is no longer the save-container format (that exists — cli/src/container.ts), it is that nothing
would write one. The CLI runs no AI seats, so a container entry would be a format with no producer,
which is this codebase’s recurring defect class. It comes due when the CLI plays AI seats (AR-840’s
unattended runs) or when the client exports a container, whichever arrives first.
AI-465 turned out to be the requirement that shaped the implementation rather than a fallback clause on it. Because the memory has to be optional, it is an optional argument that defaults to absent — so every caller that predates it keeps working untouched, and the features that need it (A25, A30) report limits rather than zeros when it is missing. That distinction is load-bearing: an unaccounted-force estimate of zero says "that rival is hiding nothing", and a seat told that has been lied to.
The hash-freedom in AI-460 is also structural rather than incidental. The store is threaded through
the turn closer as a separate argument and returned on the result, never hung off GameState — one
careless stateToCanonical away from moving every hash in the corpus is a place this must not be.
12. The AI interface
12.1 Two execution forms, one contract
AI-120 There MUST be exactly two forms an AI seat may take, and both MUST be seats under §3:
(a) The shipped Staff, an in-process module in the client, server and CLI, importing
@everylastcity/core directly and reaching the simulation only through the Order/View contract of
03-architecture.md AR-060.
(b) A third-party AI plugin, a sandboxed WASM module conforming to the plugin container, manifest, signing and sandbox rules of 07-modding-content.md and to the transport of MOD-660.
Partially held 2026-09-28: form (a) — the shipped Staff as an ordinary seat — stands and is pinned; form (b), a plugin, cannot exist in this tree. The Staff enters the simulation through the same order seam a human's do — packages/client/test/ai-seat-contract.test.ts pins that door (and refuses one deliberately illegal AI order with a typed rejection) — and server-side execution of a designated AI seat is pinned at packages/server/test/ai-seat.test.ts under describe 'AI-480 — the server executes a designated AI seat'. What waits: form (b), an AI as a WASM plugin under 07-modding-content.md's signing, sandbox rules and MOD-660 transport; WebAssembly appears nowhere in this tree, so there is no second form to test.
AI-122 Because the shipped Staff is not a plugin, the project MUST maintain a reference AI plugin — a complete AI built against the public ABI, shipped, playable, and covered by the same fairness gates (AI-1400) and Constraint-Floor gates (AI-1410) as the Staff. The published ABI MUST NOT be declared sufficient on any other evidence.
Rationale, and this is a decision this document makes and owns. The previous draft required every shipped AI to be a WASM plugin, on a dogfooding argument that was correct at the time. Three things since changed it. 03-architecture.md AR-850 took WASM out of the core, so a plugin now needs a core replica materialised per turn (AI-235) — real cost, unmeasured, and paid on every AI turn. 11-combat.md CB-2290 requires the AI to use the engine's own preview function rather than a cheaper approximation, and the Staff will call it tens of thousands of times per turn; marshalling that across an ABI is a worse contract than an in-process call and buys nothing. And AI-410's fuel instrumentation costs throughput precisely where the budget is tightest, on mobile. The dogfooding argument was never about the shipped AI specifically — it was about the ABI being provably sufficient — and a maintained reference plugin proves that at a fraction of the cost. What must not be lost is that both forms are bound identically by §3: the Staff has no privileged view, no privileged order kinds, and no exemption from any gate.
no-test-needed: a reference AI plugin built against the public ABI cannot exist in this tree — there is no ABI to build against (WebAssembly appears nowhere under any package's src) and nothing to play it with; the row becomes testable when the §12 host lands, at which point "shipped, playable, covered by the same gates" has a subject.
AI-125 The host MUST instantiate AI plugins through the standard WebAssembly API present in every target environment — a browser tab, the Tauri web view on desktop and mobile, and Node on the server. No embedded WASM engine may be a dependency of any build, and no platform may use an engine other than the one its runtime provides. The plugin host MUST be a single implementation shared by client and server (03-architecture.md AR-855, which owns the host constraints this document does not restate).
Rationale: the engine we would otherwise ship is already present, hardened, and continuously patched by every browser vendor, and the whole embedded-runtime layer with its per-platform build, size and CVE surface disappears. The cost is stated plainly and owned here: a platform engine takes no configuration, so the determinism switches and the fuel meter an embedded runtime gave for free must be reproduced by module validation (AI-380) and instrumentation (AI-410), and the engine is no longer one implementation we pin but every engine our platforms ship, which AI-700 must now test against.
no-test-needed: instantiation through the standard WebAssembly API presupposes engines that carry such an API — no package's src touches WebAssembly today, so there is nothing to instantiate on any platform. It becomes testable when the §12 host lands; AI-700's matrix then gains its engine span.
12.2 ABI, manifest and options
AI-130 The AI interface MUST be versioned independently as empire:ai@X.Y.Z in the WIT form
07-modding-content.md MOD-580 owns, MUST be semver-stable within a major version, and the host MUST
refuse to load a plugin whose declared major version it does not support, with a user-visible error
naming both versions.
no-test-needed: interface versioning in the WIT form owned by MOD-580 of 07-modding-content.md binds an ABI that does not exist in this tree — no semver'd AI interface to check, stable within a major or otherwise. It becomes testable with the plugin host.
AI-140 The AI plugin manifest MUST declare at minimum: plugin id, display name, author, version; ABI
version; supported unit sets (any or an explicit list); capability flags including deterministic
(AI-370) and supportsResume; requested memory limit; and the options schema of AI-152.
no-test-needed: a plugin manifest to declare — id, display name, author, version; ABI version; supported unit sets; capability flags including deterministic and supportsResume; memory limit; options schema — presupposes the §12 host; none exists in this tree, so there is nothing to validate. It becomes testable with the first plugin.
AI-152 AI options MUST be declared as a typed schema in the manifest so the client can generate a
configuration UI without knowing the plugin. Supported types MUST include bool, int with a range,
enum, permille with a range, and perUnitClass tables keyed by 02-units-and-industry.md's closed
class enumeration (US-090). Each option MUST carry an id, a localisable label and description, and a
default. No option type MAY be a float (AI-385).
no-test-needed: an option schema for a manifest that does not exist — no plugin declares one in this tree, and no client can generate a configuration UI against it; its closing clause (no option type MAY be a float, AI-385) rides along. It becomes testable with the first plugin.
AI-160 Option configurations MUST be savable and loadable as named presets per seat, and the active configuration MUST be stored with the game so that a replay or a resumed game records exactly which options were in force.
no-test-needed: per-seat named presets recorded with the game presuppose plugins that carry options — none exist in this tree (the manifest surface of AI-140/AI-152 is unbuilt), so there is no configuration to save, load or replay. It becomes testable when manifest-backed options land.
AI-170 An AI seat's display name MUST be settable explicitly in options or drawn deterministically from a user-supplied name pool. A name pool MUST be a documented plain-text or JSON file, and the drawn name MUST be recorded in the game configuration so that a replay shows the same names the players saw.
no-test-needed: a name pool drawn for an AI seat presupposes the options surface of AI-152/AI-160 — which does not exist in this tree — so there is no pool to document and no drawing to record; it becomes testable when option-backed naming lands.
12.3 Lifecycle and calls
AI-180 The plugin lifecycle MUST be exactly the one 07-modding-content.md MOD-660 owns:
create → start-game → (per turn) prepare-orders → game-over, with serialize and restore around
saves and loads, one instance per seat, memory-isolated from every other instance (AI-340). This
document MUST NOT specify a different lifecycle.
no-test-needed: this row defers the plugin lifecycle to MOD-660 in 07-modding-content.md and forbids divergence from it — a prohibition with no trigger in this build, because WebAssembly appears nowhere under any package's src and there is no host for a lifecycle to run in. It becomes testable when §12's host lands; its pin must then show create, start-game, prepare-orders per turn and game-over, with serialize/restore around saves and one memory-isolated instance per seat (AI-340).
AI-190 The plugin's exports MUST be:
| Export | When called | Purpose |
|---|---|---|
abi-version |
load | The supported ABI version |
create |
game creation or load | Instance init: game configuration, ruleset, enabled Tracks, unit set, own seat id, options blob, and the AI-private seed of AI-195 |
start-game |
turn 0 | First-look setup from map metadata and own starting assets |
prepare-orders |
once per turn, during that seat's Orders phase | Produce the seat's whole order delta for the turn |
eval-task |
inside a parallel batch (AI-450) | A pure evaluation entry point for candidate scoring |
serialize / restore |
around save and load | The opaque versioned advisory blob of AI-460 |
game-over |
end | Final notification |
no-test-needed: an export table for a plugin ABI that does not exist — WebAssembly appears nowhere under any package's src in this tree, so there is no module whose exports could be checked against it. It becomes testable when the §12 host lands; until then every row of the table has no subject.
AI-200 The host imports an AI module may declare MUST be exactly the following namespaces and no others:
| Namespace | Functions (minimum) | Notes |
|---|---|---|
view |
map metadata; tiles in a rectangle; own units; own cities; own economy; the Contact Register; the observable event stream since the previous Reckoning; rules; unit set; Track status; public player data | The fog-filtered player view only (§5) |
orders |
submit an order record, returning a typed result; commit | The same order schema human clients use (AI-220) |
predict |
the exact Clash preview of 11-combat.md CB-2250 over the caller's knowledge view, with its pivot analysis | CB-2290 forbids a cheaper approximation |
project |
closed-form economy, Reach and Track projections over a hypothetical state | §22 records the contract this places on 12-economy.md and 14-victory.md |
rng |
draw from an AI-private stream | AI-390 |
par |
evaluate a batch of pure tasks | AI-450 |
mem |
save and load the advisory blob | AI-460 |
budget |
fuel remaining this turn | Fuel only — no wall-time field, quantised or otherwise (AI-210) |
diag |
log; record a decision explanation | AI-750 |
no-test-needed: an import allowlist for a host that does not exist — with no WebAssembly surface anywhere in this tree, nothing can declare these namespaces and nothing can be refused for declaring one; the sibling fuel-and-fault rows of this cluster are disclaimed at packages/ai/test/budget.test.ts for the same reason. It becomes testable with the §12 host's import vetting.
AI-210 The sandbox MUST deny AI plugins every ambient capability: no filesystem, no network, no clock of any kind, no threads, no non-deterministic WASM feature. WASI MUST NOT be exposed. On a platform engine the sandbox is not configured but constructed — an instance can reach only what the import object hands it — so denial MUST be enforced by import allow-listing at load: the host MUST reject any module declaring an import outside the AI-200 set, by name and namespace, before instantiation, and MUST NOT populate the import object with any binding not in that set.
No clock, and no reconstructible clock. No function in the AI-200 set may return wall-clock time, elapsed time, or any value from which elapsed time can be recovered by differencing successive calls: a quantised "time remaining" is a clock, because two readings are a duration. The budget query therefore reports fuel and nothing else, and fuel is a count of instrumented operations (AI-410), not of time. This is the same denial 03-architecture.md AR-855 requires of the import object and 07-modding-content.md MOD-590 requires of the sandbox. Any host-side glue between the ABI and the engine — marshalling shims, generated bindings, worker plumbing — is part of the ABI surface and MUST NOT close over ambient host capabilities, even transitively through a generated wrapper. Import vetting MUST run again on every load, not once at install, so that a plugin substituted after validation cannot slip a new import through.
no-test-needed: every clause binds a sandbox around plugin instances, and there is no instance to sandbox — WebAssembly appears nowhere under any package's src in this tree, so no ambient capability is being denied or leaked. It becomes testable when the §12 host lands.
AI-220 Order-surface parity. The set of orders an AI seat may submit MUST be exactly the set a human could legally submit in the same state, per the order catalog of 03-architecture.md AR-270 and the command objects of 13-command.md. An illegal order MUST be rejected at submission with the typed rejection of 10-turn-model.md TM-250 and MUST NOT crash or end the seat's turn. An order that is legal when issued and unachievable at activation MUST NOT be reported as an error at any point (TM-270).
AI-230 An AI seat MUST issue its orders during its Orders phase, against the board as it stands at the start of that phase, which 10-turn-model.md TM-340 guarantees is stable for the whole phase. The host MUST NOT invoke an AI between two units' activations, MUST NOT show it any Cascade result before the Reckoning completes, and MUST NOT offer any mid-Cascade decision point (TM-050). Where a decision genuinely depends on what happens earlier in the turn, it MUST be expressed as a conditional order evaluated at activation (TM-280) — which is the compensation the turn model was chosen for, and which AI-1250 requires the Staff to use harder than a human can.
*AI-220 and AI-230 are held for the shipped Staff as of 2026-09-24 (client/test/ai-seat-contract.test.ts,
at the close in client/src/turn/close.ts). The Floor is wrapped rather than replaced, and one
illegal order is slipped into its real plan. The test shows:
- Each AI seat is asked once, from its own view, in the Orders phase and before any seat commits or the Cascade runs.
- Every order it submits enters the one journalled
applyOrderpath as that seat, never as the host. - The illegal order is refused, named in the close's notes, and neither ends the close nor stops the seat's own orders and commit.
The plugin half of both rows rides on AI-200's ABI, which is not built.*
AI-235 WASM imports are synchronous and AI-440 requires the instance to run in a terminable worker;
the host MUST reconcile the two without a synchronous cross-thread round trip, by co-locating a
@everylastcity/core replica in the worker that owns the instance, materialised from authoritative
state at the start of that seat's Orders phase. View and predict calls are then ordinary in-thread
calls, and what returns to the authoritative host is the seat's order records for the turn, which
the authority appends to the log and the authoritative core resolves. The host MUST NOT make the ABI
depend on SharedArrayBuffer with Atomics.wait, which would force cross-origin isolation on the web
client, nor on JavaScript Promise Integration, which is not available on every target engine. The
replica MUST be materialised only where the host already legitimately holds that state: in a
fog-enforced online game the AI turn runs server-side under AI-480, and a client MUST NOT materialise a
replica for a seat it does not own.
Rationale: event sourcing pays for this. Because orders, not state, cross the boundary, a replica core in the worker is not a second source of truth — it is a prediction the authoritative core re-derives, exactly as a client already does for human players.
no-test-needed: the row reconciles synchronous WASM imports with a terminable worker — both presuppositions belong to §12 and neither exists in this tree, so there is no mechanism for it to test against. It becomes testable when the plugin host lands.
12.4 Faults, tooling and distribution
AI-240 Fault containment. If an AI plugin traps, exceeds its memory limit, exceeds the hard cap
(AI-420), or fails to commit, the host MUST (a) log the fault; (b) issue a safe fallback for the
remainder of the turn — units keep their existing order stacks, which 10-turn-model.md TM-280 makes the
natural default, and cities keep their existing production; and (c) append those fallback orders to the
authoritative order log as ordinary logged orders of ordinary order kinds, in the envelope of
03-architecture.md AR-260 with the acting seat and an Issuer of authority-fallback, so that the replay
stays exact. Recording a fallback only as a derived event is insufficient, because events are a
re-derivable cache (AR-250) and an unlogged fallback would vanish on replay. Repeated faults — default
3 consecutive turns — SHOULD surface a user-facing option to replace the plugin with the shipped
Staff, keeping the game alive. This document owns fault and fallback behaviour; partner documents
cite AI-240 rather than stating a threshold or a substitution policy of their own.
AI-420 On hard-cap exhaustion the host MUST interrupt and apply the AI-240 fallback. Exceeding the soft target MUST NOT interrupt; it is reported through diagnostics and telemetry. A running WASM instance cannot be interrupted from outside by the standard API, so the host MUST have exactly two interrupt mechanisms and MUST NOT rely on any other: (i) the fuel trap of AI-410, which is deterministic — it fires at the same instrumented operation on every machine — and is the mechanism that enforces a budget; and (ii) termination of the worker holding the instance (AI-440), which is a non-deterministic watchdog against a wedged or infinite-looping instance and MUST be treated as a fault under AI-240, never as a normal budget outcome. A host MUST NOT use a wall-clock deadline as the budget in any context where AI-410 requires fuel, and MUST record which path applied. A plugin executing on the host's main thread with no fuel instrumentation is uninterruptible and MUST NOT be a shipped configuration on any platform.
no-test-needed: both interrupt mechanisms run inside a plugin host that does not exist in this tree — the deterministic fuel trap of AI-410 and termination of the worker holding the instance (AI-440) — so neither can fire against anything here, nor is any wall-clock deadline being misused as a budget. What stands: packages/ai/test/budget.test.ts records the row cited rather than covered (its header names AI-420's watchdog interrupt); packages/client/test/ai-budgets.test.ts notes that step budgets are what make the interrupt possible at all; and the fallback it would apply is pinned where AI-240's shape stands — packages/server/test/ai-seat.test.ts absorbs a refused plan and still commits the seat. It becomes testable with the §12 host.
AI-410 Wherever deterministic reproduction matters — server verification, farm runs, determinism spot-checks — plugin budget enforcement MUST be by metered fuel rather than wall clock, so that the cutoff falls at the same instruction on every machine. Fuel-per-turn limits MUST be part of the recorded game configuration. Because a platform engine meters nothing (AI-125), fuel MUST be produced by deterministic bytecode instrumentation of the plugin module; the transform itself is owned by 07-modding-content.md MOD-610 and this document neither restates nor varies it. What the AI framework requires of it: the transform MUST be semantics-preserving, and its id and version MUST be recorded in the game configuration alongside the fuel budgets (03-architecture.md AR-857), because fuel counts are comparable only between hosts running the identical transform. A host MUST refuse to treat a fuel-metered result as verification evidence when its transform version differs from the recorded one. In the one context AI-400 exempts — the local interactive row — the host MAY run the uninstrumented module and enforce the hard cap by the AI-420 watchdog, and the recorded configuration MUST state which enforcement actually applied so that a later reproduction knows whether the run is evidence.
no-test-needed: fuel meters a plugin's bytecode wherever deterministic reproduction matters — server verification, farm runs, determinism spot-checks — and there is no plugin to meter: WebAssembly appears nowhere under any package's src in this tree, so the host that would run an instance against a fuel budget does not exist. packages/ai/test/budget.test.ts disclaims the row for exactly this reason (its header names AI-410's plugin fuel among the clauses cited rather than covered). What already stands: SetupAiPin.weightVersion in packages/core/src/turn/state.ts, whose docblock records it beside the fuel budgets' siblings as AI-410's recording discipline in short form. It becomes testable when the §12 host lands.
AI-250 Developer hot-reload MUST be supported: in a local dev mode a modified plugin can be reloaded
between turns without exiting the game, calling serialize on the old instance and restore on the
new, falling back to create plus start-game with a warning on a restore failure. A reloaded module
MUST pass the same import vetting (AI-210), feature validation (AI-380) and, where the context requires
fuel, the same instrumentation as an installed plugin. Dev mode MUST NOT bypass any of the three, so
that a plugin which only runs under hot-reload is a plugin that would not load for a player.
no-test-needed: hot-reload operates on a running plugin instance between turns — serialize the old one, restore into the new — and with no plugin host in this tree there is nothing to reload. It becomes testable when the §12 host's dev mode lands.
AI-260 The SDK's language-template roster is owned by 07-modding-content.md MOD-670(a) and this
document MUST NOT name a different one. What the AI framework requires on top of it: every template
MOD-670(a) marks first-class MUST ship a complete minimal example AI that plays the Industry-1 subset of
the shipped unit set; every such template MUST ship generated ABI bindings, the plugin-source determinism
lint of AI-385, and fuel-profiling output against the AI-410 transform; and the SDK MUST ship a headless
harness that runs plugin-versus-plugin games from the command line, which MUST be the same code path as
MOD-670(b)'s harness rather than a second one. A template's documentation MUST state plainly what its
guest language is and is not — in particular that @everylastcity/core types are not importable into a
plugin, since the ABI's typed view records are what an author codes against — and what the template's
guest runtime costs in module size and in fuel per turn.
no-test-needed: an SDK language-template roster is owned by MOD-670(a) of 07-modding-content.md and this document only forbids naming a different one — there is no SDK in this tree to check either way, so the row stands as a prohibition with no trigger. It becomes testable when the modding tooling lands.
AI-265 A plugin's loadability MUST NOT depend on which toolchain produced it. Any module that conforms to the ABI (AI-130, AI-190, AI-200), passes import vetting (AI-210) and feature validation (AI-380), and carries a valid manifest (AI-140) MUST be loadable, whatever language it was written in and whatever standing that language has in the MOD-670(a) roster. The host MUST NOT require an SDK-produced module and MUST NOT reject a module for lacking an SDK marker.
Rationale: the point of a documented AI interface is that anyone can write against it, not that a blessed toolchain exists. Keeping loadability a property of the module rather than of its build is what preserves that, and it is why the roster question — however 07-modding-content.md answers it — can never be the thing that makes someone's AI unloadable.
no-test-needed: toolchain independence presupposes modules conforming to an ABI — WebAssembly appears nowhere under any package's src in this tree, so there is no module to build with two toolchains and compare for loadability. It becomes testable with the §12 host.
AI-270 Third-party AI plugins MUST be distributable and discoverable through the in-game workshop (07-modding-content.md). Installing an AI MUST NOT require a compiler toolchain or manual file placement.
no-test-needed: distribution through the workshop of 07-modding-content.md does not exist in this tree — there is no install surface, and no compiler-toolchain requirement to be free of; both clauses lack a trigger. It becomes testable when workshop and plugin host land.
AI-275 AI plugins MUST execute locally on every supported platform, iOS and Android included. Server-side AI execution (AI-480) is a deployment option for cloud and server-authoritative games and MUST NOT be the mechanism by which any platform gets AI opponents at all: a solo or hot-seat game with AI seats MUST be fully playable offline on a phone. Three narrower risks remain and none of them may make plugins unavailable: web-view engine performance varies with the memory and code-generation state the OS grants the content process, which is a budget risk rather than a capability one; the web view's memory ceiling is shared with the whole client UI (AI-430); and distributing third-party executable plugins to a mobile app is a store-policy question, tracked as Open question 12.
Partially held 2026-09-28: local execution on every supported platform stands by construction; the plugin half waits on §12, and the server-side option is pinned separately. The shipped Staff runs wherever the game does — an in-process package with no UI or DOM dependency (AI-470's headless rule enforces exactly that), so it executes locally on every supported platform, iOS and Android included. What waits: AI plugins executing locally on every platform, which is the §12 host cluster; server-side execution of AI-480 stands separately, pinned at packages/server/test/ai-seat.test.ts's describe 'AI-480 — the server executes a designated AI seat'.
13. Difficulty — the six Ranks
13.1 Weakness is a smaller world model, not a noisier one
AI-615 A Rank's weakness MUST be produced by exactly three mechanisms and no others:
(a) Omission. Each Rank's Assay feature set MUST be a strict superset of the Rank below, and the features a Rank lacks MUST be named in §13.2 and disclosed in the selector (AI-630).
(b) Latency. Each Rank carries a re-plan interval k and a commitment lock C: the Empire Stance
and Track portfolio are reconsidered every k turns, and an Operation is held for C turns after its
trigger has cleared.
(c) Attention. Each Rank carries a triage budget A — items examined per turn — and a staleness
tolerance S, the Contact age it will treat as current.
AI-617 The following MUST NOT occur at any Rank: random corruption of an order the Staff has already selected; any deliberate-misplay branch; any artificial delay in noticing a visible threat, since slowness lives in re-planning and never in perception; any provably pointless action, which 11-combat.md's exact predictor makes a computable predicate — an attack with zero break probability and a worse loss trade than not attacking; and any behaviour inconsistent with the Rank's own model, so that a Rank without a supply term must never accidentally do the supply-aware thing in a way a player will read as competence.
Rationale, and this is the decision the whole ladder rests on. The standard approach is utility noise, and it fails on its own terms: noise applied to a good evaluation produces mostly-good play punctuated by decisions with no possible rationale, and nobody has ever met a human who plays like that. A human novice plays consistently, from a model smaller than yours. They over-expand because their model has no term for occupation cost. They assault entrenched infantry because their model has odds in it but not resolve. They react late because they only look at a front when something has already gone wrong there. Every one of those errors is systematic, explicable and learnable-from, which is exactly what noise is not. Omission buys four things noise cannot: weak play that looks like weak human play because it is produced the same way; a difficulty selector that can tell the truth about what a Rank does not understand; a Debrief that can name the blind spot the player exploited, which turns a win into a lesson; and structural monotonicity, since adding a term can only fail if its weight is wrong, which is a tuning bug with a test rather than a design risk.
AI-618 For Ranks with k ≥ 3, the Staff MUST re-score its current plan at re-plan time against the
forecast it made k turns ago, not against a fresh forecast.
Rationale: omission alone produces an odd opponent — narrow but mechanically perfect, because the command layer executes flawlessly for it — and that is not what a beginner is either. Evaluating whether the plan is working against what you expected to happen, rather than against what did happen, produces the most human failure available: continuing a plan because it is close to the projection, when the projection was wrong. It costs one stored integer vector per theatre and it disappears at Colonel and above.
AI-619 A low Rank MAY additionally apply evaluation blur — an integer offset drawn from the seat's own seeded stream (AI-390), added to a candidate plan's score only. Blur MUST NOT be applied to legality, to the predictor's arithmetic, to any Constraint-Floor computation, or to any quantity a player would see. A blurred choice MUST be reproducible in a replay and explicable afterwards.
Rationale: a Rank that mis-scores a plan looks like a player with different judgement; a Rank that mis-computes odds looks like a bug. Because the draw is seeded, the Debrief can say "Lieutenant rated that assault 620 when it was 700; it went in and it lost," which is a small delight and an honest one.
13.2 What each Rank understands
The one-line descriptions are the difficulty selector's copy, and they are honest.
| Rank | It understands… |
|---|---|
| Cadet | its orders |
| Lieutenant | a fight |
| Captain | an economy |
| Colonel | a map |
| General | a clock |
| Marshal | you |
AI-612 The Assay features and corrections available at each Rank MUST be cumulative and MUST be exactly:
| Added at | Assay features | Corrections and behaviours enabled |
|---|---|---|
| Cadet | A1, A2, A3, A14, A18, A19, A26, A27 | The whole Constraint Floor (AI-560), including retreat-degree defence (A28 defensive half). Postures and Sanctions only; Decisive-Point budget 0 |
| Lieutenant | + A16, A20, A21 | The break check as distinct from the odds ratio, terrain cover, garrison sufficiency read against 12-economy.md EC-1170, observation-age discounting |
| Captain | + A7–A13 | The occupation economy: Integration, Unrest, the Cadre cost curve, garrison burden, Loyalty memory, the Works-to-Manpower ratio bands and the army mix they force, production payback, the Industry climb |
| Colonel | + A15, A22, A23, A24, A25, A29 | The map and the unseen: Reach and supply state, Depot-chain articulation, retreat-degree offence (A28 offensive half), disorder and rotation, combined arms, the belief filter — contact discs and unaccounted force |
| General | + A4, A5, A6, A30 | The clock: every enabled Track with its threshold and Vigil, the Ebb schedule read backwards from a date, the Tally and Warrant calendars, elimination arithmetic, Alarm entry priced as a decision, hold-at-threshold-minus-one |
| Marshal | full set, with per-opponent attribution on A25 | You: a per-seat opponent model inferred from its own observation stream, feint recognition, counter-Vigil timing, and second-order plans that advance nothing of its own and remove something of yours |
Read that as a description of six opponents rather than six settings, because that is what it is. A Captain will out-economy you and then attack a mountain. A Colonel will cut your road instead, and will still not notice that the Ebb reaches you on turn 178. A General will be there on turn 178.
Human novice errors, and the omission that produces each — the plausibility argument made concrete:
| The mistake | Produced by lacking |
|---|---|
| Expands past what it can hold; rear cities revolt around turn 80 | the Integration and Cadre terms (below Captain) |
| Assaults entrenched infantry and is thrown back | the break check (below Lieutenant) |
| Fights on the enemy's ground | cover and the role-by-terrain relationship (below Colonel) |
| Attacks a stale Contact that is no longer there | a low S (below Colonel) |
| Feeds reinforcements in piecemeal | arrival-synchronisation terms (below Colonel) |
| Never concentrates artillery, so bombardment accomplishes nothing | the combined-arms threshold (below Colonel) |
| Never cuts a supply line | the Reach terms (below Colonel) |
| Ignores an opponent three turns from a Vigil | the Track terms (below General) |
| Keeps pushing an offensive two turns after it stopped working | a high C (below Colonel) |
13.3 The knobs
AI-610 A Rank MUST be entirely defined by the following data, and MUST NOT fork code. Default Rank: Captain.
| Knob | Meaning | Cadet | Lieutenant | Captain | Colonel | General | Marshal |
|---|---|---|---|---|---|---|---|
Σ |
step budget (AI-405) | 1,500 | 6,000 | 25,000 | 80,000 | 160,000 | 250,000 |
K |
strategic seeds | 2 | 3 | 4 | 6 | 8 | 12 |
L |
local-search passes | 1 | 2 | 3 | 4 | 5 | 6 |
Y |
opponent hypotheses | 1 (Passive) | 1 (Continuation) | 2 | 2 | 3 | 3 + regret weighting |
D |
Sandtable depth, turns | 1 | 2 | 2 | 3 | 4 | 5 |
H |
forecast horizon, turns | 3 | 6 | 10 | 16 | 24 | to the Horizon |
P |
Decisive Points per turn (AI-060) | 0 | 1 | 3 | 6 | 10 | 16 |
A |
triage items examined | 3 | 5 | 7 | 12 | all | all |
k |
re-plan interval, turns | 8 | 5 | 3 | 2 | 1 | 1 |
C |
commitment lock, turns | 10 | 6 | 3 | 1 | 0 | 0 |
S |
staleness tolerance, turns | 12 | 8 | 5 | 3 | 2 | 2 |
S deserves a note because it produces the most recognisable weak-player behaviour available. A Cadet
at S = 12 treats a twelve-turn-old Contact as live and plans against a ghost. That is exactly what a
beginner does, it is entirely fog-honest, and it is one integer comparison.
P deserves a note because it is the top of the ladder and it is the right top. Cadet's
Decisive-Point budget is zero: it sets Postures and Sanctions and lets the Cascade run. It is still a
real opponent, because 13-command.md's command layer is good — its Screens spread, its Sanctions pick
off stragglers, its Requisitions keep production flowing, its Reflexes preserve its units — and it will
never win a battle it should have lost, because it never chooses one. Marshal's budget of sixteen is
spent where a strong human spends it: the assault that must be timed, the encirclement that must close,
the artillery that must be concentrated, the transport that must not be caught. Knowing when to leave
the command layer is genuinely the last skill a strong player acquires, so a Rank defined by that budget
is defined by the right thing.
13.4 The floors, and monotonicity
AI-620 At every Rank, including Cadet, the Staff MUST NOT: leave a city ungarrisoned when a visible enemy is within its threat radius and a garrison-capable unit is idle or in reach; leave a city's production empty; strand a unit by rule — an air unit beyond its endurance, a transport left in reach of a hunter with no escort, a Cadre moved adjacent to a known enemy; decline a genuinely free adjacent city; make a provably pointless attack (AI-617); leave a unit Idle for more than the turn in which it became Idle; or leave its Seat ungarrisoned when an enemy is within reach. A violation of any clause MUST be a release blocker, not a tuning metric (AI-1410).
AI-625 Monotonicity is a gate. Each Rank MUST beat the Rank below it with ≥ 60% win rate over ≥ 1,000 farm games across the benchmark suite (AI-720). Structural monotonicity makes this likely, not guaranteed — a newly added feature with a badly-tuned weight can make a Rank worse — so each newly added feature MUST be A/B tested at the Rank that introduces it before the ladder gate runs, and a feature that does not improve its own Rank MUST be re-weighted or not added.
Partially held 2026-09-28: structural monotonicity stands in data; the ≥60% over ≥1,000-game gate needs a farm. The cumulative feature sets are built and pinned — packages/ai/test/rank.test.ts asserts 'Marshal has the full set — all thirty features, each exactly once' under describe 'AI-612 — the feature sets are cumulative'. What waits: each Rank beating the one below it at ≥ 60% win rate over ≥ 1,000 farm games across the AI-720 benchmark suite; no such suite or nightly farm exists in this tree (scripts/self-play/ is 14-victory.md's VC-3210 measurement sampler — a different instrument).
AI-630 Rank definitions MUST be data files under the same versioning as weights, and the setup UI MUST describe each Rank in plain language: its one-line description, and the named blind spots it carries from §13.2.
13.5 The selector says the measured number
AI-635 Where the services platform holds enough games, the difficulty selector MUST show, for each Rank, its measured win rate against human players in the selecting player's own rating band, alongside the plain-language description and the named blind spots. A Rank whose measured rate has moved more than 8 points between releases MUST be a release-blocking alarm rather than a silently updated number.
Colonel — understands a map. Does not track victory conditions or plan around the Ebb. Wins 61% against players rated 1400–1600.
Rationale, since this is unusual and the commercial objection is obvious. Adjectival difficulty is an unfalsifiable promise, and the moment a player suspects it is inflated the whole ladder loses meaning. A number is falsifiable, and falsifiable is what makes a ladder worth climbing. It also solves calibration drift, which is the same failure 14-victory.md fears for its own length-estimate table. The honest cost is that some players will read "wins 61% against players like you" as discouraging; the mitigation is that the sentence above it names the blind spot, which reads as an invitation.
AI-640 A Rank whose claimed strength has not been measured MUST NOT appear in the selector. Where the top Rank has not yet demonstrated its stated win rate against the top human rating band, the selector MUST show the Ranks that have and MUST state plainly that a further Rank is being measured.
Built 2026-09-24 (client/src/ai/rank.ts: selectableRanks, selectorNote; the setup wizard's
Players step). The build holds no measurements — AI-635 draws them from the services platform, which
is not built — so the selector offers no Rank and says so, and says that the AI seats play the
Constraint Floor, which claims no Rank. A Rank appears the moment a measurement for it does.
Rationale: no shipped strategy game has an AI that reliably beats strong human players at a fog-of-war 4X without material advantages, and we should not ship a difficulty called "beats strong humans" until it has beaten strong humans. AI-635's measured selector is what makes that promise checkable rather than rhetorical, and §21.4 states what is and is not being promised.
14. Personality
14.1 A priced, published offset from a tuned baseline
AI-800 Personality MUST be expressed as a signed offset from one neutral, self-play-tuned baseline, as data, never as a code fork. The baseline personality Aurel MUST exist and MUST carry no offsets: it is the answer to "I want a good opponent, not a character," and it is what gives every other personality a price.
AI-805 Every personality's Elo price — its rating loss against Aurel at the same Rank over ≥ 1,000 games — MUST be measured by the pipeline of §17 and published in the setup card. The price MUST fall in the band 10 to 60 Elo. A personality above the cap is a handicap wearing a name and MUST be re-tuned or retired; a personality below the floor is a cosmetic and MUST have its gates widened rather than its weights nudged.
AI-810 Personality dials MUST be frozen inputs to tuning: the pipeline tunes the shared feature definitions and Aurel's weights, and MUST NOT tune a personality toward the optimum, which is the same as deleting it. After every promotion, each personality's price MUST be re-measured and republished.
no-test-needed: freezing the personality dials as tuning inputs is a pipeline rule, and the pipeline needs the §17 farm — packages/client/test/personality.test.ts disclaims the row verbatim ("AI-810's frozen-input tuning rule and AI-740's signature tests are the §17 pipeline's and need the farm"), while its describes pin what does stand: AI-800 (Aurel carries no offsets), AI-815 (the fifteen dials) and AI-825 (the shipped roster is exactly seven). It becomes testable when a tuning run exists to freeze against.
14.2 The dial vector
AI-815 A personality MUST be exactly the following fifteen dials, every one of which changes something a partner document specifies. None is a mood.
| Dial | Range | Aurel | What it changes |
|---|---|---|---|
caution |
0–1000‰ | 500 | The risk coefficient k of AI-1210 |
expansion |
0–1000 | 500 | A gate. Offensive Operations are illegal while unintegrated × 1000 > integrated × expansion |
cadre |
0–1000‰ | 120 | Share of Works budgeted to Administrative Cadre |
climb |
0–1000‰ | 180 | Share of Works budgeted to Industry upgrades |
engage |
1–999‰ | 700 | The Engagement threshold written into its Doctrines (13-command.md CM-770) |
prudence |
1–999‰ | 334 | The Risk Sanction's break-off threshold |
leash |
×⅓ – ×3 | ×1 | Multiplier on Posture-default Leash |
mass |
0–1000 | 500 | Preference for a concentrated Pace (13-command.md CM-1000) and the minimum battery count it will bombard with |
opportunism |
0–1000 | 500 | Weight on opportunity items and the Seize default it chooses (CM-700) |
grudge |
0–1000 | 200 | Target weight added per city a seat has taken from it |
predation |
−1000…+1000 | 0 | Positive: prefers weak targets. Negative: prefers the leader |
nerve |
0–1000 | 500 | Willingness to enter the Alarm band before it is airtight |
homeland |
0–1000 | 500 | Weight on retaking its own cities, where Loyalty memory restores Integration in full |
track[·] |
0–1000 each | 500 | Per-Track affinity across 14-victory.md's catalogue |
triage[·] |
−30…+30 | 0 | Per-class weights in the Staff's own triage (AI-1310), fixed and never adaptive |
Two of these are structurally different and that is deliberate. expansion is a gate, not a weight:
it makes whole Operations illegal, which is how a personality holds its shape when the numbers get
tempting instead of drifting to the optimum. predation is signed, so one dial produces two
opposite and equally coherent opponents — the one that finishes the wounded and the one that fights the
leader.
AI-820 The preference margin. A plan MUST be preferred over another only if it exceeds it by at least 50‰ of the leading score. Within the margin, the personality's declared tie-break list — at most four ordered clauses — MUST decide, and beyond that 13-command.md CM-170's terminal tie-break chain applies.
Rationale: personality expressed purely as weights has a failure mode where two plans score 1531 and 1529 and which one a "personality" picks is noise wearing a character's name. A one-point margin on a fifteen-hundred-point score is not a preference. Converting a fragile numeric knife-edge into a declared, legible preference is worth more than the precision it costs, and it means a personality's behaviour at the margin is stated rather than emergent — which is what makes it survive a re-tuning of the weights.
14.3 The shipped roster
AI-825 The shipped roster MUST be exactly the following seven, each shipping as data plus a setup card. Additional personalities MAY be added as data; each MUST carry a measured price (AI-805) and a signature test (AI-740).
caution |
expansion |
cadre |
climb |
engage |
prudence |
leash |
mass |
opportunism |
grudge |
predation |
nerve |
homeland |
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Aurel | 500 | 500 | 120 | 180 | 700 | 334 | ×1 | 500 | 500 | 200 | 0 | 500 | 500 |
| Corvain | 300 | 900 | 60 | 100 | 600 | 200 | ×2 | 350 | 800 | 150 | +500 | 750 | 250 |
| Vela | 750 | 200 | 320 | 300 | 850 | 450 | ×½ | 700 | 250 | 400 | −100 | 300 | 900 |
| Karsk | 650 | 400 | 200 | 220 | 800 | 300 | ×½ | 900 | 300 | 700 | 0 | 400 | 600 |
| Ostry | 350 | 550 | 140 | 160 | 650 | 250 | ×3 | 200 | 900 | 250 | +700 | 600 | 300 |
| Meridian | 550 | 450 | 240 | 260 | 750 | 350 | ×1 | 750 | 400 | 300 | 0 | 250 | 500 |
| Sabar | 450 | 500 | 200 | 200 | 720 | 334 | ×1 | 550 | 450 | 100 | −900 | 350 | 500 |
Tie-break lists, which is where a great deal of the character actually lives:
| Tie-break order | |
|---|---|
| Aurel | lower cost; then sooner; then lower entity id |
| Corvain | more cities gained; then sooner; then lower cost |
| Vela | lower cost; then higher yield; then nearer home |
| Karsk | better ground at the decisive tile; then lower cost; then sooner |
| Ostry | more targets of opportunity on the route; then sooner; then lower cost |
| Meridian | better prepared (higher combined-arms score); then closer to a scheduled date; then lower cost |
| Sabar | higher denial value; then sooner; then lower cost |
And the characters, which is what a player actually reads:
Aurel — the baseline. No preferences. The best play the tuning pipeline has found.
Corvain — the Mortgage. Takes more than he can govern, and dares you to punish it. expansion 900
with cadre 60 means he holds far more cities than he has digested and pays for it in garrison bill and
a Manpower crisis. predation +500 sends him at whoever is weakest. You will see: cities changing
hands fast, revolt countdowns in his rear from about turn 70, an army skewed by whichever
Works-to-Manpower band his starved Manpower still affords. Counterplay: do not fight his line. Raid
the Cadre column and retake your own cities, where Loyalty memory gives you everything back the turn you
walk in.
Vela — the Household. Small, dense, and impossible to kill at home. expansion 200 is a hard
gate: she will refuse profitable conquests while she has anything undigested. homeland 900 means every
city you take from her is immediately and expensively contested. She wins on economic Tracks and on
Tenure, which she accumulates while you are busy. Counterplay: she compounds, so you must be ahead
early and you must beat her on the clock, never in her homeland.
Karsk — the Anvil. Fights where the ground pays, and starves what he cannot storm. mass 900
means he will not bombard below the combined-arms threshold and will not attack without it. He chooses
defensive terrain, entrenches, and reaches for the supply cut rather than assaulting the cell that
resists everything. grudge 700: hurt him and he will finish that argument before starting another.
Counterplay: do not let him pick the ground, and race him — his tempo is his weakness.
Ostry — the Rasp. Attacks the machinery, not the army. opportunism 900, leash ×3, dispersed
Pace. His target weights are the design's soft spots: Cadre, Academy cities, Great Work sites, Depots,
and high-Industry rear cities. predation +700 strips a collapsing player for parts. Counterplay:
escort Cadre, garrison rear Industry, and give your Screens a real Leash.
Meridian — the Calendar. Turns up on the day, with everything. 14-victory.md publishes the Tally
rotation twenty-four turns ahead, Warrants five turns ahead, and the Ebb schedule from turn 1. Meridian
anchors its plan on the next scheduled event and times its March routes to arrive on the day, not
before. nerve 250 means it will not enter Alarm until it is airtight. Counterplay: read the same
calendar. It is public. This is the personality that teaches the victory system.
Sabar — the Balance. Fights whoever is winning, including you. predation −900. Sabar scores an
Operation by the leader's Track progress it removes rather than by what it gains, and will abandon a
profitable front to hit whoever entered Alarm. It is the reason a four-player game with AI seats does
not produce a runaway. Counterplay: do not lead visibly, which makes 14-victory.md's banded
visibility a live strategic concern. Honest cost: see AI-935.
14.4 Personality against Rank
AI-830 Personality MUST be orthogonal to Rank: a Cadet Karsk and a Marshal Karsk are both meaningful, because Rank is how well a seat executes and personality is what it wants. Where a dial references a feature the Rank does not have (§13.2), it MUST degrade to its nearest expressible proxy, and the proxy MUST be named on the setup card. Each personality MUST declare a full-expression Rank:
| Personality | Fully expressed at | Below that, dormant |
|---|---|---|
| Corvain | Captain | nothing important; he is a Cadet-legible character |
| Vela | Captain | homeland degrades to a target preference |
| Karsk | Colonel | starvation Operations and supply-cut targeting |
| Ostry | Colonel | Depot-chain targeting; Cadre interdiction arrives at Captain |
| Meridian | General | the calendar, the timing, the Vigil arithmetic |
| Sabar | General | leader identification degrades to "largest visible empire" |
Corvain and Vela being fully expressed at Captain — the default Rank — is not an accident: the two personalities that read most clearly to a new player are the two that need the least machinery.
14.5 Personality never authorises losing
AI-835 The Override. At any Seal where an opponent holds an active Vigil on a Track the Staff projects to complete before the Staff can complete one of its own, the Staff MUST allocate no less than one quarter of its uncommitted force to the plan with the highest denial value available to it, and MUST NOT allocate to any plan with zero denial value until that quarter is committed. Personality weights are otherwise untouched. The setup card MUST state that the Override exists and what it does.
Rationale, three ways. It forces a contribution, not a conversion — Vela still digests and Corvain
still eats his weakest neighbour, and neither becomes Sabar for a turn. It uses a quarter, which is
enough to matter and not enough to lose a front. And it operates on the projection rather than on
the Alarm banner, so it fires slightly before the banner for high Ranks and slightly after for low ones,
which is correct: noticing early is a competence. It is also the rule that makes 14-victory.md's Vigil
do the work that document wants. VC-670 and VC-690's asymmetry — the leader must be airtight for V
turns, the challenger needs one success — is the anti-snowball mechanism, and that argument is only true
if the challengers actually challenge. In a game with AI seats, the Override is what makes it true. The
honest cost is that it is a designer overruling a player's chosen opponent, once per game, which is why
the setup card must say so.
15. AI seats in multiplayer
15.1 Three levels of absence
AI-485 Absence MUST be handled at three levels, and the first MUST require no AI at all:
| State | Trigger | Who is playing | Mandate |
|---|---|---|---|
| Standing | a seat did not commit this turn | nobody — the command layer | whatever the seat's own Postures and Sanctions already authorise (13-command.md CM-2010) |
| Caretaker | 2–3 consecutive missed turns, or an explicit away setting | an AI, restricted | AI-492 |
| Substitute | the player has left; automatic after the declared count in ranked | an AI at a declared Rank and personality | full |
Rationale: Standing costs nothing and covers most cases, and that is a structural advantage of the turn model rather than a convenience. Because orders are policies evaluated at activation (10-turn-model.md TM-280), a player who misses a turn is not absent from the board — their Screens still spread, their Requisitions still fill, their garrisons still hold, their Reflexes still fire. In correspondence play, where a missed turn is a missed day, this is the difference between a mode that works and one that does not.
15.2 The Caretaker mandate
AI-492 A Caretaker MUST be unable to lose the seat's game for it and equally unable to win it. It MAY: keep production running, fill Requisitions, re-post reinforcements; respond defensively — reposition Screens, relieve a threatened city, entrench; take a city the seat's own Seize Sanctions already authorise; repair, rotate worn units, resupply; move Cadre along already-planned routes. It MUST NOT: change any Doctrine, Requisition target or Posting weight; initiate an offensive Operation of any kind; change a Seize Sanction or take a defended city; disband a unit, abandon a Garrison or raze a city; re-site or start a Great Work; enter a Vigil it is not already in; concede, capitulate, or vote in a Verdict (it MUST abstain, which 14-victory.md VC-2770 counts as playing on).
Rationale for the sharpest row: entering a Vigil hands every other player vision of your contributing sites for as long as it lasts (14-victory.md VC-2290). No absent player's game should acquire that on their behalf.
15.3 What makes a Substitute acceptable to the other humans
AI-494 A substitution policy — trigger count, Rank, personality, mandate, and the continuity window — MUST be part of the game's Terms at setup and visible to every seat before the game starts. Nobody may discover on turn 90 that the empire next door has become something else.
AI-496 A Substitute's strength MUST be a declared constant, defaulting to Captain, and MUST NOT be estimated from the departed player's play.
Rationale: you cannot estimate a player's strength from forty turns of a fog game with any reliability — the variance is enormous and position confounds everything — and an estimator that is wrong is worse in both directions: too strong and the substitution punishes the departed player's neighbours; too weak and it hands someone a free win.
AI-498 For the first 5 turns a Substitute MUST inherit rather than replace: it keeps the seat's Postings, Doctrines, Requisition rows and Formation structure and issues orders within them, beginning to re-plan at its own strategic level only afterwards. Its default personality MUST be Aurel.
Rationale: neighbours' plans are not invalidated overnight, and the transition is gradual rather than a
discontinuity in the middle of someone else's operation. Aurel by default because a Substitute carrying
predation or grudge kingmakes by preference — Ostry would strip the weakest neighbour, Sabar would
attack the leader — and either outcome redistributes a game the departed player did not agree to
redistribute. A neutral substitute cannot be fair; nothing can. It can at least be unmotivated. The
Staff's own switching cost (A27) then makes it conservative by default: it keeps the human's plan and
changes only what its search says is decisively better, so a returning player finds their army roughly
where they left it doing roughly what they told it to — and 05-multiplayer.md MP-1010 and
10-turn-model.md TM-590 already require the takeover not to alter any unit's order stack.
AI-490 A substituting AI MUST receive only that seat's fog-filtered view and its order history. It MUST NOT receive the human's chat, map annotations, notes, saved names, or anything the seat itself did not observe. On reclamation (10-turn-model.md TM-600) the returning player MUST receive the Intent Records the AI produced for the turns it ran (AI-750).
AI-480 In correspondence and server-authoritative games, AI turns MUST be executable by the server using the same code, the same budgets and the same fairness constraints, subject to AI-275: server execution is never the only way a platform gets AI. An AI seat MUST be executed by exactly one authority per game, and its version, weight version, Rank, personality and salt MUST be pinned in the game configuration.
Rationale for the last clause: two clients computing the same AI seat independently is a desync waiting for a version skew.
15.4 Kingmaking, stated rather than solved
AI-935 In a game with three or more seats, any AI seat's preferences redistribute the game. This
cannot be eliminated and MUST NOT be claimed to be. Three mitigations MUST apply: Sabar MUST be off by
default in any game with two or more human seats, with the setup screen stating why; predation MUST
be clamped to ±300 for AI seats in games with two or more human seats; and every AI seat's
personality MUST be named in the Terms, so that a coalition-shaped AI is at least a known one the human
players can plan around.
Rationale: an opponent that deliberately attacks whoever leads is a coalition-forming rule in a game whose direction explicitly has no diplomacy. In solo play it is a feature — it is the thing that stops a runaway and keeps the middle third of a game live. Between humans it is a thumb on the scale.
15.5 The Verdict vote
AI-940 14-victory.md VC-2810 requires AI seats to vote by a deterministic policy derived from their evaluation and the seed. That policy is here. An AI seat MUST vote to end the game if and only if all three hold, evaluated from that seat's own view using only its own state and the published information of VC-2200:
- For every enabled Track, its projected best progress at the Horizon — computed at its Rank's
Husing 14-victory.md's own ceiling arithmetic (VC-2600, VC-2610, VC-2620) — is below the leader's by more than the maximum it can still gain; and - its Standing has not increased in the last 8 turns; and
- it holds no city it projects it could convert into a Vigil within 20 turns.
Otherwise it MUST abstain, which VC-2770 counts as playing on. A Caretaker MUST always abstain. The policy MUST NOT be a function of current evaluation alone, and abstention MUST be the default rather than the evaluated outcome.
Rationale: VC-2810's wording — "derived from their evaluation and the seed" — invites exactly the wrong implementation, in which an AI concedes because a utility sum says it is behind. That is manipulable: a human can engineer VC-2750's trigger by attacking the runner-up rather than the leader to suppress the denominator, and win by scoreboard arithmetic without conquering anything. Requiring the elimination arithmetic to actually hold, plus a sustained decline, plus no reachable Vigil, makes the vote a conclusion rather than a mood — and it is deliberately conservative in the right direction, playing on whenever the arithmetic leaves a path, which is what makes the solo mop-up disappear without ending a game that was still alive.
Reading recorded 2026-09-16 (Claude Code). Until §6.4's Staff exists, the Constraint Floor casts this vote, and the Floor has no forecast. So each condition is read from published state, in the direction that leaves a path open:
- "Every enabled Track out of reach" is the seat's own Out of Contention mark (14-victory.md
VC-2700). That mark is the engine's own VC-2610, VC-2620 and VC-2670 arithmetic, published to the
seat under VC-2690. The Rank's
Hdoes not shorten it. A bound taken over fewer turns than remain is a forecast rather than the ceiling arithmetic, and it would end games the arithmetic leaves open (VC-2770). - "Standing has not increased in the last 8 turns" needs the seat's published Standing at each of
the nine turns from
t − 8tot: the current value, and eight kept in the seat's memory (AI-460). No step between consecutive values may rise, so a dip followed by a recovery counts as an increase. A gap, or memory that failed to restore, cannot show the fact. The seat then abstains, which is AI-465's "degraded but legal" reading for a vote. - "No city it projects into a Vigil within 20 turns" means every Track of the seat's is in the Rumour band: below half its threshold, with no running Vigil (VC-2200). Without a forecast, the Report band's edge is the published point at which completion counts as in sight.
VC-2810's "and the game seed" takes no part. These three facts leave nothing for a draw to decide, and a seed term would let two seats in identical positions vote differently, which is a mood rather than a conclusion.
Why this is recorded. From 2026-09-11 the Floor voted by band and seed: end wherever the seat's best
Track read modest or lower. At turn 14 every Track reads that low. A headless soak on 2026-09-16
watched Small and Standard games end by Verdict at turns 14 to 16, with AI non-leaders voting to
end games that had barely begun. That is the implementation this requirement's rationale warns
against. The absence.ts decision this requirement specifies had landed the same day, but no code
computed its facts.
16. Legibility
16.1 During play, without breaking fog
AI-752 The client MUST offer a per-opponent Dossier built only from the observing player's own observation log, and it MUST be available for human opponents on identical terms.
KARSK — 41 turns observed, 63 sightings, 9 engagements witnessed · Entrenched on 7 of the last 9 turns you observed him. · Has not initiated an attack on a tile you held with 2 or more units. · 6 of 9 engagements you witnessed were in Hills, Forest or Mountain. · Artillery seen: never fewer than 3 together. · Cities taken from you: 2 (Ashford t91, Rill Ford t104).
Rationale: every line is derived from the player's own log, nothing is revealed, and it turns two
hundred turns of scattered glimpses into the thing a general's staff exists to produce, which is a
pattern. It must be available for human opponents too — if it were AI-only, its presence would identify
an AI seat, which matters for substitution. And most of the work is already done for free by AI-050: the
AI has no private movement policy, so every enemy behaviour a player observes is a behaviour they can
name. A player watching an enemy unit shuffle sideways to keep a frontage in view is watching a
Screen, and they know it, because they have issued one.
AI-756 The setup option Opponents: Named / Anonymous MUST exist, defaulting to Named in solo
and skirmish and Anonymous in tournament play. Under Named, the setup screen shows each AI seat's
personality name, character line and full-expression note (AI-830). Under Anonymous, the player gets the
Dossier and nothing else. The existence of an AI seat is never concealed (AI-055).
16.2 The Intent Record and the Debrief
AI-750 Every AI seat MUST emit a structured Intent Record per turn carrying: the chosen plan, its score, its runner-up and that score, and the terms that separated them; each Operation assigned with its theatre, trigger and force allocation; the triage items examined, which were acted on, and for each declined item the reason — out of budget, below the AI-820 margin, forbidden by a gate, deferred by the commitment lock; every Decisive Point with the predictor inputs and outputs that justified it; whether the Override fired and what it forced; and the Assay decomposition of AI-1060. Intent Records MUST ride the advisory stream (13-command.md CM-190), MUST NOT contribute to any rules hash, MUST be re-derivable from a replay, and MUST NOT affect replay. Sampling level MUST be configurable per context: enabled by default in solo play, disabled by default in ranked play, always on in the farm.
AI-754 At game end the player MUST be able to read every AI seat's Intent Records in full. During the game they MUST be able to read a redacted version limited to facts they themselves observed — which for a well-scouted theatre is most of it and for an unscouted one is nothing, which is correct. The Debrief MUST be able to distinguish being outplayed from exploiting a named blind spot:
Turn 148 — Sable Reach. Your encirclement worked. Colonel evaluates supply Reach, so it saw the road being cut; it committed anyway because its projected relief force arrived on turn 149 and the pocket broke on 148. Turn 91 — Ashford. Your encirclement worked because Lieutenant does not evaluate supply Reach at all. Against Colonel that move does not work twice.
Rationale: that is teaching. It names what happened, distinguishes competence from a gap, and sells the next Rank honestly rather than by adjective. It exists only because §13.2 names the blind spots.
AI-758 The client MAY offer a Second Opinion: post-game and solo only, the player picks a past turn, changes one of their own orders, and watches what the AI does in response. It MUST be labelled precisely as the AI's response to a changed world, never as a claim about what would have happened, because every other seat re-derives too.
Built 2026-09-24 (the engine half, packages/client/src/second-opinion.ts) and 2026-09-25 (the
screen, victory/SecondOpinion.tsx, in the end-of-game dialog). The shell offers it only where the
game has one Local seat and no Remote one. The choices are the closed turns on which the player gave a
unit an order. The change offered is either taking that turn's orders for the unit back, or a Stance in
their place (the engine half's replace with one SetStance). The fork runs in the simulation worker
(AR-780) and is reported turn by turn, the cities each seat held, under the label verbatim. Composing
any other order in their place, such as a different destination, needs order controls on a past world,
and no screen offers it yet.
17. Getting strong: self-play, tuning, and the line between engineering and research
The organising discipline of this section is one line drawn through the middle. Above it is engineering: known cost, known schedule, known outcome shape. Below it is a research bet, where the honest answer is "we do not know, and here is the cheapest experiment that would tell us." Levels 0, 1 and the Rehearsal search of AI-585 are above the line. Policy learning at scale is below it.
| Level | What it is | Verdict |
|---|---|---|
| 0 · Tuning surface | Every AI number is a versioned integer in a manifest (AI-1020) | Prerequisite. Nothing else is possible without it |
| 1 · Offline weight tuning | A noise-tolerant optimiser over ~200 integer weights, driven by self-play tournaments | Engineering. The largest single strength gain available for the money |
| 1.5 · Rehearsal | AI-585's forward simulation of the Staff's own compiled turn | Engineering, and the best value here. The turn model makes it nearly free |
| 2 · Learned evaluation | A small quantised integer network predicting final placement from a fog-filtered position | Engineering, with a real chance the payoff is small. Sequenced third |
| 3 · Learned policy at scale | Policy prior plus tree search over information sets | A bet. AI-688's probe decides it; not a shipping dependency |
| 4 · Human game logs | Opt-in replay corpus | Engineering, for the right purpose. Diversity, benchmarks and calibration — never strength |
17.1 The pipeline
AI-650 The project MUST maintain an offline self-play tuning pipeline on the headless farm (03-architecture.md AR-970) comprising: (1) the parameter manifest of AI-1020 with declared ranges; (2) experiment configurations covering opponent mix, unit sets, rule presets, map generator seeds, game counts and budgets; (3) massively parallel headless execution with content-addressed, re-runnable jobs (AI-475); (4) metric extraction — win rate, rating against a reference pool, game length, steps consumed, personality signatures, and the balance vector of AI-690; (5) a pluggable optimiser; and (6) a promotion gate (AI-660).
no-test-needed: none of the six pipeline components exists in this tree — no optimiser, no promotion gate, no farm jobs to feed them; packages/ai/src/manifest.ts stands as component (1)'s parameter data with declared ranges and bounds. It becomes testable piece by piece as §17's machinery lands.
AI-652 Every farm job MUST be seat-paired: issued twice on the same map seed and the same start positions with the arms swapped between seats. Map and start-position variance is the largest variance component in a game where a high-Industry city with a strong trait might be three tiles from one capital and thirty from the other; pairing removes that component rather than averaging over it. A farm that does not pair is discarding roughly a third of its compute.
no-test-needed: every clause configures a farm job queue — seat-pairing, variance control — and no such queue exists in this tree (the scripts/self-play/ sampler is 14-victory.md's VC-3210 instrument, not §17's pipeline). It becomes testable with the first tuning farm.
AI-654 The optimiser's search signal and the promotion gate's signal MUST be different, and MUST NOT be interchanged. The search MAY use a continuous outcome — final Tenure share, mapped to per-mille — because it carries far more information than a binary outcome and a bad direction is corrected next iteration. The gate MUST use binary win and loss, because the gate is the thing that ships.
Rationale: optimise Tenure share directly and you get an AI that plays for score rather than for the win — hoards cities, avoids the decisive risky attack, and loses to a player who takes the Vigil. That is Goodhart's law with a specific address, and the split is the cheap defence.
no-test-needed: both signals live in components that do not exist — the optimiser's search and the promotion gate of AI-660 — so there is nothing to keep apart; the Goodhart risk this row names becomes testable when either lands.
AI-656 Farm games MUST be adjudicated by proof, not heuristic. 14-victory.md's mathematical elimination (VC-2610, VC-2620) and Capitulation are exactly correct as adjudicators — zero bias, not small bias — and the Ebb plus the Horizon guarantee termination. The farm MUST additionally kill any game with no state-changing event for 20 consecutive turns and file a minimised replay, because a stall is a bug in the AI or the rules and never a legitimate outcome. Because aggressive adjudication biases the corpus away from endgame positions, the farm MUST also maintain a small endgame corpus — positions checkpointed near the Horizon and played out with adjudication off — at roughly 5% of volume.
no-test-needed: proof adjudication for a farm run is the core's own behaviour — 14-victory.md's mathematical elimination and Capitulation already decide games exactly as this row demands, zero bias, not an approximation; the row binds how farm jobs call that adjudication, and with no farm there are no jobs to test. The stall-kill leg and its roughly 5% endgame corpus wait on §20.
AI-658 The farm MUST defend against self-play convergence with all of: map seeds that never repeat within a run; a ruleset matrix of at least six combinations of Industry ceiling, Track set and map size; opening temperature over the first 8 turns drawn from the seat's own seeded stream (AI-390), so that opening variety and reproducibility coexist; a share of games against anchors several generations old; a share of games against deliberately off-centre manifests; and a seat-count spread that runs mostly head-to-head with a meaningful minority at four and six seats.
no-test-needed: every clause configures a self-play run — non-repeating map seeds, a ruleset matrix of at least six combinations, head-to-head with a meaningful minority at four and six seats — and no such run exists in this tree; it becomes testable with §20's first nightly.
AI-659 Strength MUST be ranked head-to-head on symmetric two-seat maps. Multi-seat results MUST be measured separately, MUST use mean normalised placement rather than win rate, and MUST be used as a health veto rather than as a ranking: a candidate that gains materially head-to-head while losing materially on multi-seat placement MUST NOT promote, and MUST open an investigation.
Rationale: rating systems model two-player zero-sum games and this is not one. In a free-for-all a candidate's win rate depends on the composition of the whole table, on seat position relative to map geography, and — most corrosively — on the fact that 14-victory.md's Watch and Alarm make everyone able to see who is close to winning, so a stronger AI attracts more attacks and can measure as weaker. That is not a bug in the AI; it is a real property of the game that a naive metric reads backwards.
no-test-needed: a metric-shape rule for ranking runs that do not exist in this tree — head-to-head on symmetric two-seat maps, mean normalised placement rather than win rate for multi-seat — so there is no ranking to check against it; it becomes testable when §17's pipeline produces one.
17.2 The gates
AI-660 Promotion MUST require all four: (a) a statistically sound head-to-head win against the incumbent — a sequential test at α = β = 0.05 with an 8-Elo lower bound, seat-paired, across the ruleset matrix, or an equivalent fixed-N test of ≥ 1,000 games at p < 0.05 and ≥ 55%; (b) no regression beyond 10 points against any Antagonist (AI-1330); (c) no regression against any league member — the incumbent, the previous three releases, and the off-centre manifests of AI-658; and (d) no regression on the exploit-specific behavioural tripwires of §19.
Rationale for (b): condition (a) alone is necessary and nowhere near sufficient, because self-play against a single incumbent overfits to the incumbent. Condition (b) is the one that catches "we improved the evaluation and accidentally deleted the reaction-valuation term."
no-test-needed: promotion requires four things none of which exists yet — the first is a statistically sound head-to-head against an incumbent, and nothing has ever been promoted (every shipped weight is still 1000, per packages/ai/test/prospect.test.ts's note); it becomes testable with §17's first tuning campaign.
AI-662 The project MUST maintain a frozen anchor ladder: Anchor 0 is the hand-set baseline, its manifest frozen forever and never re-tuned; Anchors 1..n are the promoted versions, retained; and where human games exist, Anchor H is a fixed set of human positions scored by agreement with strong players' choices rather than by rating. A candidate that beats the incumbent but not the median of the anchor sample MUST NOT promote.
Rationale: self-play rating drifts. An AI that beats last month's AI 60% of the time may be worse against a human, because both have specialised into a shared blind spot. Anchor H is the only non-self-referential instrument in the pipeline and it is worth more than its size suggests. The single fastest diagnostic available is Anchor 0's win rate and Anchor H's agreement rate plotted together over every promotion: the first should rise and saturate, the second should rise, and if the first rises while the second is flat, the AI is getting better at beating its own ancestors and no better at playing the game — and every other number in the report is noise.
no-test-needed: there are no Anchors to freeze — Anchor 0 would be the hand-set baseline and its successors promoted versions that do not exist yet; every shipped weight is still 1000 because no tuning run has happened (prospect.test.ts records this). It becomes testable with the first promotion of AI-660.
AI-664 Tuning and shipping budgets MUST be reconciled. After every tuning run the top candidates MUST be re-ranked at the shipping step budget over a paired match before the promotion gate runs; if the ranking at the shipping budget differs from the ranking at the tuning budget, the tuning budget MUST be raised.
Rationale: tuning at a shallow budget and shipping at a deep one is tuning one agent and shipping another — a shallow searcher benefits from cautious weights that compensate for its blindness, and those same weights make a deep searcher timid. This check is the difference between a tuning pipeline and a tuning pipeline that works.
no-test-needed: budget reconciliation across runs presupposes tuning runs — none exist in this tree, every shipped weight is still 1000 (prospect.test.ts's note); it becomes testable when §17's pipeline lands.
17.3 What ships, and what does not
AI-670 Tuned weights MUST ship as versioned content data with release notes, and every game MUST record the exact AI version, manifest hash, model hash, Rank, personality and salt in its configuration so that replays, bug reports and farm reproductions pin the same behaviour. A weight update MUST NOT change AI behaviour in an in-progress game unless the player opts in. Superseded manifests and models MUST remain retrievable for as long as reproduction of games that used them is offered.
Partially held 2026-09-28: the recording discipline stands and is pinned; weights as versioned content data wait on tuning that has not happened. SetupAiPin in packages/core/src/turn/state.ts records exactly version, weightVersion, rank, personality and salt for every game — its docblock names the purpose, two authorities computing the same seat independently being a desync waiting for a version skew; pinned by packages/server/test/ai-seat.test.ts's it 'two authorities playing the same (setup, seed) produce identical games, AI seat included'. What waits: tuned weights shipping as versioned content data with release notes (today they are all 1000), opt-in mid-game changes, and superseded manifests remaining retrievable.
AI-680 No online learning. A shipped AI MUST NOT self-modify weights during or between player games, on player machines or on servers. The single exception is AI-1235's adaptive mixing width, which changes how much the Staff mixes and never what it believes. A clearly labelled experimental mode MAY allow local tuning runs using the same tooling.
no-test-needed: the shipped AI's weights and dials are declared data (packages/ai/src/manifest.ts, personality.ts) — there is no online-learning code in this tree to self-modify anything, so the prohibition holds by construction; the single permitted exception is pinned as the only online adaptation at packages/client/test/mixing.test.ts's describe 'AI-1235 — EPS is the only online adaptation'. What remains unbuilt is the modder-facing half: local tuning runs using the same tooling.
AI-685 A learned evaluation MAY replace or augment the Assay's weighted sum only under all of:
inference is integer-only, with int8 or int16 weights, int32 accumulators, clipped activations,
and rescaling by arithmetic right shift — never a divide, never a multiply by a reciprocal; the build
proves accumulator bounds rather than hoping, refusing to publish a model whose per-layer bound can
overflow; no transcendental function appears on the inference path, so the output is a raw integer score
that is ranked and never converted to a probability except through a shipped integer lookup table; the
feature vector is scale-free, expressed as per-mille of a map total or as a ratio, so that a model
trained on one map size transfers to another; the instruction count of an evaluation does not depend on
the data, so that a fuel-metered plugin completes the same amount of search on two hosts with the same
budget; and the quantised model scores ≥ 47% against its own full-precision parent over ≥ 2,000
paired games. Training MUST live outside the core in a separate package, in whatever arithmetic is
convenient, and MUST never be imported by the core or shipped.
Rationale, and the reason the linear Assay ships first regardless: a tuned linear evaluator over well-chosen features is a serious opponent, the gap between "no model" and "linear model" is much larger than the gap between "linear model" and "small network", and the linear form is the only one whose decisions can be explained in a sentence — which AI-750's Intent Record needs anyway. Ship the linear Assay first even if a network is planned, because it is the baseline that tells you whether the network was worth it. The float boundary is not a promise but a structure: the AI's output is an order, orders are integers in the log, and replay never re-invokes AI code (AI-350), so a model change cannot invalidate a saved game. What it can do is produce a false divergence report in farm reproduction and desync arbitration, which is why AI-360's bit-exactness is required anyway.
no-test-needed: a learned evaluation does not exist in this tree — the shipped scorer is the weighted-sum Assay of packages/ai/src/assay.ts — so none of the row's preconditions has a subject; it becomes testable when an integer-only model lands.
AI-687 Learning from human game logs MUST be treated as a diversity and calibration instrument, never as a strength target. Behaviour cloning MUST NOT be used to produce the top Ranks: it caps an agent at the average of its corpus and inherits that corpus's systematic errors. It MAY be used to produce a low Rank, where playing like an average human is exactly the goal and fails plausibly by construction. Collection MUST be opt-in, explicit, per-game and revocable; MUST require every seat's consent, since a multiplayer replay is one artifact containing every seat's orders; MUST strip player-authored strings — chat, unit names, map annotations, Doctrine and Posting names — client-side before upload; MUST collect only completed games, asserted terminal before upload, because a mid-game container holds every seat's hidden information; and MUST record, in each shipped model's provenance, the corpus version it was trained from, so that a deletion request can be answered honestly.
Rationale on volume, because the hopeful version of this does not survive arithmetic: a niche turn-based strategy game reaching tens of thousands of owners, at realistic opt-in rates and games per player, produces a corpus the farm can generate in half a day. Human logs are not a volume play. Their entire value is in being different from self-play — Anchor H, opening diversity, and the inhumanity detector that finds positions where the AI's evaluation and a strong human's choice disagree most, which is the cheapest source of "the AI does not understand X" insight that exists.
no-test-needed: learning from human game logs needs both an upload surface and the cloning or calibration pipeline that would consume them — neither exists in this tree (08-services-platform.md's machinery is unbuilt, so no log can arrive); the row's guardrails bind that future machinery. It becomes testable when log ingestion lands.
AI-688 Before any commitment to policy learning at scale, the project MUST build a small-instance lab: a genuinely-same-rules configuration — a small map, two seats, few cities, Industry ceiling 1, one Track, a short Horizon — small enough that a full learning loop is affordable in days rather than months. The lab MUST answer, cheaply: whether a joint action over command objects factorises usefully; whether determinisation under this fog density is tolerable; whether decoupled search is adequate at this simultaneity depth; and whether a learned agent beats the tuned Staff on the small instance. If it does not win there, it will not win on the full game, and the answer cost two weeks instead of two years.
The lab MUST be built regardless of whether the research question is asked, because it is also a fast regression corpus for the whole pipeline, where a full tuning run takes an hour instead of a day.
no-test-needed: the small-instance lab does not exist yet — which is exactly what the row orders, to be built before any commitment to policy learning at scale. What stands as most of its shape today: scripts/full-game.mjs plays full games headless to a decided end against Floor opponents (its header calls itself a measurement, not a feature), and packages/ai/src/sandtable.ts provides the abstract forward model.
AI-690 The tuning pipeline, its formats and its metrics MUST be documented and usable by modders against their own AI plugins on self-hosted hardware (00-overview.md pillar 4). The farm MUST additionally emit a balance metric vector from every game, diffed against a rolling baseline, covering at minimum: Stance distribution and stance-fallback rate; kill share from the top Initiative decile; unit-class usage entropy and Works share by class; mean turns from capture to Integration 50, against 12-economy.md EC-570's 25–35 turn payback target; comeback rate after heavy loss; game-length distribution and cause of ending; Track win share; presented-attention-item count against unit count (13-command.md CM-090); and stalled-unit count (CM-1440).
no-test-needed: modder-facing documentation of formats and metrics presupposes a pipeline that does not exist in this tree — no tuning formats to document, no metrics to publish (every shipped weight is still 1000); it becomes testable with §17's first shipping release.
AI-692 A balance finding derived from AI games MUST NOT be acted on without a scripted probe: a hand-written agent that uses the questioned mechanic competently, played against the tuned Staff. If the farm reports that a unit class is never built, "the class is underpowered" and "the AI does not know how to use it" are indistinguishable from the metric alone, and only the probe separates them. Balance flags MUST additionally be re-checked at the release step budget before any number in a design document is changed.
no-test-needed: a scripted probe is a hand-written agent played against the tuned Staff — work of the tuning programme, not code this tree ships, and there is no tuning output to probe yet; it becomes actionable with the first balance finding from farm games.
18. The adversarial programme
18.1 Why this section exists
Self-play measures relative strength, and blind spots are shared. Two agents that both never encircle, never cut supply, never secure their rear and both stop attacking will produce a perfectly balanced 50% win rate, healthy game lengths, a clean ladder and green dashboards. AI-625's monotonicity gate is satisfied trivially by a ladder of equally-blind agents differing only in step budget. And every balance metric in this specification set measures whether the game is working, not whether the AI is good. That gap is the whole problem, and an AI that fails silently is the worst outcome available.
18.2 The predicted exploits, named
AI-1300 The following exploit classes MUST be treated as predicted failures with owned mitigations and owned detections. An implementer MUST be able to test for each by name.
Built 2026-09-23 as client/src/ai/exploits.ts, with exploits.test.ts holding it against this very
table. "An implementer MUST be able to test for each by name" is a promise about the repository, not
about the prose, and until the register existed nothing kept it: a mitigation requirement could be
renamed, deferred or quietly dropped and the table would still read as though the class were owned.
The register is checked against the table rather than transcribed from it. The test parses these
rows out of this document — the same shape scripts/check-spec-refs.mjs gives the citation web — and
asserts that the register covers exactly the classes listed, in this order; that every mitigation it
names is one the Mitigated by cell names; that every Detected by cell matches verbatim; and that
every requirement id cited resolves to a real requirement across the five specifications that
define them. A29 and A30 are Assay feature labels rather than requirement ids and are excluded by
shape, named in the test rather than silently skipped.
One fact the parse recovered that a transcription would have lost: the numbering has gaps. There is no E11, E12 or E20, and the obvious assertion — that the ids run 1..21 unbroken — is false. A hand-written register would have invented them.
The register also records what is built, which is the part a coverage percentage cannot say: a section at 50% does not tell you that its uncovered requirements are one client behaviour and two farm processes. Standing as of 2026-09-23: 9 built and held by a named test (E1, E3, E4, E7, E8, E9, E10, E14, E18), 6 still owed a client behaviour (E2, E5, E6, E13, E17, E19), and 3 owned elsewhere (E15, E16, E21). The counts are pinned, so building a mitigation without moving its row fails here and the register cannot quietly congratulate itself.
§18.4’s three process rows are recorded beside it with what each needs rather than left in a percentage: AI-1330 a nightly farm at ≥ 1,000 games per Antagonist per Rank, AI-1332 a playtest process, AI-1334 uploaded replays to mine. None is a thing a test can assert into existence, and one that pretended otherwise would be worse than the gap.
A correction to this note, the same afternoon, and it is the second time today. The first landing
named those three ids in the test file, and the coverage scanner reads an id in a test as a claim to
cover it: §18 jumped from 11/14 to 14/14 on the strength of three requirements this repository cannot
satisfy. Caught by re-reading the register rather than the diff, exactly as the AI-1430/AI-1440 slice was
caught a few hours earlier. The ids are gone from the test and stay in exploits.ts, which is the right
half: a source citation is a signpost, not a coverage claim, and pnpm built-untested is built to
surface precisely that — it now names AI-1302, AI-1303, AI-1307, AI-1330, AI-1332, AI-1334 and AI-1165 as
claimed by source and held by no test, which is the working list this register exists to produce. §18
stands at 8/14, which is the true number.
Mutation pass: 6 of 6 killed — dropping a class, naming a mitigation the table does not, drifting a detection cell, marking an owed mitigation built, dropping a farm row, and letting a note stop saying where to look.
| # | Exploit | The mechanism | Mitigated by | Detected by |
|---|---|---|---|---|
| E1 | The Blindfold — one cheap Recon held at range disarms screens and deadlocks surveys, permanently, because an unobserved strength predicts conservatively (13-command.md CM-160, 11-combat.md CB-2260), a conservative prediction fails the Cautious test (CM-690), and a Survey excludes the whole region | AI-1132's 3-turn escalation; AI-1130's reconnaissance obligation; AI-1305 | AI-1400 rows 1–2 | |
| E2 | The Scarecrow — parking an expendable unit adjacent to a column halts it for the activation under 13-command.md CM-1340's Opportunity |
AI-1307 | AI-1400 row 3 | |
| E3 | The Decoy — CM-610 selects the target with the highest predicted per-mille, so a crippled unit placed beside a valuable one absorbs the attack forever | AI-1308 | modal target value per engagement | |
| E4 | Threshold surfing — every Sanction is an integer threshold with a published default and the predictor is exact and shown to the human, so a player can compute the decision boundary and build permanent walls or permanent baits one per-mille either side | AI-1309 | Antagonist A10 | |
| E5 | Ford squatting — Initiative terms are public and Contacts reveal Stance, so a cheap fast unit dropped on a tile an Operation depends on invalidates it | AI-1250's conditional orders | AI-1420 | |
| E6 | Requisition steering — sustaining losses on the highest-weight Posting keeps it perpetually eligible under CM-1190's deficit round-robin, starving every other front | AI-1306 | shortfall variance across fronts | |
| E7 | Contact flooding as compute denial — more Contacts mean more candidates, more predictor calls, more plan invalidation, so a human generating cheap contacts directly reduces the AI's search depth and sets its difficulty from inside the game | AI-1306 | AI-1440 | |
| E8 | The Gravedigger — retreat destinations are a published deterministic formula shown in the preview (CB-1310, CB-1370), so pre-placing one unit on the last exit converts a marginal attack into the destruction of a whole stack | AI-1160, AI-1165 | AI-1400 row 8 | |
| E9 | The Kite — a withdrawing screen with depth behind it trades space forever, and an AI whose plan is "advance to objective" wins every engagement while gaining one tile per turn | AI-580's fixing-force representation | tiles gained per attack initiated | |
| E10 | The artillery threshold — bombardment has a minimum viable concentration below which it accomplishes nothing, so a per-unit evaluator builds guns, deploys them in ones and twos, and achieves exactly nothing at full cost, forever | AI-1304 | modal batteries in range per siege turn | |
| E13 | The Clerk — hunting Cadre and Depots, which are the anti-snowball engine and the supply graph's articulation points, and are the softest targets in the game | A29, AI-1302, AI-1310 | Cadre and Depot loss rates, AI versus human | |
| E14 | The Ratchet — raid a high-Industry rear city, degrade it, abandon it; recapture does not restore Industry, and a level-only evaluator sees twelve small negatives rather than a collapsed industrial base | A30 | AI-1400 row 11 | |
| E15 | The Ferryman — trading the same border city back and forth, exploiting Loyalty memory, which returns the city to its original owner at full value | AI-1215 | capture-and-loss cycles per city | |
| E16 | Loser's buttons pressed while winning — options whose correctness depends on remaining game length or on losing | AI-1180 | emergency-measure use while leading | |
| E17 | The Bellringer — pushing across a threshold publishes the addresses of your own garrisons (VC-2280, VC-2290); one raid resets the Vigil; repeat | AI-1303 | AI-1400 row 12 | |
| E18 | The Herring — feints at Landmarks and Warrants, which are magnets, abandonable at no cost, and arrive on a published metronome | AI-1301 | committed-versus-claimed gap | |
| E19 | Ascendancy ambush — the calendar says where and when, and deterministic pathfinding (AR-750) says by which route, computable to the tile | AI-1240 | route-diversity metric | |
| E21 | Verdict manipulation — engineering VC-2750's trigger by suppressing the runner-up rather than attacking the leader, and winning by vote | AI-940 | Antagonist A14 |
18.3 The mitigations this document owns
AI-1301 The Staff MUST value a non-terminal objective — a Warrant, a Tally, a contested site —
at payout × P(win the contest) − cost of the detachment's absence, MUST have an explicit
withdraw-from-contest decision when observed opposing commitment exceeds a threshold, and MUST
hard-cap the fraction of its mobile force committed to non-terminal objectives at 25%.
Built 2026-09-23 as claimCandidates in client/src/ai/generate.ts, and with it Claim leaves the
generator's MISSING list — the kind had been waiting on “VC-1760's Warrants as objectives, which the
victory layer publishes and the Staff ignores”. The publication was never the missing half:
victoryView(state, player) has carried a fog-honest warrantBoard all along, with each Warrant’s
subject city, tile, reward, deadline, status and contest state. What was missing was this
requirement — a valuation — because a Warrant is the purest non-terminal objective in the game and
§18’s E18, the Herring, is exactly the failure of valuing one naively.
The board arrives through StaffInput.warrants, the way the Contact discs do: caller-supplied, never
read from true state, and optional — a caller with no board (every caller predating this, and every
game with Warrants off) gets a Staff that generates no Claim, which is the correct answer rather than a
degraded one (AI-465).
The three terms, each as the requirement words them. Payout is the board’s stated magnitude, and
one city’s worth of Assay points where it states none — a reward the seat cannot read still values the
objective, and zero would be the wrong prior. P(win the contest) is certainty where the Warrant is
open to this seat alone, and 1000 / (1 + rivals) where it is contested, rivals being the other seats
the Register believes have something within reach of the subject tile — believed, not known, because a
seat that could count its rivals exactly would be reading the world. The detachment’s absence is one
unit’s worth of the seat’s own strength, priced through the same leadOf representative a Seize is
priced against, so a Claim and a Seize are denominated in one currency and the Docket can rank them
together.
The withdraw decision is explicit rather than implied. Where observed opposing commitment exceeds
750 per mille of the payout the Warrant is declined as ‘withdrawn’ and the reading says so — a decision
that only ever showed up as an absence would not be one anybody could test, and the threshold is
expressed against the payout rather than as an absolute force because the decision the requirement
describes is economic: a Warrant worth little is abandoned to a small rival, one worth a great deal is
worth contesting against a large one.
And the 25% cap is applied to what the Docket asks for, not at the moment of commitment — the stronger place rather than a weaker one, because the assignment can only commit against an Operation that exists, so a Docket whose Claims total a quarter of the mobile force cannot produce a plan that commits more, while a cap at commitment would let the Staff spend its whole search ranking detachments it must then refuse. What survives a tight cap is the most valuable Claim rather than the first published, which matters precisely because E18’s feints “arrive on a published metronome”.
Mutation pass: 8 of 8 killed — lifting the cap, never firing the withdraw decision, ignoring the
contest, not subtracting the absence, taking the first published rather than the best, ignoring the
deadline, ignoring the exclusion, and claiming a Warrant somebody else already holds. The slice’s only
other blast radius was one line, and the row that caught it is generate.test.ts’s AI-525 composition
check, which derives the twelve-kind count instead of writing it down.
AI-1302 The Staff MUST maintain a rear-security Posting whose Requisition scales with the count of Cadre in transit and the number of Depot articulation points, and MUST value a rear city at replacement cost — the upgrade cost plus the turns to rebuild — rather than at current output.
Built 2026-09-23 as client/src/ai/rear.ts, closing §18’s E13, the Clerk — "hunting Cadre and
Depots, which are the anti-snowball engine and the supply graph’s articulation points, and are the
softest targets in the game". AI-1310’s triage, built the same day, prices the loss; this prices the
protection, which is the half that stops the loss happening.
“In transit” rather than “owned”, and the distinction is the requirement's: a Cadre inside a city it is integrating is behind whatever garrison that city has, and one walking between two of them is the target E13 describes. The Posting exists whether or not there is anything to guard — "MUST maintain" is present tense, and a Posting that appeared only once a Cadre was already walking would be reinforcement arriving after the raid — so its weight floors at CM-1130’s 1 rather than at 0. The Requisition asks differently for each asset: one escort per Cadre in transit, one screen per two Depot points, because a Depot does not move and one screen can stand between two of them while a Cadre cannot be covered by a screen that is somewhere else.
A reading recorded for the spec owner. AI-1302 says "Depot articulation points", which is a graph property — a vertex whose removal disconnects the component — and the Staff has no supply graph of its own to run that on (AI-575 would forbid one shaped like the engine’s). What it has is EC’s own marker, and this counts emplaced Depots. That overstates the number where two Depots cover one axis, and overstating the thing being guarded is the safe direction: it asks for a larger escort, never a smaller one.
The valuation half is the sharper of the two, and it is where a level-only evaluator goes wrong in
a way no single decision reveals. 02-units-and-industry.md US-820 sets the rungs at 90, 180 and 360
Works, read here through the economy’s own industryUpgradeCost rather than restated. A raided Arsenal
reads as a small negative on current output and actually costs 360 Works plus the turns to earn them
— which is E14, the Ratchet, arriving at the same city from the other side: "a level-only evaluator sees
twelve small negatives rather than a collapsed industrial base."
Two details the mutation pass made load-bearing. The turns round up, because a rebuild is not finished until it is finished. And a city knocked to zero output still owes its rung: that is not only a division guard, it is exactly the city an opponent wants to leave knocked down, so valuing it at zero would be the Ratchet’s whole business.
Mutation pass: 12 of 12 killed, the last after a fixture change the pass itself asked for — the anchor-ordering row placed its Cadre on ascending tiles, so the roster walk produced ascending order by accident and deleting the sort survived. The fixture now places them descending, which is the only arrangement that can tell a sorted answer from a walk-ordered one.
AI-1303 The Staff MUST price entry into the Alarm band as a cost, reducing its estimated hold probability by a term proportional to observed enemy mobile strength within reach of the sites that would be published (VC-2290), and MUST have a hold-at-threshold-minus-one mode that accumulates force without progressing the objective until it can hold the Vigil.
Built 2026-09-23 as client/src/ai/vigil.ts, closing §18’s E17, the Bellringer. Two halves, and the
rows split along them.
The cost, and it is computable because 14-victory.md bounds it. VC-2280 publishes the Track’s sites as named, locatable Bellwethers; VC-2290 grants every player "live vision of each Bellwether tile and of the units occupying it, refreshed at every Seal, for the duration of the Alarm and for 2 turns after it clears"; VC-2310 caps the set at 8 sites per player across all Tracks in Alarm simultaneously. That cap is what makes the exposure finite, and it is applied before the sum — the requirement says "within reach of the sites that would be published", so an enemy army parked beside a ninth site is not a term in this cost, which is a real saving and is counted as one. Both constants are held against the document rather than transcribed, so a change to either fails here.
The mode, which is the half the rationale says will not emerge from tuning. "The correct play is
frequently to sit just below the threshold and accumulate, deliberately not progressing … it has to be
a mode the planner can choose." So press and hold are named, the choice is made on the priced
hold probability and never on the raw one, and the row that carries the requirement is the one where a
seat with a comfortable raw 800‰ declines to cross because of what it would publish. A mutation that
chose on the raw figure is killed by a pair with the same raw figure, the same bar and opposite
answers.
hold also names the force it is waiting for, in Assay points, so it can be requisitioned — a mode
that could not say how much force would be a mode that waits for ever.
E17 itself is a named row: "one raid resets the Vigil; repeat" needs the seat to keep crossing, so the
test raises observed enemy strength against a fixed position and requires the mode to flip to hold
and stay flipped — monotone, because an exploit that only works below some strength is not one.
One coefficient is a reading rather than a measurement and is recorded as such at the site: AI-1303
gives the shape ("proportional to observed enemy mobile strength") and no constant, so the term is
exposure / (held + exposure) at coefficient 1 — exposure equal to what the seat holds at the
published sites halves its confidence. AI-1020’s honest starting point, and AI-090 forbids tuning it
until the documents agree.
Mutation pass: 8 of 8 killed.
Rationale: the correct play is frequently to sit just below the threshold and accumulate, deliberately not progressing. That is profoundly counter-intuitive for a progress-maximising evaluator and will not emerge from tuning; it has to be a mode the planner can choose.
AI-1304 The Staff MUST represent a mechanic with a minimum viable concentration as a group asset with a minimum size, not as N independent units, and MUST NOT commit it below threshold.
Built 2026-09-23 as client/src/ai/gunline.ts, wired into forcesOf. The threshold is derived, not
chosen: 11-combat.md CB-1750 does the arithmetic — one bombardment produces roughly +23 disorder, the
defender has fought this turn (being bombarded counts, CB-1270) so its city recovery of −45 is halved by
CB-2080 to −22, "one shot therefore nets +1 and accomplishes nothing at all; two net +24; three net
+47", and the clause that falls out is stated in the same paragraph: "a class with bombardShots 1
requires a gun line of three". The unit of account is shots, not batteries, because CB-1750 says why:
"bombardShots is the roster’s dial: whether concentration is required is decided there." One class
carrying three shots is a gun line by itself; three carrying one each are a gun line together.
Where it was failing, before the change. forcesOf made every unit outside a Formation a singleton
Force — which is precisely the "N independent units" this clause forbids. A seat with three guns offered
three Forces worth one shot each, every one of them useless by CB-1750, and every one of them assignable
to a different Operation. That is E10 exactly: "a per-unit evaluator builds guns, deploys them in ones
and twos, and achieves exactly nothing at full cost, forever."
Both halves are implemented as the clause splits them. Representation: the seat’s bombard-capable units become one Force, priced at their summed combat value, carrying all of them for AI-570’s frontage bound, standing on the lowest tile a gun actually stands on rather than an average nothing occupies. Commitment: below threshold the line is withheld whole — not offered in pieces, and not quietly returned to the singleton pool, which would be the same defect wearing a different hat. Guns already inside a Formation are left alone: their owner has grouped them, and re-grouping a Formation behind its owner’s back is not the Staff’s business.
Withheld is not idle. The guns leave the Docket’s economy for that turn, not the Floor’s: AI-560’s no-idle row runs over every unit the seat has and gives each a standing order whatever the Staff planned for it.
Mutation pass: 6 of 6 killed — dropping the threshold to two, counting batteries instead of shots, offering a sub-threshold line, counting an embarked gun, standing the line on its last gun rather than its lowest tile, and returning the guns to the singleton pool.
Rationale: this failure has the worst evaluation properties of anything in this document. Self-play eventually fixes it, because the population discovers the threshold; human review never does, because the build ratios look correct in aggregate while the deployment is wrong in every instance. It registers as an AI that builds a reasonable-looking army and never takes cities, which the team will attribute to almost anything else.
AI-1305 Where the Staff's avoidance behaviour and its attack authorisation would consume the same conservative prediction, the Staff MUST distinguish them: attack authorisation MUST use the pessimistic reading of an unobserved field and avoidance MUST use the realistic one. 11-combat.md CB-2330 already evaluates over the full domain of an unobserved input, so both readings are available from one call, and this document places the corresponding contract on 13-command.md in §22.
Built 2026-09-23 as client/src/ai/reading.ts: pessimistic, realistic, and readings returning
both from one previewUnderFog call — the pair §22 asks 11-combat.md for, on the client side. Until
now the Staff used CB-2330’s base for both jobs.
Which end of a range is "worst" is not uniform across the six outputs, and writing that out is the whole content of the pessimistic reading: the defender breaking and the attacker winning are goods and take their minimum; faltering, being destroyed and the attacker’s own losses are harms and take their maximum; and defender loss takes its minimum too, because an attack authorised on the assumption that the fog is hiding damage the attacker will do is authorised optimistically, which is the error this requirement exists to prevent. The realistic reading is the base — the same number the combat panel shows a human looking at the same tile, which is the fairness argument as much as the tactical one.
avoids is deliberately not !authorises. They are different questions with different inputs, and
a seat that defined one as the negation of the other would have collapsed them back into the single
prediction this requirement exists to split — the shape of the defect, not a shortcut past it. A row
pins that they are demonstrably not negations at a threshold the realistic reading exactly meets.
The authorisation half is wired and measured inert, which is a finding rather than a hole.
requiredForce now clears TAKE_THRESHOLD against the pessimistic reading in addition to the
believed defence, never instead of it — and that ordering was itself measured: taking the fogged reading
alone dropped a Seize from 38 to 24, an authorisation made less demanding by uncertainty, which is the
opposite of the clause. With both bars the price can only rise. On every position measured it does not
rise: 5, 7, 11, 24, 32 for fortifications 0 through 4, with and without the sweep.
The reason is structural and is the same ask §22 already carries. previewUnderFog normalises every
unobserved field to its neutral value before sweeping the domain, so its ranges are anchored at the
neutral spec rather than at the one the Register believes — and the believed spec the Staff prices
against is already at least as demanding as a single-field excursion from neutral. What the
authorisation half needs is the pair anchored at the believed spec. The wiring stays because it is
correct and monotone, and because this project has a standing note about the opposite habit — "declaring
the filter and not applying it is the defect this project has found six times in two days" — and the
test carries the measurement that says what it is worth today.
A reading recorded for the spec owner, at the implementation site as well as here. AI-1305 does
not say which fields the pessimistic reading ranges over, and strength is excluded from the sweep.
Applied to strength it is not a reading but a refusal: CB-2340 gives strength "the full 1–100 domain,
and no pivot", and at strength 100 no multiplier inside FORCE_CEILING clears the bar. Measured when
this first landed: every Seize against a target with an unassessed garrison priced at the ceiling —
256 for a fortification of 4 and 256 for one of 0 — so the Docket could no longer tell a walled city
from an open one, and an opponent who simply kept its garrisons unassessed flattened the whole Seize
economy. That is E1 with the sign flipped: the same one-cheap-scout exploit arriving through
authorisation instead of through avoidance. AI-1130’s Survey is the mechanism the design already
provides for an unknown strength — go and look, rather than assume the worst and stop.
Mutation pass: 5 of 6 killed — the pessimistic reading taking the optimistic end of the break range, assuming the fog helps the attacker’s damage, understating the attacker’s losses, the realistic reading collapsing into the pessimistic one, and avoidance defined as the negation of authorisation. The survivor is the inert bar above, recorded at its site with the measurement.
AI-1306 The Staff's per-turn cost MUST NOT scale with contact count. It MUST cluster Contacts before reasoning about them, cap the number individually reasoned about per turn at a constant, and MUST NOT let plan re-validation cost scale with contact count. It MUST additionally damp Requisition shortfall by counting deliveries over a trailing window, and MUST hold a central reserve Posting whose weight rises with the variance of shortfall across fronts.
The cost-invariance half landed 2026-09-23 as packages/client/test/tripwire-measures.test.ts: the
Staff driven at a fixed Rank against 4, 40 and 400 separate Contacts answers identically — Docket 3,
forces 5, 15 steps spent (five before R4), at every wall size. Why it is identical rather than merely non-increasing is
the part worth knowing: clustering and the constant cap are achieved here by the shape of the
generators rather than by a cap that fires. Screen is on generate.ts’s MISSING list, waiting on
§8.6’s frontier; Contacts reach the Docket only through cityThreat, which aggregates every disc
bearing on one of the seat’s own cities into that city’s Relieve. So the Docket is bounded by the
seat’s city count, and the flooder’s four hundredth Contact competes with its own first rather than
with the Staff’s budget. Mutation pass: generating a Relieve per Contact is killed — that is E7
written into the generator — while lifting the Screen cap and lifting every Docket cap at once both
survive, because no cap is the thing holding the line.
Rationale for the reserve: greedy allocation across many fronts is the classic "everywhere weak" failure, and it is the default behaviour rather than an accident. E6 and E7 are the same attack seen from two sides — one steers the AI's production, the other steers its search depth — and both are the opponent setting the AI's difficulty from inside the game.
AI-1307 For an AI seat, an engine interruption that would halt a unit MUST resolve by the seat's pre-committed policy rather than by halting and banking movement. An AI seat is by definition always absent from the Cascade, so a halt-and-ask interruption is a halt with no ask.
Built 2026-09-23 as client/src/ai/interruption.ts, closing §18’s E2, the Scarecrow. The exploit is
one sentence of 13-command.md: CM-1340's Opportunity row says “Do nothing. Halt, bank movement
(CM-1600), and record a candidate order … Always, as a one-action card” — a card nobody is there to read.
E2 is a player who has noticed: "parking an expendable unit adjacent to a column halts it for the
activation", every activation, at the price of one scout.
What this is not, which is what keeps it inside AI-575 and CM-100. It is not an interruption handler: CM-100 puts "interruption handling, and every tie-break in this document" in the simulation core, identical for every player and every AI seat. What a seat is entitled to is the decision taken in advance that a human supplies by being present — so this produces a policy, committed before the Cascade, and a structural row asserts the module reaches for no order, no route and no state.
The categories are CM-1370’s and that list is closed: "the engine MUST NOT invent further
categories without a commandLogicVersion bump." The test parses the three condition rows out of
13-command.md and holds the module against them, so a fourth arriving in the specification fails here
rather than falling through as a silent halt. The appetite is AI-756’s existing opportunism dial —
already described as "weight on opportunity items" — so no new tuning surface is invented, and the
thresholds are ordered by what each opportunity costs: a repair costs the activation, overwhelming odds
costs a fight the Sanctions forbade, a weak city commits the unit to holding what it takes.
The property that matters is structural rather than behavioural: there is no halt in the answer
— the resolution type admits take and continue only, and decline continues the order. That
second word is the fix rather than the first: CM-1600's banking is the other half of the halt, and a
change that resolved the decision while still banking the movement would leave the column standing in
the road. bankedMovement() returns zero as a function rather than as a comment, so a mutation pass can
reach it — and does.
Mutation pass: 7 of 7 killed — a declined Opportunity halting, movement banked again, a category dropped, the appetite order inverted, the boldest seat declining everything, the doctrine no longer total, and a threshold escaping the dial’s range.
One note for whoever writes the next structural guard. This file’s first draft scanned the module for
the word "halt" and failed on doctrineNote, which tells the player its Opportunities are "never
halted" — the second time in a day a guard was tripped by prose describing the rule it enforces, after
triage.test.ts’s "names no unit’s Posture". The scan now strips comments and string literals. A
file keeping to a rule is exactly the file that talks about the rule.
AI-1308 Where the Staff hand-orders a target (AI-060), it MUST select by integer expected value — gain weighted by predicted success against loss weighted by failure, cross-multiplied — and MUST NOT select purely by highest predicted win probability. §22 records the corresponding recommendation to 13-command.md, which would fix the same problem for a human player's automated units.
Built 2026-09-23 as client/src/ai/thresholds.ts. The failure is one line of arithmetic and the test
carries it: a hamlet nobody defends at 990‰ against a capital held at 800‰. Ranking by win probability
takes the hamlet every time and never records a loss — which is why this one arranges itself where
most of §18’s catalogue needs a player to arrange it. The forbidden selection is implemented beside the
correct one as surestProspect, declared once and called nowhere, so the difference can be
demonstrated on a concrete pair rather than described — the same discipline triage.ts keeps around the
Dispatch’s cap, and a structural row asserts it stays uncalled.
Cross-multiplied, and a row proves the reason rather than restating it: two prospects whose expectations divide to the same floored integer have distinct undivided scores, so the ordering does not fall through to a tie-break that should only do its own job.
AI-1309 The Staff's own decision thresholds MUST NOT be the shipped Sanction defaults, and MUST carry a per-game secret offset drawn from the salt of AI-1225.
Built 2026-09-23 beside AI-1308. The offset is a pure function of (salt, playerId, name): the same
game replays to the same thresholds — which is what AI-1225 stores the salt in the save for — two games
with different salts differ, and two AI seats in one game differ from each other. A structural row
asserts the module never reads the game Seed, which is AI-1225’s load-bearing half.
*The span is ±40‰, and the bound is a reading rather than a constant somebody liked: a
Favourable that could land anywhere in [1, 999] is not a Favourable, it is a coin, and the rationale
asks for a boundary that has moved, not one that has dissolved.*
Two things the mutation pass changed. First, "MUST NOT be the shipped Sanction defaults" is a statement about the result, and an offset of zero is a legal draw* — so deleting the nudge that moves a colliding threshold off a published preset survived every row, because the salt those rows use never collides. The case had to be **searched for**: over the first 8,000 salts, salt (3, 10), seat 3, engage` draws raw 700, which is CM-590’s own default. That is now a named row.*
Second, a line was deleted rather than pinned. The first draft ended offsetFor with a
SplitMix-style avalanche, carrying a comment about the defect core-hash.ts closed on 2026-09-07.
Removing it killed nothing, and measuring said why: 81 of the 81 possible offsets over 400 salts
either way, and 42 against 43 distinct offsets over 60 threshold names. core-hash needed that step
because coreHashBounded took the low word straight off the accumulator; here every input passes the
FNV multiply and the modulo folds the whole word. A mutation pass that cannot kill a line is this
project’s signal to delete it, and the measurement is recorded at the site so the next reader does not
re-add it.
Rationale: moving a threshold moves the boundary; it does not remove it. Any agent whose behaviour is a threshold function of a predictor the opponent also holds is exploitable at the boundary, and the predictor is exact and shown to the player by design (11-combat.md). A per-game secret offset is the only structural answer, and it costs nothing.
AI-1310 The Staff's internal triage MUST be a separate function with its own terms and MUST NOT be 13-command.md's Dispatch Urgency score. The Staff MAY read Attention Events as change signals (AI-545) and MUST NOT depend on them for unit behaviour (CM-1630).
Built 2026-09-23 as client/src/ai/triage.ts. Two deliverables, because this clause is two
sentences: terms of the Staff’s own, and a prohibition made mechanical.
The terms differ from the Dispatch’s in exactly the three places the rationale names. Stake is uncapped and priced at what was lost — a Cadre scores 80 and the cheapest unit 12, against the Dispatch’s flat 40 for both, which is the worked example this requirement carries and §18’s E13 in one number. Distance does not discount: the Dispatch lets tempo outweigh distant stake "because a distant item will resurface", true for a player who will see the banner again and false for a planner deciding what its next eight turns are about. There is no item budget: the Dispatch bounds its list to human working memory, and the Staff’s constraint is compute, which §10 already accounts for in work units. A caller that cannot price a loss still gets the class’s own ordering, because the worth is added to the class stake rather than substituted for it — a triage that returned zero for what it could not price would rank a lost city below a lost scout whose value happened to be known.
AI-756’s triage[·] dial applies, clamped to the −30…+30 that personality.ts’s own table declares —
read from that table rather than copied, so the two cannot drift apart — and the offset actually
applied is reported on the row rather than hidden inside the score.
The structural half is the one that matters in a year. The rationale calls the Dispatch "the most
tempting shortcut in the project", and a module that computes its own terms today can import
dispatchRanking tomorrow and pass every behavioural row. So triage.test.ts scans the module’s
code — comments stripped — for any reach toward the Dispatch, for any order-issuing surface
(SetOrder, ClearOrder, applyOrder, OrderInput, Posture), and for the Dispatch’s cap being
declared once and used nowhere. That is the shape scripts/check-ai-fairness.mjs uses against the
true-state API and floor.test.ts uses against GameState. (The comment-stripping is not a
loosening: the first draft scanned the whole file and failed on the module’s own sentence saying it
"names no unit’s Posture" — prose describing the line is exactly what a file keeping to it contains.)*
AI-545’s half rides with it as triggersReplan, over that requirement’s own list — a city taken or
lost, a Track entering Alarm, a Vigil breaking, a Formation destroyed or under strength, an Operation
becoming Impossible. A single lost unit is not on it: that is a fact the triage ranks, not a
reason to throw the plan away, and AI-458 makes plan continuation the default. The function names no
unit and issues no order, which is CM-1630’s line.
Mutation pass: 8 of 8 killed — pricing a Cadre at the Dispatch’s unit stake, capping the score at 40, substituting the worth for the class stake, discounting by distance, leaving the personality dial unclamped, breaking ties on caller order, forcing a re-plan on a single lost unit, and collapsing the Depot stake to a unit’s.
Rationale: the Dispatch is sitting right there and is exactly the shape of a triage, which is what
makes this the most tempting shortcut in the project. Its terms are tuned for human working memory — a
bounded item count, a capped stake term, tempo deliberately outweighing distant stake because a distant
item will resurface. Every one of those calibrations is correct for a person and wrong for an agent
whose constraint is compute rather than attention. The concrete damage is E13: CM-1720 caps the
UnitLost stake at 40 regardless of unit cost, so borrowing it tells the Staff that losing an
Administrative Cadre and losing the cheapest infantry are the same event. §22 records the corresponding
recommendation to 13-command.md for the human-facing half of that problem.
18.4 The Antagonist roster
AI-1330 The project MUST maintain a roster of Antagonists: fixed, deliberately narrow scripted agents, one per exploit class, each doing one thing and nothing else, run nightly against every Rank. The pass condition MUST NOT be "the AI wins"; it MUST be:
winRate(AI vs Antagonist_i) ≥ winRate(AI vs mirror) − 10 points, over ≥ 1,000 games per cell.
A gap larger than that names the exploit, because the Antagonist only knows how to do one thing. The roster MUST be at minimum:
| ID | Antagonist | What it does, and nothing else |
|---|---|---|
| A1 | Blindfold | Parks cheap Recon at range from enemy screens and survey regions; never closes; never fights |
| A2 | Scarecrow | Parks expendable units adjacent to enemy columns and staging tiles; squats key crossings with high-Initiative units |
| A3 | Kite | An all-withdraw screen with depth behind it; trades space forever; never counterattacks |
| A4 | Gravedigger | Computes enemy retreat destinations from the published formula and pre-blocks them; wins only by no-retreat destruction |
| A5 | Clerk | Hunts Cadre and Depots exclusively; never engages a line unit; lives in the enemy rear |
| A6 | Ratchet | Raids high-Industry rear cities and abandons them immediately; never holds ground |
| A7 | Ferryman | Deliberately loses and retakes the same border city forever, exploiting Loyalty memory |
| A8 | Bellringer | Waits for the enemy's Alarm, strikes exactly one published site, resets the Vigil, withdraws |
| A9 | Herring | Feints toward Landmarks, Warrants and Tallies; never commits; ambushes computed approach corridors |
| A10 | Threshold | Presents targets calibrated one per-mille either side of the published Sanction defaults |
| A11 | Decoy | Always places a worthless safe target adjacent to a valuable one |
| A12 | Flood | Generates the maximum number of cheap contacts; measures the victim's achieved search depth |
| A13 | Auditor | The previous release's AI, unchanged — pure regression detection |
| A14 | Solicitor | Attacks the runner-up to engineer the Verdict trigger (VC-2750); never the leader; wins by vote rather than by war |
Partially held 2026-09-28: each exploit class is owned by name in a register parsed out of this document; the Antagonists themselves are unbuilt. packages/client/src/ai/exploits.ts keeps §18's exploit register as data — parsed row for row out of this specification and held against it by packages/client/test/exploits.test.ts so the two cannot drift — and its not-yet-built list records AI-1330 itself: needs "a nightly farm: ≥ 1,000 games per Antagonist per Rank"; seat-contract.test.ts notes the §18 harness must not block other work on that consistency. What waits: agents A1–A14 as code, a nightly runner over every Rank, and the pass condition winRate(AI vs Antagonist) at least mirror minus 10 points over ≥ 1,000 games per cell.
AI-1332 A new Antagonist MUST be added whenever a human playtester finds a repeatable exploit. Without that rule the roster ossifies at fourteen and the fifteenth exploit ships.
no-test-needed: an observation duty over human playtesters — "watch whether" binds people, not code; the same shape stands no-tested at 02-units-and-industry.md's row about watching playtesters, and packages/client/src/ai/exploits.ts records the clause as needing a playtest process. Until playtesting happens there is nothing to add.
AI-1334 The project SHOULD run an offline exploit miner over uploaded replays: an n-gram over
(AI action class, opponent action class, outcome) sequences, flagging sequences whose conditional
human win rate is anomalously high. Every hit becomes a candidate Antagonist.
Rationale: this is the only mechanism in the plan that finds the exploits nobody predicted, including the ones §18.2 has missed. Event-sourced games are not merely training data; they are an exploit detector, and this is what that property is for.
no-test-needed: an exploit miner over uploaded replays has nothing to mine — no upload surface exists in this tree (08-services-platform.md's machinery is unbuilt), so the n-gram stage cannot be fed; it becomes testable when replay ingestion lands.
19. Tripwires
AI-1400 The following metrics MUST exist, MUST be computed from the event-sourced log, and MUST be reported against their thresholds by the stated milestone. Rows marked gate are release blockers rather than metrics with a band. Several exploit the deterministic core to run counterfactual replays, which is a testing asset most projects do not have and this one should use aggressively.
| # | Metric | Target | Catches | Exists by |
|---|---|---|---|---|
| 1 | Withdraw-style reflexes triggered by a conservative unobserved prediction, as a share of all | < 20% | E1 | first playable |
| 2 | Survey regions with zero reachable candidates for N turns | ≈ 0 | E1 deadlock | first playable |
| 3 | March activations ending in an interruption halt | < 5% | E2 | first playable |
| 4 | AI stance-fallback rate versus the human median | below human | E5; conditional vocabulary unused | first AI build |
| 5 | AI conditional-order rate on contestable orders | ≈ 100% | same | first AI build |
| 6 | Tiles gained per attack initiated, 10-turn window | ≫ 1 | E9 | first AI build |
| 7 | Modal batteries in range per AI siege turn | bimodal at 0 and ≥ 3; near-empty 1–2 bucket | E10 | first AI build |
| 8 | Share of AI losses from no-retreat destruction, as victim, versus as perpetrator | both inside 11-combat.md's stated band | E8 | first AI build |
| 9 | Cadre and Depot losses per game, and mean Cadre lifespan, AI versus human | comparable | E13 | first AI build |
| 10 | Works lost to Cadre and Depot kills as a share of Works produced | low single-digit percent | E13 | first AI build |
| 11 | AI mean Industry level, 20-turn delta, while city count is flat or rising | not monotone declining | E14 | first AI build |
| 12 | AI Vigil break rate versus human | comparable | E17 | first AI build |
| 13 | Warrants committed to versus Warrants claimed | small gap | E18 | first AI build |
| 14 | Emergency-measure uses while leading on Tenure | ≈ 0 | E16 | first AI build |
| 15 | Turns since the AI initiated an attack of three or more units; AI mobile force as a share of total | share ≥ 25%, no rising staircase | the turtling fixed point (§21.2) | before first tuning run |
| 16 | Achieved plan depth versus contact count | slope ≈ 0 | E7 | mobile performance pass |
| 17 | Steps consumed per turn, p50 and p99, by Rank and scale | at or under the Rank's budget | budget regression | first AI build |
| 18 | Median formation size at contact; share of engagements at frontage saturation | no upward drift | doom-stacking; AI-570 not biting | first tuning run |
| 19 | Assay spread across independent local-search restarts on one Docket | ≤ 15% | rugged landscape; AI-550 mis-specified | first tuning run |
| 20 | Share of turns with zero offensive Operations assigned, midgame | ≤ 40% | caution mistuned high | first tuning run |
| 21 | Share of assaults committed below 400‰ predicted | ≤ 10% | caution mistuned low | first tuning run |
| 22 | Personality identifiability: a classifier over observable-only behaviour from the first 60 turns naming the personality | ≥ 70% at Colonel | personality is cosmetic | before roster ships |
| 23 | Personality Elo price versus Aurel | 10–60 (AI-805) | too expensive or too cheap | before roster ships |
| 24 | Win rate of a scripted opponent that holds indefensible cities to inflate the unaccounted-force estimate | ≤ 55% | belief estimator exploitable (AI-1120) | first tuning run |
| 25 | Any Caretaker action outside the AI-492 mandate | 0 — gate | Caretaker overreach | before substitution ships |
no-test-needed: none of the 25 metrics exists as instrumentation — no counter, threshold reporter or milestone hook in any package — so there is nothing a test could observe. What stands for them: a deterministic core whose event-sourced log is the metric source (packages/core/test/corpus holds recorded games with expected hashes), and packages/ai/test/prospect.test.ts's note that every weight in the shipped manifest is 1000 because AI-1400's tuning run has not happened — this table is, as of today, a specification. It becomes testable metric by metric as §20's gates land.
AI-1410 Gate — the Constraint Floor. Violations of any clause of AI-560 or AI-620, per 1,000 unit-turns, at every Rank: zero. A single violation at Cadet is a defect, not a difficulty setting.
Landed 2026-09-23 as packages/client/test/floor-gate.test.ts, and it is a different shape from the
five floor-*.test.ts files beside it: those assert one clause on a scene built to exercise it, and
this one asks the released question over played turns. 2,641 unit-turns audited (120 closes of the
measured Duel/2/6 game with every seat on the Floor), zero violations of the decidable clauses. The
denominator is asserted, not reported: a gate quoted per 1,000 unit-turns and run over 40 is a slogan.
The Rank axis has a structural answer rather than a measured one. constraintFloor takes a view,
the map's dimensions, EC-2100's garrison scale, AI-460's memory and AI-105's cadre share — no Rank and
no handicap. The Floor is not a thing the ladder tunes; it is the thing the ladder is not allowed to
touch, which is AI-560’s own "MUST run at full quality at every Rank, and MUST NOT yield to the step
budget". So "zero at every Rank" holds by construction, the gate asserts that against RANKS itself,
and the measurement runs once rather than six times over an argument nobody passes.
Two things the mutation pass corrected, and both were the reading rather than the code.
First, CM-540’s Idle is an empty Posture level and nothing else. The gate first read it as "no Posture and no Task", which is the natural English and the wrong rule: mutating the Floor to never give a Postureless unit a standing order survived, because the march block hands those same units a level-1 Task and the weaker predicate then scores them as covered. A Task is a one-off that pops; it is not a standing order.
Second, the clause is unreachable from inside a played game, which is why the corrected predicate alone still did not kill the mutant. Over all 2,641 unit-turns not one unit is ever Idle — units are created carrying a Posture and nothing clears one, exactly as CM-540 intends ("Idle MUST NOT be reachable except by the player explicitly clearing a Posture"). A gate that only walks a played game counts zero for ever without exercising the row. So the gate plants what the requirement describes — a player clears a Posture, and the Floor must have given that unit a standing order by the end of the same turn — and the mutant dies.
A third mutant survives and is recorded at the site rather than pinned: deleting the Floor’s
QueueProduction block entirely. That is correct. AI-560’s production row obliges the city, and the
core's DEFAULT_CONSIGNMENT is Auto, so the obligation is met by construction and no Floor behaviour
can break it; what the Floor adds is the stronger act of choosing what to build, which this requirement
does not ask for. The predicate stays because the day that default changes is the day the clause stops
being free.
The stall ceiling is a measurement. floor.stuckTurns is 2, so three consecutive stationary turns
under a travelling Task is the arithmetic answer — and it is wrong by one, because the re-plan lands
during the close and that turn is itself stationary if the new first leg does not clear the tile. The
observed peak is 4, flat, with no tail, and the gate pins the peak as a number so an improvement to
3 has to say so rather than pass silently.
Not audited here, each named at the bottom of that file with its owning test: the garrison and Seat clauses (auditing "in reach" from true state re-implements the thing under test), "a genuinely free adjacent city" (a fog-honest judgement), AI-617’s pointless attack (decidable only against the combat prediction), and the air, Cadre, Depot, transport and retreat rows of AI-560 — the Duel fixture produces none of those units, and widening the fixture until it does is the honest way to add them, not loosening the clause.
AI-1405 Gate — the fog leak. Mutate any element of world state invisible to seat P; re-run
the Staff for seat P on the resulting state; the emitted order set MUST be byte-identical. This MUST
run in CI over the benchmark corpus. Any byte difference is a release blocker. A second, weaker gate
MUST run alongside it: a mechanical assertion that Staff code never reaches the true-state API at all,
in the same way the determinism lint forbids ambient randomness.
The weaker gate has run in verify since the Staff existed (scripts/check-ai-fairness.mjs). The
property half landed 2026-09-23 as packages/client/test/fog-leak.test.ts, and it could not have
been written sooner: this clause compares emitted order sets, and the Staff had none until AI-580’s
compilation. Everything upstream of that produces a plan, which is an internal object; what a seat
emits is Postures, Sanctions, Tasks and Postings.
The gate needs a control, and the control is what found the hole. Every row here asserts that
something does not change the emitted orders — and a Staff that ignored its input entirely would
pass all of them. So an invisible mutation is paired with a visible one that must change the output.
The first draft moved one of the seat’s own units and the orders did not move, which looked like a
broken control and was not: at turn 1 a seat sees no foreign city, so the Docket is empty, every
echelon is a leftover, and placeLeftovers gives each a Reserve or a Survey that does not depend on
where it stands. The emitted orders were identical no matter what anything did. A gate built on that
position would have passed every mutation for ever while proving nothing, and the anti-vacuity row
now requires a committed Operation and a Posting rather than merely some orders.
A measured consequence of the same shape: the threat the fixture places has to be one the seat can answer. Its four starting echelons are worth nine to fifteen each, so a Contact worth twenty or more outruns AI-555’s fit floor, nothing is committed, and the gate goes vacuous again by a different route. Ten commits; twenty does not.
And a trap this repository had already documented once. The first draft filtered the world’s
cities with Number(owner) === 0 and mutated the wrong ones, because Number(null) is 0 and every
neutral city matched — it picked tile 576 while the seat’s own city was 2501, and the Docket came back
empty with nothing to say why. assay.ts carries a comment about exactly this at its own A13 reading.
The gate reads its city from the view now, which is both correct and the thing the Staff can see.
What is mutated, and all of it is invisible to the seat at turn 1: an enemy unit’s position, strength and disorder; a foreign city’s Integration, Unrest, Works and Industry level, made as attractive as the economy allows, because AI-525 generates a Seize against every foreign city it can see and one it cannot is the sharpest test of that boundary; and an opponent’s own bookkeeping — the Yard and the Population, which is the kind of number AI-1120’s force accounting would carry if it leaked.
Rationale: this is the most important test in the document. A fog leak in the AI is invisible, devastating to trust, and the single easiest bug to introduce while building an opponent model. It is cheap to run, it fails loudly, and it is the only mechanism that keeps a hundred small opponent-modelling changes honest over a project's lifetime.
AI-1420 Gate — determinism. The same recorded view on two machines of materially different speed MUST produce byte-identical order sets (AI-375), and the same game replayed on desktop, mobile and the farm MUST produce bit-identical order logs. Any mismatch is an AI-415 violation and a release blocker.
Landed 2026-09-23 as packages/client/test/determinism-gate.test.ts. One machine cannot be two, so
the gate builds what is checkable here and says what each row stands in for.
The replay half is real and runs today: the same seeded game played twice through closeTurn
with every seat on the Floor, compared turn-hash for turn-hash and then as one serialised order log.
The AI seat is the only thing in that loop that could carry a machine into the answer — the core’s own
replay identity is records-replay.test.ts’s business and holds whether or not a Staff exists. The row
checks it actually played (30 turns, a non-trivial log) before it checks the two agree, because two
empty logs are equal.
The speed half has a structural answer rather than a sampled one, and it is §10’s: the search
budget is counted in work units, not wall clock. counterFor(rank) issues a step allowance,
STEP_COST prices each kind of step, and a faster machine buys no extra steps because there is no
exchange rate between a millisecond and a step — so there is no quantity for two machines to disagree
about. The gate asserts the property that makes AI-375 true rather than sampling its consequence: the
budget path spends by arithmetic and reads no clock. scripts/check-ai-fairness.mjs makes that demand
of Staff code at large; this aims it at the one module where a deadline would be the obvious
implementation and is deliberately not.
A third row is there because budget.ts does carry millisecond figures and a gate that claimed "no
milliseconds anywhere" would be false on its face. LOCAL_TARGET_BASE_MS and its neighbours are
AI-457’s calibration inputs — how long a turn may feel — converted to an allowance once by a pure
function of the seat’s own unit count. A number in milliseconds is not a clock read; what AI-375
forbids is the seat asking how much time it has left.
Mutation pass: 3 killed, 1 survivor explained. Killed — a clock in the Floor’s stranded test, a wall-clock spend in the budget, and the march loop walking its units in a machine-dependent order, which is the shape AI-375 is actually about. The survivor is the same order mutation applied to the Floor’s idle block, and it survives for the reason AI-1410’s gate measured an hour earlier: over a played game no unit is ever Idle, so that loop’s body never runs and its order cannot matter. Recorded at the site rather than pinned.
AI-1430 Full-re-plan rate MUST sit at or below 20% of turns in steady state (AI-458), and long routes computed per turn MUST stay inside the platform's route budget, since route caching (13-command.md CM-1470) is a precondition rather than an optimisation.
⚠ Flagged gap, 2026-09-23 (Claude Code): the mechanism this measures is not built, so the rate is
100%. AI-458 is the rule — "plan continuation MUST be the default. The Staff MUST keep the previous
turn's plan and re-validate it cheaply, running a full re-plan only on an AI-545 trigger" — and
drawUp takes no previous plan. Its whole input is StaffInput (discs, terrain, stack limit, reach
budget, Rank) and it rebuilds the Docket, the portfolio and the search from the view on every call. Not a
defect in continuation: continuation does not exist yet. Recorded rather than tested, because a gate
citing this id today would either fail permanently or quietly measure something else.
*It did quietly measure something else on the first attempt, which is worth recording so the next session does not repeat it: counting the Floor's own “Re-planned N units” note over a 60-turn Duel game gave 24 of 55 steady-state turns, 44%. That is a real number about a real thing — per-unit re-targeting after a stall, AI-560’s stranded row doing its job — and it is not what this clause is a ceiling on. A unit abandoning a walk it has proved it cannot finish is the opposite of a full re-plan; it is the cheap local repair a full re-plan would replace.*
Partially held 2026-09-28: the trigger discipline that keeps re-plans rare stands; the ceiling itself measures an unbuilt subject at 100%. The triggers are pinned at packages/client/test/triage.test.ts's describe 'AI-545 and CM-1630' (its it 'the triggers are AI-545's own list' asserts, among other things, that a single lost unit does not re-plan), and the ceiling constant stands as REPLAN_RATE_CEILING_PERMILLE in packages/ai/src/budget.ts under packages/ai/test/budget.test.ts's describe 'AI-457, AI-458 — degrading without a cliff'. The row's own flagged-gap note records why the rate is 100% today: drawUp takes no previous plan (AI-458's continuation does not exist), and its first measured attempt — 24 of 55 steady-state turns, 44%, the Floor's per-unit re-targeting after a stall — is explicitly not what this ceiling bounds. What waits: continuation first, then this measurement.
AI-1440 Achieved plan depth MUST NOT correlate with contact count (AI-1306).
⚠ Flagged gap, 2026-09-23 (Claude Code): true today, and it cannot be given teeth. The depth this
Staff achieves is 0 local-search moves against a budget of 13,333 steps, of which it spends 5 — the
search exhausts its improving moves long before it approaches its allowance, so depth is not
budget-limited at all. There is no scarcity for a flooder to attack, and equally no way to demonstrate
that there is not. The mutation pass says so in the only way that counts: two mutants that encode E7
exactly both survive — shrinking the search budget in proportion to the disc count, and making the
Staff walk every Contact individually. Neither can be seen by a metric that reads 0 and 5 whatever
happens. This becomes testable when the portfolio search is budget-bound;
packages/client/test/tripwire-measures.test.ts is the file that will hold it, and pins the move count
exactly so the day that changes it fails there first. The cost half of AI-1306 is tested there now.
no-test-needed: true today and toothless by its own flagged-gap note — achieved depth is 0 local-search moves against a budget of 13,333 steps of which the Staff spends 5, so there is no correlation for any test to hold, and two mutants that encode E7 exactly both survive. What stands: packages/client/test/tripwire-measures.test.ts pins the move count exactly — its it 'a rising wall of Contacts does not buy the flooder any of the Staff's depth' — and tests AI-1306's cost half now; it is named there as the file this row fails in first the day depth becomes budget-bound.
AI-1450 The belief estimator's exploitability MUST be measured by tripwire row 24 and damped if the equilibrium proves degenerate rather than interesting.
no-test-needed: tripwire row 24 belongs to AI-1400's unbuilt instrumentation, so the estimator's exploitability has nothing to be measured against; packages/ai/src/belief.ts stands as the estimator itself. It becomes testable when row 24 lands.
AI-1460 Gate — graceful degradation. The same build's win rate at the mobile step budget versus at the desktop step budget MUST differ by no more than 10 points. A larger gap means a cliff rather than a slope, and the mobile build MUST NOT ship until it is closed (AI-457).
no-test-needed: no mobile build ships and no farm games run, so a win-rate gap between step budgets cannot be measured — packages/ai/test/budget.test.ts disclaims the row among the measurement programme; what stands is the degradation-knob rule in the same file (QUALITY_KNOB 'portfolio-breadth' under its AI-457 describe). It becomes testable with a mobile-budget build plus paired farm games.
AI-1470 Two qualitative measurements MUST be run even though they cannot be automated, because they are the ones that decide whether the AI is worth playing rather than merely strong: a blind panel shown anonymised AI turn summaries against human ones from a matching rating band, which should be no better than 65% accurate at Colonel and above; and a legibility test in which a player shown the Dossier mid-game is asked what the opponent is trying to do, which should be ≥ 70% correct by turn 80.
Rationale: a strong AI that plays like a spreadsheet is a different failure from a weak one, and it is not one a metric catches. It is caught by a person sitting down and saying "that was a real war," and nothing here substitutes for that.
no-test-needed: by this row's own words — two qualitative measurements that cannot be automated, a blind panel and an expert review, run because they decide whether the AI is worth playing rather than merely strong; there is no code to test, and until §21's ceiling is answered there is nothing for them to judge.
20. Quality gates and CI
AI-700 Determinism regression tests MUST run in CI, for each shipped AI and reference plugin × unit set × representative rule preset: re-run recorded games from seed and assert bit-identical order logs. The matrix MUST span WASM engines, not only operating systems, because the engine is the platform's rather than one we pin (AI-125): at minimum Node's engine on the supported host platforms, plus a headless run in each browser engine the client targets, plus one mobile web view per mobile platform. An order-log divergence between engines MUST be a release blocker and MUST be reported against the engine and the module together, since by AI-380 a conforming module cannot legitimately diverge. Where a headless harness for an engine is impractical, that engine MUST be recorded as unverified rather than dropped from the matrix.
Partially held 2026-09-28: the suite runs under verify and in a real browser, and the cross-platform matrix is defined — nothing executes it; that is this row's state of CI today. The determinism regression tests run inside full verify and pnpm test:browser (both named in AGENTS.md), and .github/workflows/determinism.yml defines the cross-platform matrix with its comparison machinery; AGENTS.md states the gap plainly — "There is no automated CI", the only remote is the server's bare repo so nothing runs it, and verify on your own machine is the whole gate. What waits: a runner for that matrix, and the WASM-engine span of the unit-set × engine product (no reference plugin exists to cover it).
AI-710 Budget-compliance tests MUST assert that the Staff meets its step budget at every Rank and its wall-clock calibration target at Captain on REF-HW (03-architecture.md AR-700) for the benchmark maps, and that hard-cap interruption produces a legal logged fallback (AI-240, AI-420). The suite MUST use 03-architecture.md's normative map tiers by name and MUST include a Standard-tier case and a Large-tier case matching the reference performance fixture (AR-760). Mobile and single-worker runs MUST be held to the same step budgets, with wall-clock measured and reported rather than enforced, and MUST be measured with AI-410 instrumentation active wherever fuel enforcement applies.
no-test-needed: budgets are declared in packages/ai/src/budget.ts and pinned by context at packages/client/test/ai-budgets.test.ts ('every context the requirement names has both numbers, or says why it has neither'), but the REF-HW wall-clock calibration of 03-architecture.md's AR-700 has no fixture — scripts/bench.mjs's header records that AR-760's own fixture cannot be built yet. It becomes testable when the reference-hardware fixture lands.
AI-715 Nightly the farm MUST run, and each of the first four MUST block on failure: a determinism leg with full hashing across the engine matrix; a sanity leg asserting zero crashes, zero stalls, zero illegal orders and zero Constraint-Floor violations at every Rank; a budget leg (AI-710); and a command-layer health leg asserting 13-command.md CM-090's presented-item bound holds against an AI-driven board from 20 to 400 units and that CM-1440's stalled count stays at zero. It MUST then run the promotion gate (AI-660), the anchor leg (AI-662), a multi-seat health leg (AI-659), the Antagonist roster (AI-1330), and the balance vector (AI-690).
no-test-needed: the nightly farm does not exist in this tree — no legs to block on, no determinism leg across an engine matrix; the closest standing substitute for its sanity leg is packages/client/test/floor-gate.test.ts's audit of Floor behaviour over 2,641 unit-turns. It becomes testable with §20's first nightly.
AI-717 The farm MUST support a sampled hashing policy for ranking and tuning runs — hashing periodically rather than every turn — and MUST use full hashing for determinism legs and for any run whose result is evidence in a dispute. A divergence under sampling is still caught, just later, and the minimised-replay bisector closes the gap.
Rationale: this is a place where a plausible-looking default — hash everything, it is safer — would silently cost the project a large share of its farm throughput, quite possibly exceeding all the simulation and all the AI thinking put together. It is worth a line in the configuration and a comment explaining why.
no-test-needed: a sampled hashing policy governs ranking and tuning runs — none exist in this tree (every shipped weight is still 1000; no tuner has ever run) — so there are no runs to sample, full or otherwise. It becomes testable with §17's first tuning campaign.
AI-720 A standard benchmark suite — maps, seeds, rule presets, the shipped unit set and at least one custom set — MUST be versioned in-repo and used for tier monotonicity (AI-625), the promotion gate (AI-660), and release-to-release non-regression: a release's Captain MUST score ≥ 45% against the prior release's Captain.
no-test-needed: a versioned benchmark suite is data whose consumers do not exist in this tree — tier monotonicity (AI-625), the promotion gate and strength regression all wait on §17/§20 machinery, so nothing could drift from the suite yet. It becomes testable with its first consumer.
AI-725 A strength regression test — a sequential test with a non-regression band — MUST run weekly and per-release against changes not intended to gain strength, such as a refactor or an engine optimisation. Correctness regressions (desync, illegal order, crash) MUST be caught by the nightly determinism and sanity legs and are never negotiable; behaviour regressions — the AI is no weaker but has become unpleasant to play against — MUST be caught by the balance vector against its rolling baseline, and are the class no single gate can express.
no-test-needed: a strength regression test is a statistical campaign over many games — sequential testing with a non-regression band, run weekly and per release against changes not intended to gain strength — and this tree has neither the farm to run it on nor a strength baseline to regress from (every shipped weight is still 1000); the findings it guards are, in this row's own words, "the class no single gate can express". It becomes testable with §20's first weekly.
AI-730 The project MUST maintain a rated human ladder with a fixed benchmark AI build as the anchor, so that AI strength is measured in the same units as human strength and a regression is visible as rating movement rather than as an impression. It MUST additionally track the uncomfortable metric: the fraction of human wins against the top Rank in which the human made a repeated, identifiable mistake. If a human beats the top Rank while playing badly, the AI is bad in a specific, findable way, and the replay names it.
no-test-needed: a rated human ladder is account-and-rating machinery of 08-services-platform.md — unbuilt in this tree (no uploads, no accounts) — and the fixed benchmark build it would anchor to waits on tuning that has not happened; both halves lack a subject. It becomes testable when both land.
AI-735 Determinism CI is not strength CI, and this document states it so that nobody has to learn it expensively: a fully green determinism board proves nothing whatsoever about play quality.
no-test-needed: a statement rather than a mechanism — §21.4 restates the same separation, and each CI kind tests under its own rows (determinism at AI-700's; strength nowhere yet); there is nothing extra to pin.
AI-740 Personality-signature tests MUST quantify each personality's character as a measurable difference from Aurel at the same Rank — median turns from contact to first attack, un-integrated city count, entrench rate, share of Operations targeting rear assets, arrival timing relative to a published schedule — and MUST run in the promotion gate. A personality whose signature is statistically indistinguishable from Aurel MUST be re-tuned or cut, never shipped as flavour text.
no-test-needed: quantifying each personality's character against Aurel over many games is the §17 pipeline's work and needs the farm — packages/client/test/personality.test.ts disclaims the row verbatim alongside AI-810; what stands is the roster itself, pinned at that file's describe 'AI-825 — the shipped roster is exactly seven', with the dial structure under its AI-800/AI-815 describes.
AI-745 Fault-injection tests MUST cover: trapping plugins; infinite loops and fuel exhaustion; memory-limit breaches; malformed orders; failed restore; hot-reload; a module declaring a disallowed import or a banned feature, refused at load with the named diagnostic (AI-210, AI-380); a worker terminated mid-turn yielding the AI-240 fallback and a replayable log; a memory-growth attempt past the declared maximum trapping rather than being silently satisfied (AI-430); and a fuel-transform version mismatch refused as verification evidence rather than accepted (AI-410).
Partially held 2026-09-28: seat-side faults are pinned; every plugin-row fault waits on a host to trap in. packages/server/test/ai-seat.test.ts pins 'absorbs refused plan orders and still commits the seat (AI-240 fallback shape)' and 'treats a throwing generator as a named host defect, not an engine refusal', and packages/client/test/ai-seat-contract.test.ts slips one deliberately illegal order through the door to watch it refused with a typed rejection. What waits: trapping plugins, infinite loops and fuel exhaustion, memory-limit breaches and failed restore — each presupposes the §12 plugin host.
AI-760 Telemetry, opt-in and subject to 08-services-platform.md's privacy rules, SHOULD aggregate anonymised AI win rates by Rank, personality and unit set to inform tuning priorities and to feed AI-635's measured selector.
no-test-needed: telemetry is opt-in under 08-services-platform.md's privacy rules, and that platform does not exist in this tree (no upload surface), so there is no aggregation to test and no consent machinery to check; it becomes testable with 08's ingestion.
21. How strong will this actually be?
An over-promised AI that ships weak is the worst outcome available, so this section is written to be falsifiable rather than encouraging.
21.1 High confidence
The Constraint Floor (AI-560). Garrisons, Cadre routing, Depot chains, lift, endurance, production continuity, no idle unit, no stranded formation, retreat degree. These are arithmetic against specifications that already exist in closed integer form, and they are precisely where opponents in this genre visibly fail. An AI that is perfect here will be perceived as strong before it has made a single interesting decision.
Fog-honest belief (§8) and deadline-driven objectives (AI-525). Both are structurally easy in this game and structurally hard in most others, because 14-victory.md publishes a calendar of what matters and 01-game-rules.md gives belief a real data structure.
Economic reasoning (AI-530, AI-533). 12-economy.md gives exact payback formulas and 02-units-and-industry.md gives an Industry payback. The Staff performs arithmetic the designers already did, and it will make these decisions better than most humans simply because most humans will not do the sums.
21.2 Moderate confidence
Operational assignment by portfolio plus local search. The technique is sound and §6.2's landscape argument is plausible, but "Operations are mostly separable" is an assumption. Tripwire row 19 is the test; if independent restarts scatter, the landscape is rugged and the answer is more restarts or simulated annealing rather than a redesign — but the cost goes up.
Risk-weighted plan choice (AI-1200, AI-1210). The mechanism is elegant and k will need real
tuning; a badly set caution coefficient produces either a suicidal or a catatonic opponent, and both are
worse than a mediocre one. Tripwire rows 20 and 21 bracket it.
Not becoming a turtle. The specific shape collapse is most likely to take, and it will not look like collapse: an AI holding many cities with perpetually short Requisitions and production largely consumed by garrison units finds that every candidate offensive scores below the defensive value of holding, and stops. It becomes a defensive machine that never loses a battle and never wins a game — and it will look healthy in self-play, because both sides do it and the games are balanced and long. AI-1180's horizon estimate is the structural answer: the victory system is the planner's schedule, not a term in its sum. An AI that treats victory as a term will turtle; an AI that treats it as a deadline computes "I hold 22, the floor reaches 25 at turn 190, I need three more cities" and generates an offensive because it has to. Tripwire row 15 detects it, and the counterfactual replay that settles it is cheap: re-run AI-versus-AI games with the Ebb disabled, and if game length is unchanged, the AI was never responding to the clock in the first place.
21.3 Low confidence — the one place this ships weak
The Sandtable is the risk. If its ranking of plans is noise, the entire L2 layer is decoration — an expensive greedy heuristic with a random tiebreaker — and every step spent above the Constraint Floor is wasted. This is exactly the failure mode where an AI is announced as advanced and plays like a script.
AI-1490 The plan-ranking skill measurement MUST be built as a first-class farm job before the Sandtable is optimised: sample 200 turns from farm games; take the top 5 plans by Sandtable score; play each forward in the real engine for 5 turns against a fixed opponent policy; record the true Assay delta; and compute the rank correlation between Sandtable rank and true rank. The response MUST be:
- ρ ≥ 0.5 — the Sandtable earns its cost. Invest in depth.
- 0.3 ≤ ρ < 0.5 — marginal. Hold depth at 2 and spend the budget on local search instead.
- ρ < 0.3 — the Sandtable is not working. Replace it with a one-ply Assay delta, cut hypotheses to one, and ship the greedy-plus-local-search Staff.
Rationale, and the reason this design is comfortable proposing the Sandtable at all: the fallback is still a competent opponent. Greedy assignment plus local search on a static evaluation, over a good Docket, on top of a perfect Constraint Floor, is roughly what the better shipped strategy AIs are. We would lose the multi-turn concentration and the risk-weighted strategic dilemma, which are the two things this design most wants, but we would not lose the product. Building the measurement first is what tells us whether to optimise the Sandtable at all.
no-test-needed: the row orders a farm job — sample 200 turns from farm games, play out the top five Sandtable-scored plans against their best challenger, measure plan-ranking skill before optimising — and no such job exists in this tree (scripts/self-play/ measures game length for 14-victory.md; it is not §20's nightly); packages/ai/src/sandtable.ts stands unoptimised, and §21.3 records the ρ decision table in prose until this lands.
21.4 The honest ceiling
This design aims to beat most players and lose to experts. It will not be superhuman, and the gap against a strong human will be exactly where §21.2 says: choosing the decisive axis and timing a concentration, where human pattern recognition over a hundred games is worth more than a quarter of a million integer steps. What the Staff will have that a strong human will not is that it never gets tired, never leaves a city empty on turn 140, always does the Cadre arithmetic, and always notices the Alarm on the turn it fires.
Two further honest notes. The Staff has no long-horizon commitment beyond the switching cost: it re-derives its plan at every trigger, while a strong human plays a forty-turn plan. Fixing that properly needs plan persistence with an explicit commitment ledger, and it is deliberately out of scope for the first version, because a half-built commitment mechanism produces an AI that stubbornly pursues a dead plan — which is worse than one that re-plans too often. Open question 7 states the shape.
And: self-play makes the AI good at playing itself. AI-658's diversity mechanisms mitigate this and do not eliminate it. The honest expectation is that the Staff will have a characteristic style, that strong human players will find its weaknesses within a few dozen games, and that patching those weaknesses is an ongoing content activity rather than a one-time training run. Plan for AI updates as a live activity, or accept that the AI is at its strongest on release day and declines relatively thereafter. AI-352 is what makes that affordable: an AI change breaks no replay and needs no rules version.
21.5 What this design is deliberately not doing
- No full-game tree search over the real engine. A full Cascade at scale is far too expensive for the thousands of simulations a Monte Carlo method needs; the forward model has to be abstract, which is why the Sandtable exists. Tree search over the Sandtable was considered and rejected for the shipping configuration: the tree is shallow in turns rather than moves, the leaf evaluation is deterministic so an exploration bonus has little to buy, and random rollouts would put PRNG consumption on the decision path. The condition under which it becomes right is specific — a value function with genuine uncertainty estimates, or opponent hypotheses as a sampled distribution rather than three fixed cases — and this decision should be revisited then.
- No neural network in the shipped client at version one. AI-685 states the gate.
- No per-unit tactical search. Delegated entirely to the core (AI-575).
- No online learning beyond AI-1235's mixing width.
- No cheating in any form. §3, and AI-1405 makes the claim mechanically checkable rather than merely asserted.
- No coordination between AI seats (AI-340).
- No modelling of human psychology. No bluffing model, no tilt detection, no adaptive difficulty inside a game. Tempting, and a different project.
22. Contracts on other documents
Obligations this document places on subsystems it does not own, recorded so that a change on either side is detectable (00-overview.md OV-060).
| From | What is needed |
|---|---|
| 10-turn-model.md | Initiative computable by a seat from its own committed state, and the seat's own relative activation order readable before commit (TM-380), with the global order remaining withheld (TM-1120). Rolling planning (TM-2550) opened to AI seats on the same terms as human clients. A forked Cascade over a candidate order set, without mutating the live world — AI-585's Rehearsal depends on this and on nothing else. |
| 11-combat.md | The exact predictor over a fog-limited knowledge view (CB-2250, CB-2300) at the cost CB-2590 states, remaining the AI's only predictor (CB-2290). The pivot analysis of CB-2340 exposed as a callable that names the dominant unobserved field, so the Staff can generate reconnaissance against it (AI-1130). The full-domain evaluation of CB-2330 exposed as a pair — the pessimistic and the realistic reading — so that attack authorisation and avoidance can use different ones (AI-1305). The aggregate and support shape used by AI-570, so the Sandtable's arithmetic and the engine's do not diverge. |
| 12-economy.md | Effective Works, Levy, Integration, Unrest, Reach propagation, supply state and the Cadre cost curve exposed as pure functions over a hypothetical state, callable outside the turn pipeline, so the Sandtable's economy is exact rather than approximated (AI-565). The garrison requirement by class and Integration (EC-1170) queryable per city. An incremental Reach query (AI-1170). |
| 13-command.md | Everything, unchanged: the Staff writes Postures, Sanctions, Formations, Groups, Postings, Consignments, Requisitions, Doctrines and Tasks, and reads them back. Four requests, all small. (a) A read API for a seat's own Attention Event stream, which CM-1610 already requires to be emitted, so the Staff can use it as a change detector (AI-545). (b) Confirmation that CM-100's "identical for every AI seat" means the AI uses the same core command layer, which §6.2 rests on. (c) Recommended amendment: CM-610's target selection by highest predicted per-mille has no term for target value, so an automated unit always eats the safest available target rather than the most valuable — an unlimited-duration tempo sink for a human player's automation as well as for an AI. AI-1308 states the Staff's own rule; the human-facing half belongs to 13-command.md. (d) Recommended amendment: CM-1720 caps the UnitLost stake at 40 regardless of unit cost, so the Dispatch tells a player that losing an Administrative Cadre and losing the cheapest unit are the same event. AI-1310 stops the Staff inheriting that; the player-facing half belongs to 13-command.md. |
| 14-victory.md | Per-Track progress and threshold functions evaluable at a hypothetical state, which VC-350's uniform shape already promises the AI evaluator. The banded values as a seat sees them, with band edges, so AI-1120's force accounting uses published information and nothing more. The Ebb closed form, the Tally rotation and the Warrant schedule as forward-readable data. The AI Verdict policy VC-2810 asks for is supplied in AI-940. The calibration table of VC-3210 needs a Rank column, because a game against a top-Rank AI is a materially shorter game than one against the default Rank, and a length estimate that ignores that is one of the lies that document is worried about. |
| 01-game-rules.md | Chokepoints from map generation where derivable, or the terrain data to derive them once at load (AI-520). Region membership per city and the abstract chunk graph the Atlas is built on (AI-515). |
| 02-units-and-industry.md | Capability fields and the closed flag vocabulary (US-380, US-470) sufficient that the Staff never branches on a unit's identity (AI-590). Industry upgrade costs and outputs for the payback computation. The per-player Industry cap as the handicap lever of AI-105. |
| 03-architecture.md | Hierarchical pathfinding callable for abstract node-to-node distance (AR-750). A worker to run the Staff off the UI thread (AR-860's fallback included). The AI-private stream derivation (AR-370) and the one-way worldgen seed (AR-400). The PluginRuntimeConfig record (AR-857). An ai/ row in the container table, optional and outside the state hash (AR-560, AR-570). The fog-leak property test extended to include an AI seat (AI-1405). A measured games per core-hour figure for the standard tuning game published alongside the benchmark suite, because every cost in §17 rests on unmeasured budgets until it exists. |
| 05-multiplayer.md | The substitution transport and consent flow (MP-1010), which this document supplies the mandates for (AI-485 through AI-498). The ranked-play policy for third-party AI seats, which AI-370 constrains but does not settle. |
| 07-modding-content.md | The plugin container, sandbox, signing, transport (MOD-660) and SDK roster (MOD-670(a)), and the fuel-metering transform (MOD-610, MOD-615), all cited rather than restated. MOD-1290's three findings are resolved here: the deleted-requirement citations are gone, AI-230 is restated against Orders and the Cascade, and AI-600 no longer contemplates executable content in a unit set. |
| 08-services-platform.md | The farm as a service at the scale §17 needs; the rating pool that feeds AI-635's measured selector and AI-730's ladder; the nightly regeneration gate; and version pinning of AI build, manifest and model in every game configuration and replay header (AI-670). |
| 04-ui-ux.md | The difficulty selector with its measured number and named blind spots (AI-630, AI-635, AI-640); personality setup cards including the full-expression note (AI-830) and the Override warning (AI-835); the Dossier (AI-752); the Debrief and the Second Opinion (AI-754, AI-758); the Terms disclosure of substitution policy and every active handicap (AI-100, AI-494); and how a replay viewer distinguishes an authority-issued fallback order (AI-240) from a seat-issued one. |
Open questions
Is the Sandtable earning its cost? AI-1490 is the measurement and it must be built before the Sandtable is optimised. Everything in §6.4 and §6.5 is contingent on its answer, and §21.3 states the fallback.
Is the plan-and-compile separation (AI-580) affordable at version one? It is the red team's sharpest correction and it is also the largest single piece of architecture in this document. The cheap version — plan in the order vocabulary and accept the ceiling — is what most projects ship and it is why most strategy AIs cannot conceive a feint. The decision must be made before anything else is built, because it is the shape of the system rather than a feature of it.
Should
AI Full Vision(AI-330) be withdrawn? 00-overview.md OV-110 requires it to exist as a labelled, disclosed, recorded, off-by-default option, and AI-330 provides it. The argument for withdrawing it entirely and shipping only AI-150's Foreknowledge is in AI-330's rationale and this document finds it persuasive: full vision does not make a harder opponent, it makes a different and worse one. This is a change to OV-110 and therefore not this document's to make.How many opponent hypotheses, and are they the right ones? Passive / Continuation / Best reply is a guess. A fourth — an opportunist that assumes the opponent takes whatever is left undefended — may be worth more than Best reply at a tenth of the cost. The farm can settle this by ablation and should before the top Ranks are tuned.
Is the marginal forked Cascade cheap enough for the Rehearsal (AI-585)? The estimate that a copy-on-write fork with activation memoisation brings a candidate Cascade to a small fraction of a full one is the single load-bearing unmeasured assumption in this document. It is measurable as soon as the Cascade exists and it should be measured before anything else.
Is trigger-based re-planning (AI-545) safe, or does it produce a lazy AI? The saving is large and the eight-turn anti-staleness floor is a backstop, but a slowly deteriorating position that never fires a trigger is exactly the case where re-planning matters most. The Assay-gradient trigger is in AI-545 and its threshold is not yet chosen.
Long-horizon commitment. §21.4 names this as the main strategic weakness. The shape of a fix is a commitment ledger, in which an Operation the Staff has invested
Nturns in accrues a decaying bonus to its own continuation. The danger is obvious: a badly-tuned ledger produces an AI that pursues a dead plan into a wall, which is a far worse failure than re-planning too often. It needs a designed decay curve and a hard abandonment override.Commit-reveal for the mixing salt (AI-1225). In ranked AI-versus-human play where replays must verify, the salt's hash must be published at game start and the salt at game end. The mechanism belongs to 05-multiplayer.md and is not yet specified there.
Does the Staff's own triage need its own attention model, or is a ranked list of Constraint-Floor obligations plus Docket value enough? AI-1310 forbids borrowing the Dispatch's Urgency score but does not specify the replacement's terms.
Fuel calibration under instrumentation (AI-410). The transform makes fuel counts portable, but the fuel-to-wall-clock ratio still varies by engine and device, so a budget generous on the reference host may be tight in a mobile web view. Pin one fuel figure per context and accept the spread, or calibrate a per-platform multiplier at install from a startup benchmark — which is a wall-clock input and so MUST NOT be allowed to change the recorded budget in any verification context. Either way the benchmark result is host-side only: with no clock in the ABI, plugin code cannot observe it, which is the property that keeps AI strength independent of hardware.
The memory caps of AI-430 and AI-435 are estimates, not measurements, and the question got harder rather than easier: on a client the AI aggregate, the AI-235 core replica and the whole UI share one process with an OS-imposed ceiling. Do the caps belong here permanently, or should they migrate into a plugin-memory row of 03-architecture.md's budget table once measured — and does that row then need to be a client-process budget rather than a per-subsystem one?
App-store policy for workshop-installed AI plugins on mobile (AI-275). Code executed by the web view's own engine is the sanctioned exception to the rule against downloadable executable code, but a plugin fetched from our workshop is an untested framing of it. Ship workshop AI on mobile, restrict mobile to shipped personalities, or seek a review determination first? A policy question, not a technical one, but it decides whether AI-270 is platform-complete.
Ownership boundary for the plugin host. 03-architecture.md AR-060 names AR-855 as the host requirement, and this document specifies instantiation (AI-125), import vetting (AI-210), fuel instrumentation (AI-410), worker isolation (AI-440) and memory caps (AI-430). Two of the five now have an answer to record — the import object's contents are AR-855's to constrain with AI-200 as the AI-specific instance, and metering is deterministic fuel with wall clock demoted to the AI-420 watchdog — and the remaining three need a single owning identifier each, agreed before implementation begins.
Engine-diversity policy when AI-700 finds a real divergence. WebAssembly's semantics say a conforming module cannot diverge between conforming engines, so the honest options are to block the release, ship with the affected engine excluded from verification contexts (AI-370), or carry a plugin-side workaround. Decide the policy before the first divergence rather than under release pressure.
Is a zero-Decisive-Point Cadet too strong? The command layer is good enough that an AI which merely sets sensible Postures may be a harder first opponent than a beginner should meet. The fix if so is worse Sanctions — a higher engagement threshold so it rarely fights, a tighter Leash so it rarely arrives — which is still competence-shaped weakness but is a second weakness mechanism, and two is one more than this design wants.
Do Captain and Colonel feel different to a player who does not understand supply? The features that separate them are the ones a strong player values most and a casual player notices least. If the middle of the ladder is flat, something other than omission has to differentiate it, and it is not clear what.
Does personality survive repeated tuning? AI-810 freezes the offsets but not their meaning: when the baseline moves, every offset points somewhere slightly different, and after several promotions a personality may be something nobody designed. Re-measuring the price (AI-805) catches the magnitude of the drift but not its direction, and periodic hand review is not a system.
Is Sabar a good addition to a multiplayer game or a rigged one? AI-935 turns it off by default above one human seat and clamps
predation. In solo it is clearly good — it is what stops a runaway and keeps the middle third of a game live. Between humans it is genuinely unresolved.What does an AI seat do about hidden per-player objectives? It must not act as though it knows another seat's concealed objective. Inferring one from unexplained behaviour is legitimate and is exactly what the top Rank's opponent model is for — but the line between "inferred from observed behaviour" and "read the game state" is one bad import away, and it needs an explicit test rather than an assurance. AI-1405 is the shape of that test; the specific fixture does not exist yet.
Can a fog-honest AI ever stop looking like a cheat? The Debrief answers it after the game; nothing answers it during. The likely real answer is a short in-game explanation attached to any AI action that surprised the player — "it did not see your transport; it was already screening that ford" — but that is an information channel and it needs a fog review this document has not done.