feat: on-device move preview (local eval) with network fallback
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 58s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 58s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
Score and validate a tentative move on-device instead of a per-arrangement network
round trip. The dawg reader and the validate/score/direction slice of the
scrabble-solver engine are ported to TypeScript (ui/src/lib/dict), pinned
byte-for-byte to the Go engine by a `conformance` CI job (full-dictionary reader
parity plus a battery of plays across every variant and both cross-word rules,
including the inferred orientation). The server stays authoritative — submit_play
re-validates — so the local result is an advisory accelerator only.
- backend: Registry.DictBytes + an authed GET /api/v1/user/dict/{variant}/{version}
(immutable) streaming the pinned per-game dawg; caddy routes /dict to the gateway.
- gateway: a session-gated /dict edge route proxying it; fetchDict on the transport.
- client: the dictionary loads on game open (low priority so it never starves the
game on a slow link; aborted at a 5s cap or when leaving the game), is cached in
IndexedDB (best-effort, self-healing on a rejected blob) and reused across
sessions; a warm-up overlay covers a cold load, then the network preview is the
fallback; a bad-connection breaker stops warming after repeated misses; the move
preview cancels its in-flight request when the tiles change.
- parity generators backend/cmd/{dictgen,validategen} + gated Vitest suites, run in
CI against the release dictionaries. A hidden debug readout lists the cached
dictionaries + breaker state, and its reset clears the cache.
- docs: ARCHITECTURE §5, TESTING, UI_DESIGN, FUNCTIONAL (+ru).
This commit is contained in:
@@ -207,11 +207,66 @@ jobs:
|
||||
run: pnpm run test:e2e
|
||||
timeout-minutes: 5
|
||||
|
||||
# conformance proves the client's local move preview (the ported dawg reader +
|
||||
# validator, ui/src/lib/dict) byte-for-byte against the authoritative Go engine:
|
||||
# a Go step generates golden parity vectors from the release dictionaries, then the
|
||||
# gated Vitest suite replays them. It spans both toolchains, so it runs whenever the
|
||||
# Go engine side or the UI side changed.
|
||||
conformance:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.go == 'true' || needs.changes.outputs.ui == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
GOPRIVATE: gitea.iliadenisov.ru/*
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch dictionary DAWGs
|
||||
run: |
|
||||
mkdir -p "${GITHUB_WORKSPACE}/dawg"
|
||||
curl -fsSL -o /tmp/dawg.tar.gz "https://gitea.iliadenisov.ru/developer/scrabble-dictionary/releases/download/${DICT_VERSION}/scrabble-dawg-${DICT_VERSION}.tar.gz"
|
||||
tar xzf /tmp/dawg.tar.gz -C "${GITHUB_WORKSPACE}/dawg"
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.work
|
||||
cache: true
|
||||
|
||||
- name: Generate golden parity vectors
|
||||
run: |
|
||||
go run ./backend/cmd/dictgen -dawg-dir "${GITHUB_WORKSPACE}/dawg" -out /tmp/dictgold
|
||||
go run ./backend/cmd/validategen -dawg-dir "${GITHUB_WORKSPACE}/dawg" -out /tmp/validgold
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm@11.0.9
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ui
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Local-eval conformance (reader + validator vs the Go engine)
|
||||
working-directory: ui
|
||||
env:
|
||||
DICT_DAWG_DIR: ${{ github.workspace }}/dawg
|
||||
DICT_GOLD_DIR: /tmp/dictgold
|
||||
DICT_VALID_DIR: /tmp/validgold
|
||||
run: pnpm exec vitest run src/lib/dict/
|
||||
|
||||
# gate is the single branch-protection required check. It always runs and passes
|
||||
# only when each upstream job succeeded or was skipped (a path-filtered no-op),
|
||||
# failing the merge if any actually failed or was cancelled.
|
||||
gate:
|
||||
needs: [unit, integration, ui]
|
||||
needs: [unit, integration, ui, conformance]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
@@ -221,7 +276,7 @@ jobs:
|
||||
- name: Aggregate required checks
|
||||
run: |
|
||||
fail=
|
||||
for r in "unit:${{ needs.unit.result }}" "integration:${{ needs.integration.result }}" "ui:${{ needs.ui.result }}"; do
|
||||
for r in "unit:${{ needs.unit.result }}" "integration:${{ needs.integration.result }}" "ui:${{ needs.ui.result }}" "conformance:${{ needs.conformance.result }}"; do
|
||||
name="${r%%:*}"; res="${r#*:}"
|
||||
echo "$name = $res"
|
||||
case "$res" in
|
||||
@@ -340,6 +395,23 @@ jobs:
|
||||
docker logs --tail 50 scrabble-backend || true
|
||||
exit 1
|
||||
|
||||
- name: Probe the /dict edge route reaches the gateway
|
||||
run: |
|
||||
set -u
|
||||
# The client fetches each game's dictionary blob at {edge}/dict/{variant}/{version}
|
||||
# for the local move preview. If caddy does not route /dict to the gateway the request
|
||||
# falls to the static landing and the client silently gets a non-dawg blob. Probed
|
||||
# unauthenticated it must be the gateway's 401 (the route reaches the gateway), never a
|
||||
# 404/200 from the landing catch-all.
|
||||
out="$(docker run --rm --network edge alpine:3.20 wget -S -q -O /dev/null http://scrabble/dict/scrabble_en/v1 2>&1 || true)"
|
||||
echo "$out" | grep -E "HTTP/" || true
|
||||
if echo "$out" | grep -q " 401"; then
|
||||
echo "ok: /dict reaches the gateway (401 unauthenticated)"
|
||||
else
|
||||
echo "FAIL: /dict did not reach the gateway (expected 401) — caddy route missing?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Probe the Telegram validator and bot liveness
|
||||
run: |
|
||||
set -u
|
||||
|
||||
Reference in New Issue
Block a user