# Order composer This doc covers the local order draft: the per-game in-memory list of commands the player has composed but not yet submitted, the `Cache`-backed persistence that survives reloads, and the `historyMode` flag that hides the composer when the user is browsing past turns. The user-facing spec — the IA section's sidebar description, the `Order` tab placement, the per-command status display — lives in [`../PLAN.md`](../PLAN.md), section `Information Architecture and Navigation`. This doc is the source of truth for how those rules are implemented. ## Draft replaces server order The client's view of "the player's intent for the next turn" lives entirely in the local draft. The gateway exposes a turn-scoped `user.games.order` endpoint that the submit pipeline drains the draft into; until the player explicitly submits, the server has no view of pending commands. The composer never reads back the server's idea of an order — it reads its own draft and renders it. The motivation is operational: a draft can be edited, reordered, or partially removed without round-trips, and a half-composed order during a connectivity hiccup keeps every line the player typed. A remote-first composer that reflects the gateway's pending-orders queue would force a sync on every keystroke. When the submit pipeline lands (Phase 25), it iterates the draft in order, sending one `command` per line. The gateway's per-line result rejoins the draft entry through `cmdId`, and the entry's `CommandStatus` flips to `applied` or `rejected`. Successfully applied entries stay visible until the next turn cutoff so the player can see what was committed; rejected entries stay until the player edits or removes them. ## Local-validation invariant Every command in the draft is *locally valid as of submission time*. A command may live in the draft as `draft` (not yet validated) or `valid`; `invalid` is allowed during composition but the submit pipeline refuses to drain a draft that contains any `invalid` entries. The validation step is per-command and pure — it consults the current `GameStateStore` snapshot only, never the network. Phase 12 ships the skeleton without any concrete validators: the single `placeholder` variant is content-free and stays at `draft` forever. Phase 14's `planetRename` is the first variant that exercises the `draft → valid | invalid` transition. ## Command status state machine ```text draft ──validate──▶ valid ──submit──▶ submitting ──ack──▶ applied ╲ │ ╲ ╲──validate──▶ invalid ╲──nack──▶ rejected ``` Transitions: - **`draft → valid` / `draft → invalid`**: local validation. May re-run when the underlying `GameStateStore` snapshot changes (Phase 14+). - **`valid → submitting`**: the submit pipeline picks the entry off the draft and sends it to the gateway. - **`submitting → applied` / `submitting → rejected`**: the gateway responded; the entry is no longer in flight. Phase 12 stores the type but does not implement any transitions. Every entry remains at `draft` until later phases land the validators (Phase 14) and the submit pipeline (Phase 25). ## Discriminated union shape `OrderCommand` is a discriminated union on the `kind` field. Phase 12 ships a single variant: ```ts interface PlaceholderCommand { readonly kind: "placeholder"; readonly id: string; readonly label: string; } type OrderCommand = PlaceholderCommand; ``` The `id` field is the canonical identifier the store uses for remove and reorder; later variants must keep `id: string` so the store API stays uniform. The whole draft round-trips through IndexedDB structured clone, so every variant must use only JSON-friendly value types. Phase 14 adds the first real variant (`planetRename`) and updates this list. ## Store `OrderDraftStore` lives in [`../frontend/src/sync/order-draft.svelte.ts`](../frontend/src/sync/order-draft.svelte.ts). The class is a Svelte 5 runes store, so the file extension is `.svelte.ts` (the original PLAN.md artifact line listed `.ts` — the deviation is documented inline in `PLAN.md`'s Phase 12 "Decisions" subsection). Lifecycle: | Method | Effect | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `init({ cache, gameId })` | Reads the persisted draft from `cache`; sets `commands` and flips `status` to `ready`. Errors flow into `status = "error"`. | | `add(cmd)` | Appends to the end and persists. | | `remove(id)` | Drops the matching entry and persists. A miss is a no-op. | | `move(fromIndex, toIndex)` | Reorders within the list and persists. Out-of-range and identical indices are no-ops. | | `dispose()` | Marks the store destroyed; subsequent `persist()` calls are no-ops so a fast game-switch does not write stale state into the next id. | Mutations made before `init` resolves are silently ignored — the layout always awaits `init` through `Promise.all([...])` next to `gameState.init` before exposing the store. Layout integration mirrors `GameStateStore`: - One instance per game, created in [`../frontend/src/routes/games/[id]/+layout.svelte`](../frontend/src/routes/games/[id]/+layout.svelte). - Exposed through the `ORDER_DRAFT_CONTEXT_KEY` Svelte context. - Disposed in the layout's `onDestroy`. The order tab consumes the store via `getContext(ORDER_DRAFT_CONTEXT_KEY)`; Phase 14's planet inspector will use the same key to push a new command. ## Persistence Cache row layout: | Namespace | Key | Value type | | -------------- | ------------------ | ---------------- | | `order-drafts` | `{gameId}/draft` | `OrderCommand[]` | The store writes the full draft on every mutation. Phase 25 may profile the submit pipeline and batch into a microtask if write amplification becomes a problem; until then the deterministic write-on-every-mutation model is what tests assert and what the layout relies on for crash safety. The namespace is registered in [`storage.md`](storage.md). Cache implementation details live there; this doc only describes how the order composer uses the namespace. ## History mode wiring Phase 26 introduces a global history-mode flag. The IA section specifies that the Order tab is hidden when history mode is active — the player is browsing a past turn snapshot, and composing commands against an immutable snapshot would be confusing. Phase 12 wires the flag end-to-end as a prop. The layout owns the flag (a constant `false` until Phase 26) and passes it to: - `Sidebar` as `historyMode`. The sidebar forwards it to its `TabBar` as `hideOrder`. The Order entry is filtered out of the tab list when true. If a `?sidebar=order` URL seed lands while the flag is true, the sidebar falls back to `inspector`. If the active tab is `order` when the flag flips on, an effect resets it to `inspector`. - `BottomTabs` as `hideOrder`. The mobile bottom-tab `Order` button is suppressed when true. The store itself stays alive across history-mode round-trips so the draft survives. Phase 26 will replace the constant with the real signal from `lib/history-mode.ts` and exercise the toggle in its own test suite. ## Testing Two test artifacts cover the skeleton: - [`../frontend/tests/order-draft.test.ts`](../frontend/tests/order-draft.test.ts) — Vitest unit tests for the store. Drives `OrderDraftStore` directly with `IDBCache` over `fake-indexeddb`. Covers init, add, remove, move, per-game isolation, mutations-before-init, and dispose hygiene. - [`../frontend/tests/e2e/order-composer.spec.ts`](../frontend/tests/e2e/order-composer.spec.ts) — Playwright spec. Seeds three commands through `__galaxyDebug.seedOrderDraft`, navigates into `/games//map`, opens the Order tool (sidebar tab on desktop, bottom tab on mobile), asserts the rows, reloads, and asserts the rows again. The `__galaxyDebug.seedOrderDraft(gameId, commands)` and `__galaxyDebug.clearOrderDraft(gameId)` helpers in [`../frontend/src/routes/__debug/store/+page.svelte`](../frontend/src/routes/__debug/store/+page.svelte) write directly to the cache namespace, so seeding is independent of any mounted store.