feat(dict): make the build's dictionary version deploy-authoritative

The dictionary lives on a persistent volume seeded from the image once and never
re-seeded, so a redeploy that bumped BACKEND_DICT_VERSION left the new DAWGs
unreachable (the volume mount shadows the image copy) and the active version stuck
at whatever the admin console last installed — a native offline-first client then
fetched the server's older pinned version over the network instead of using its
bundled dict.

On boot the backend now DELIVERS the build version onto the volume add-only, from a
second unshadowed image copy at BACKEND_DICT_SEED_DIR, into DICT_DIR/<version>/ when
absent (the flat seed and prior uploads untouched, so in-flight games keep theirs),
and InitActiveVersion makes the build version active — except a console-installed
version NEWER than the build (compared numerically) is not downgraded by a restart.
The .seed_version guard still protects the flat seed's label (a bump is a new
subdirectory, never a relabel). The console stays for out-of-band updates.

Docs: ARCHITECTURE.md §5, deploy/README.md, seedmarker.go. Tests: engine.DeliverVersion
(add-only, idempotent, flat-seed skip), compareDictVersions, and the dictionary-update
integration test's deploy-delivers path.
This commit is contained in:
Ilia Denisov
2026-07-15 00:21:20 +02:00
parent f8aeec8008
commit 48614e622d
13 changed files with 342 additions and 44 deletions
+21 -16
View File
@@ -446,22 +446,27 @@ Key points:
version while new games use the new one. A restart re-loads every resident
version via `engine.OpenWithVersions` (the flat seed plus each subdirectory,
skipping the `.staging/` upload area) and restores the active pointer from
`dictionary_state`. The volume preserves uploaded versions across redeploys;
once seeded it is not re-seeded, so after bootstrap dictionary changes go through
the console rather than a rebuild. Because the flat DAWGs carry no embedded
version, `OpenWithVersions` records the version the flat directory was first seeded
at in a `.seed_version` marker on the volume and treats that marker as
**authoritative** (the **seed-drift guard**): on an already-seeded volume a later
`BACKEND_DICT_VERSION` is ignored, so a bumped build seed cannot relabel the
already-seeded bytes — which would otherwise silently serve the wrong dictionary
and void games pinned to the prior label. A running contour therefore moves to a
new release **through the console** (the prior version stays resident, so its games
keep replaying), and `DICT_VERSION` is the seed for a **fresh** volume only:
bumping it on a live contour is a harmless no-op that takes effect on the next
fresh volume. Set it per contour from the deploy's `TEST_`/`PROD_DICT_VERSION`.
(The dictionaries ship as a versioned **release artifact** from the
`scrabble-dictionary` repo; the build's `DICT_VERSION` selects
only the seed.)
`dictionary_state`. The volume preserves uploaded versions across redeploys, so
in-flight games keep replaying on the version they pin. The build's
`BACKEND_DICT_VERSION` is **deploy-authoritative**: on boot `engine.DeliverVersion`
copies the image's DAWGs — kept unshadowed at `BACKEND_DICT_SEED_DIR`, since the
volume mount hides the image's `BACKEND_DICT_DIR` copy — into
`BACKEND_DICT_DIR/<version>/` when that version is not already resident (**add-only**:
the flat seed and prior uploads are never touched), and `InitActiveVersion` makes it
the active version. So a redeploy that bumped `DICT_VERSION` moves new games to the
new dictionary automatically, while old games stay on theirs — no console step. The
one exception: a version installed out-of-band through the console that is **newer**
than the build version and still resident is kept, so a restart on an older image never
downgrades a hotfix (versions compare numerically). Because the flat DAWGs carry no
embedded version, `OpenWithVersions` records the version the flat directory was first
seeded at in a `.seed_version` marker and treats it as authoritative for **that flat
directory** (the **seed-drift guard**): a later `BACKEND_DICT_VERSION` never relabels
the already-seeded flat bytes — it is delivered as a **new subdirectory** instead — so
the guard still prevents mis-serving a game pinned to the flat label. The console
remains the way to update a dictionary **without** a redeploy (an out-of-band upload).
Set `DICT_VERSION` per contour from the deploy's `TEST_`/`PROD_DICT_VERSION`. (The
dictionaries ship as a versioned **release artifact** from the `scrabble-dictionary`
repo.)
- Move generation/validation/scoring use `Solver.GenerateMoves` (ranked),
`Solver.ValidatePlay` and `Solver.ScorePlay`; board mutation uses
`scrabble.Apply`. The engine adds its own deterministic, seeded tile **bag**