scrabble-solver 2.18.3 → 2.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nx/workspace-data/d/server-process.json +3 -3
- package/.nx/workspace-data/file-map.json +1615 -1582
- package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
- package/.nx/workspace-data/lockfile-nodes.hash +1 -1
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/parsed-lock-file.dependencies.json +1091 -1156
- package/.nx/workspace-data/parsed-lock-file.nodes.json +2038 -2029
- package/.nx/workspace-data/project-graph.json +3290 -3338
- package/.nx/workspace-data/source-maps.json +44 -8
- package/.oxlintrc.json +0 -1
- package/CLAUDE.md +10 -8
- package/bun.lock +707 -424
- package/lerna.json +1 -1
- package/package.json +11 -11
- package/packages/configs/package.json +3 -3
- package/packages/constants/package.json +1 -1
- package/packages/dictionaries/bin/update-dictionaries.js +2 -5
- package/packages/dictionaries/build/Dictionaries.d.ts +1 -1
- package/packages/dictionaries/build/Dictionaries.js +9 -4
- package/packages/dictionaries/build/lib/DiskCache.js +1 -1
- package/packages/dictionaries/build/lib/downloadDictionary.d.ts +1 -1
- package/packages/dictionaries/build/lib/downloadDictionary.js +12 -7
- package/packages/dictionaries/package.json +5 -5
- package/packages/dictionaries/src/Dictionaries.ts +13 -6
- package/packages/dictionaries/src/lib/DiskCache.ts +2 -2
- package/packages/dictionaries/src/lib/downloadDictionary.ts +14 -6
- package/packages/logger/README.md +18 -0
- package/packages/logger/build/constants.d.ts +3 -0
- package/packages/logger/build/constants.js +4 -1
- package/packages/logger/build/describeError.d.ts +5 -0
- package/packages/logger/build/describeError.js +23 -0
- package/packages/logger/build/events.d.ts +92 -0
- package/packages/logger/build/events.js +13 -0
- package/packages/logger/build/formatCsvRow.d.ts +2 -0
- package/packages/logger/build/formatCsvRow.js +20 -0
- package/packages/logger/build/index.d.ts +5 -1
- package/packages/logger/build/index.js +13 -3
- package/packages/logger/build/logError.d.ts +4 -0
- package/packages/logger/build/logError.js +17 -0
- package/packages/logger/build/logEvent.d.ts +3 -0
- package/packages/logger/build/logEvent.js +29 -0
- package/packages/logger/package.json +7 -8
- package/packages/logger/scripts/export-csv.ts +113 -0
- package/packages/logger/scripts/readEvents.ts +34 -0
- package/packages/logger/scripts/tsconfig.json +10 -0
- package/packages/logger/src/constants.ts +9 -0
- package/packages/logger/src/describeError.test.ts +44 -0
- package/packages/logger/src/describeError.ts +30 -0
- package/packages/logger/src/events.ts +103 -0
- package/packages/logger/src/formatCsvRow.test.ts +41 -0
- package/packages/logger/src/formatCsvRow.ts +24 -0
- package/packages/logger/src/index.ts +13 -1
- package/packages/logger/src/logError.ts +24 -0
- package/packages/logger/src/logEvent.test.ts +59 -0
- package/packages/logger/src/logEvent.ts +26 -0
- package/packages/scrabble-solver/.next/BUILD_ID +1 -1
- package/packages/scrabble-solver/.next/build-manifest.json +19 -19
- package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
- package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
- package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
- package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +2 -2
- package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/packages/scrabble-solver/.next/diagnostics/framework.json +1 -1
- package/packages/scrabble-solver/.next/dynamic-css-manifest.json +1 -1
- package/packages/scrabble-solver/.next/next-minimal-server.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/next-server.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
- package/packages/scrabble-solver/.next/react-loadable-manifest.json +6 -6
- package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
- package/packages/scrabble-solver/.next/server/chunks/30.js +1 -1
- package/packages/scrabble-solver/.next/server/chunks/546.js +1 -1
- package/packages/scrabble-solver/.next/server/chunks/612.js +5 -0
- package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
- package/packages/scrabble-solver/.next/server/chunks/715.js +1 -0
- package/packages/scrabble-solver/.next/server/chunks/853.js +2 -2
- package/packages/scrabble-solver/.next/server/chunks/961.js +1 -1
- package/packages/scrabble-solver/.next/server/dynamic-css-manifest.js +1 -1
- package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
- package/packages/scrabble-solver/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/404.html +2 -2
- package/packages/scrabble-solver/.next/server/pages/500.html +2 -2
- package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/_document.js +2 -2
- package/packages/scrabble-solver/.next/server/pages/_document.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/_error.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/_error.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js +2 -2
- package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/verify.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/visit.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/visit.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.html +2 -2
- package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.meta +1 -0
- package/packages/scrabble-solver/.next/server/pages/not-found.html +2 -2
- package/packages/scrabble-solver/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.html +29 -0
- package/packages/scrabble-solver/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.json +1 -0
- package/packages/scrabble-solver/.next/server/route-cache/PAGES/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1/$/index.meta +1 -0
- package/packages/scrabble-solver/.next/static/{8WnIUTe-yJvPD6stltvYI → YHx536pklAqlc9UGAXCj8}/_buildManifest.js +1 -1
- package/packages/scrabble-solver/.next/static/chunks/101-fee295c7587c0836.js +3 -0
- package/packages/scrabble-solver/.next/static/chunks/{256.6a20b23a766cfebd.js → 256.2a4fa7f278e0a97d.js} +1 -1
- package/packages/scrabble-solver/.next/static/chunks/693.d295da7c7b873986.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/827.09f660ca0a951d12.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/980.dd35e807b18063c1.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/framework-988fd6f19d2744e8.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/main-d397eb8af33c5163.js +5 -0
- package/packages/scrabble-solver/.next/static/chunks/pages/_app-0c68f165d6de8507.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/pages/index-ea21d96a88f25000.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/{webpack-d7138f1ef6b8c9d6.js → webpack-31879a8fb6dcffd0.js} +1 -1
- package/packages/scrabble-solver/.next/static/css/{92f85ce358a0a468.css → 4dbcacc1122ac6b9.css} +1 -1
- package/packages/scrabble-solver/.next/static/css/8b8087461767360a.css +1 -0
- package/packages/scrabble-solver/.next/static/css/{65bcc9440e067113.css → 98051ed25076b896.css} +1 -1
- package/packages/scrabble-solver/.next/trace +22 -22
- package/packages/scrabble-solver/.next/trace-build +1 -1
- package/packages/scrabble-solver/package.json +18 -18
- package/packages/scrabble-solver/public/service-worker.js +1 -1
- package/packages/scrabble-solver/src/api/BadRequestError.ts +6 -0
- package/packages/scrabble-solver/src/api/index.ts +2 -1
- package/packages/scrabble-solver/src/api/withApiLog.ts +85 -0
- package/packages/scrabble-solver/src/components/Button/Button.module.scss +1 -1
- package/packages/scrabble-solver/src/components/Solver/Solver.module.scss +4 -4
- package/packages/scrabble-solver/src/components/Solver/Solver.tsx +2 -2
- package/packages/scrabble-solver/src/hooks/useColumns.ts +5 -7
- package/packages/scrabble-solver/src/modals/ResultsModal/ResultsModal.tsx +2 -2
- package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.module.scss +1 -1
- package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.tsx +2 -2
- package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordsTable.tsx +2 -2
- package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/[word].ts +22 -30
- package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/index.ts +36 -42
- package/packages/scrabble-solver/src/pages/api/solve.ts +32 -42
- package/packages/scrabble-solver/src/pages/api/verify.ts +35 -43
- package/packages/scrabble-solver/src/pages/api/visit.ts +38 -13
- package/packages/scrabble-solver/src/pages/index.tsx +2 -2
- package/packages/scrabble-solver/src/parameters/index.ts +0 -2
- package/packages/scrabble-solver/src/sdk/solve.ts +3 -9
- package/packages/scrabble-solver/src/sdk/verify.ts +2 -10
- package/packages/scrabble-solver/src/sdk/visit.ts +8 -3
- package/packages/scrabble-solver/src/state/sagas.ts +7 -1
- package/packages/scrabble-solver/src/styles/_tokens.scss +0 -2
- package/packages/scrabble-solver/src/styles/mixins.scss +0 -5
- package/packages/scrabble-solver/src/styles/variables.module.scss +0 -1
- package/packages/scrabble-solver/src/styles/variables.module.scss.d.ts +0 -1
- package/packages/scrabble-solver/src/styles/variables.scss +1 -1
- package/packages/scrabble-solver/src/types/api.ts +7 -0
- package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
- package/packages/solver/package.json +6 -6
- package/packages/types/package.json +2 -2
- package/packages/word-definitions/package.json +3 -3
- package/packages/word-lists/package.json +3 -3
- package/playwright-report/index.html +6 -6
- package/packages/gaddag/build/Gaddag.d.ts +0 -39
- package/packages/gaddag/build/Gaddag.js +0 -128
- package/packages/gaddag/build/buildGaddag.d.ts +0 -10
- package/packages/gaddag/build/buildGaddag.js +0 -360
- package/packages/gaddag/build/constants.d.ts +0 -12
- package/packages/gaddag/build/constants.js +0 -15
- package/packages/gaddag/build/index.d.ts +0 -3
- package/packages/gaddag/build/index.js +0 -13
- package/packages/logger/build/logger.d.ts +0 -1
- package/packages/logger/build/logger.js +0 -28
- package/packages/logger/scripts/stats.js +0 -73
- package/packages/logger/src/logger.ts +0 -30
- package/packages/scrabble-solver/.next/server/chunks/392.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/101-829ab8a8d310e7a0.js +0 -3
- package/packages/scrabble-solver/.next/static/chunks/693.677c88e0eb1340b5.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/827.c81b92b383a748c9.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/980.7ce17464fc56648a.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/framework-626cbd48ac5aca57.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/main-7e52da35a4beea0b.js +0 -5
- package/packages/scrabble-solver/.next/static/chunks/pages/_app-e3fe880e24c024ab.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/pages/index-b073a1686818dd26.js +0 -1
- package/packages/scrabble-solver/.next/static/css/a691348b788f8bc0.css +0 -1
- package/packages/scrabble-solver/src/api/getServerLoggingData.ts +0 -17
- package/packages/scrabble-solver/src/hooks/useIsCompactLayout.ts +0 -14
- /package/packages/scrabble-solver/.next/static/{8WnIUTe-yJvPD6stltvYI → YHx536pklAqlc9UGAXCj8}/_ssgManifest.js +0 -0
|
@@ -602,7 +602,11 @@
|
|
|
602
602
|
"packages/logger/package.json",
|
|
603
603
|
"nx/core/package-json"
|
|
604
604
|
],
|
|
605
|
-
"tags.npm:
|
|
605
|
+
"tags.npm:events": [
|
|
606
|
+
"packages/logger/package.json",
|
|
607
|
+
"nx/core/package-json"
|
|
608
|
+
],
|
|
609
|
+
"tags.npm:csv": [
|
|
606
610
|
"packages/logger/package.json",
|
|
607
611
|
"nx/core/package-json"
|
|
608
612
|
],
|
|
@@ -630,6 +634,10 @@
|
|
|
630
634
|
"packages/logger/package.json",
|
|
631
635
|
"nx/core/package-json"
|
|
632
636
|
],
|
|
637
|
+
"metadata.targetGroups.NPM Scripts.3": [
|
|
638
|
+
"packages/logger/package.json",
|
|
639
|
+
"nx/core/package-json"
|
|
640
|
+
],
|
|
633
641
|
"metadata.description": [
|
|
634
642
|
"packages/logger/package.json",
|
|
635
643
|
"nx/core/package-json"
|
|
@@ -706,31 +714,59 @@
|
|
|
706
714
|
"packages/logger/package.json",
|
|
707
715
|
"nx/core/package-json"
|
|
708
716
|
],
|
|
709
|
-
"targets.
|
|
717
|
+
"targets.export": [
|
|
718
|
+
"packages/logger/package.json",
|
|
719
|
+
"nx/core/package-json"
|
|
720
|
+
],
|
|
721
|
+
"targets.export.executor": [
|
|
710
722
|
"packages/logger/package.json",
|
|
711
723
|
"nx/core/package-json"
|
|
712
724
|
],
|
|
713
|
-
"targets.
|
|
725
|
+
"targets.export.metadata": [
|
|
714
726
|
"packages/logger/package.json",
|
|
715
727
|
"nx/core/package-json"
|
|
716
728
|
],
|
|
717
|
-
"targets.
|
|
729
|
+
"targets.export.options.script": [
|
|
718
730
|
"packages/logger/package.json",
|
|
719
731
|
"nx/core/package-json"
|
|
720
732
|
],
|
|
721
|
-
"targets.
|
|
733
|
+
"targets.export.options": [
|
|
722
734
|
"packages/logger/package.json",
|
|
723
735
|
"nx/core/package-json"
|
|
724
736
|
],
|
|
725
|
-
"targets.
|
|
737
|
+
"targets.export.metadata.scriptContent": [
|
|
726
738
|
"packages/logger/package.json",
|
|
727
739
|
"nx/core/package-json"
|
|
728
740
|
],
|
|
729
|
-
"targets.
|
|
741
|
+
"targets.export.metadata.runCommand": [
|
|
742
|
+
"packages/logger/package.json",
|
|
743
|
+
"nx/core/package-json"
|
|
744
|
+
],
|
|
745
|
+
"targets.test": [
|
|
746
|
+
"packages/logger/package.json",
|
|
747
|
+
"nx/core/package-json"
|
|
748
|
+
],
|
|
749
|
+
"targets.test.executor": [
|
|
730
750
|
"packages/logger/package.json",
|
|
731
751
|
"nx/core/package-json"
|
|
732
752
|
],
|
|
733
|
-
"targets.
|
|
753
|
+
"targets.test.metadata": [
|
|
754
|
+
"packages/logger/package.json",
|
|
755
|
+
"nx/core/package-json"
|
|
756
|
+
],
|
|
757
|
+
"targets.test.options.script": [
|
|
758
|
+
"packages/logger/package.json",
|
|
759
|
+
"nx/core/package-json"
|
|
760
|
+
],
|
|
761
|
+
"targets.test.options": [
|
|
762
|
+
"packages/logger/package.json",
|
|
763
|
+
"nx/core/package-json"
|
|
764
|
+
],
|
|
765
|
+
"targets.test.metadata.scriptContent": [
|
|
766
|
+
"packages/logger/package.json",
|
|
767
|
+
"nx/core/package-json"
|
|
768
|
+
],
|
|
769
|
+
"targets.test.metadata.runCommand": [
|
|
734
770
|
"packages/logger/package.json",
|
|
735
771
|
"nx/core/package-json"
|
|
736
772
|
]
|
package/.oxlintrc.json
CHANGED
package/CLAUDE.md
CHANGED
|
@@ -16,20 +16,20 @@ word-definitions ─────────────┘
|
|
|
16
16
|
logger (independent, used by app + dictionaries)
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The GADDAG itself lives outside this repo, in [`@kamilmielnik/gaddag`](https://github.com/kamilmielnik/gaddag) — a flat typed-array automaton with `has`/`hasPrefix`/`getArc`, binary `serialize`/`deserialize`, and `Gaddag.fromArray(words)`. It replaced `@kamilmielnik/trie` (#164) and is consumed by `dictionaries`, `solver`, and the app as a regular npm dependency (`^
|
|
19
|
+
The GADDAG itself lives outside this repo, in [`@kamilmielnik/gaddag`](https://github.com/kamilmielnik/gaddag) — a flat typed-array automaton with `has`/`hasPrefix`/`getArc`, binary `serialize`/`deserialize`, and `Gaddag.fromArray(words)`. It replaced `@kamilmielnik/trie` (#164) and is consumed by `dictionaries`, `solver`, and the app as a regular npm dependency (`^3.0.0`). A local `packages/gaddag/` directory may exist — it holds git-ignored compiled output left over from before the package was extracted and is **not** a workspace.
|
|
20
20
|
|
|
21
21
|
- `solver` — pure word-finding engine. Given a `Gaddag`, `Config`, `Board`, and `Tile[]`, returns scored `ResultJson`s. Has no I/O. `solve.ts` delegates to `MoveGenerator` (a single file holding all the logic) — anchor-based GADDAG move generation with per-cell cross-check masks, run once per direction; results are scored inline and sorted deterministically so UI ties resolve identically. `MoveGenerator` throws on alphabets over 64 tiles or boards over 32×32 because move sort keys pack direction/line/start/end into one integer. `benchmarks/` measures median `solve()` times on fixed mid-game boards (en-US, en-GB, pl-PL; 0–2 blanks): `bun run benchmark` from the root reruns them and rewrites the chart SVG + results table in the package `README.md`; it needs real (downloaded) dictionaries.
|
|
22
|
-
- `dictionaries` — downloads/caches per-locale word lists to `$HOME/.scrabble-solver/dictionaries` and exposes them as `Gaddag`s (binary `<locale>.gaddag` files on disk; building the Polish GADDAG from its 3.2M-word list takes ~25 s, so it happens in `postbuild`/background updates, not per request; the `update-dictionaries` / `remove-dictionaries` bin scripts wrap this, and `update()` refreshes only stale locales unless forced). The `Dictionaries` class layers a `MemoryCache` over a `DiskCache` (`LayeredCache`) and uses a per-locale `createAsyncProxy` to coalesce concurrent downloads. Cache entries older than `CACHE_STALE_THRESHOLD` (1 day) are considered stale, but only `update()` (run from `postbuild` and the packaging workflows) refreshes them — `get()` serves stale entries as-is. `DiskCache.get` treats files it cannot deserialize (corrupted, or written by an app version with an incompatible format — the binary magic doubles as the format version) as cache misses: it deletes them and the dictionary is re-downloaded and re-serialized automatically; `set` also removes the pre-#164 `<locale>.txt` trie cache. The solver worker does the same client-side (`src/solver-worker/getGaddag.ts`): it returns undefined for undeserializable cached dictionaries, falling back to the server and revalidating. Only this package and `logger` perform filesystem I/O — keep other packages pure so they can run in Edge / browser contexts.
|
|
22
|
+
- `dictionaries` — downloads/caches per-locale word lists to `$HOME/.scrabble-solver/dictionaries` and exposes them as `Gaddag`s (binary `<locale>.gaddag` files on disk; building the Polish GADDAG from its 3.2M-word list takes ~25 s, so it happens in `postbuild`/background updates, not per request; the `update-dictionaries` / `remove-dictionaries` bin scripts wrap this, and `update()` refreshes only stale locales unless forced, returning the locales it could not build — `update-dictionaries` exits 1 when there are any, so `bun run build` fails instead of shipping without a dictionary). The `Dictionaries` class layers a `MemoryCache` over a `DiskCache` (`LayeredCache`) and uses a per-locale `createAsyncProxy` to coalesce concurrent downloads. Cache entries older than `CACHE_STALE_THRESHOLD` (1 day) are considered stale, but only `update()` (run from `postbuild` and the packaging workflows) refreshes them — `get()` serves stale entries as-is. `DiskCache.get` treats files it cannot deserialize (corrupted, or written by an app version with an incompatible format — the binary magic doubles as the format version) as cache misses: it deletes them and the dictionary is re-downloaded and re-serialized automatically; `set` also removes the pre-#164 `<locale>.txt` trie cache. The solver worker does the same client-side (`src/solver-worker/getGaddag.ts`): it returns undefined for undeserializable cached dictionaries, falling back to the server and revalidating. Only this package and `logger` perform filesystem I/O — keep other packages pure so they can run in Edge / browser contexts.
|
|
23
23
|
- `word-lists` — pulls raw word lists from upstream sources (one fetcher per locale in `src/languages/`). Used by `dictionaries` during downloads.
|
|
24
24
|
- `word-definitions` — per-locale `crawl(word) → string` and a parser for each source (Wiktionary, CNRTL, DWDS, SJP, dexonline, vajehyab, etc.). English uses the Wiktionary REST API (#431): `parse` JSON-parses the payload and uses cheerio only to strip HTML from definition strings, so the English fixtures are `.json`. Add a new locale by adding a `crawl` and a `parse` function in `src/languages/` and wiring them into `crawl.ts` / `parse.ts`. `parse.test.ts` is where you add fixture-based parser tests.
|
|
25
25
|
- `types` — domain model classes (`Board`, `Cell`, `Tile`, `Result`, `Config`, `Locale`, …) plus `*Json` shapes. Most of these have a `fromJson` / `toJson` round-trip — use them at the wire boundary instead of hand-rolled serialization. Free functions live one-per-file under `src/lib/` (`getBoardWords`, `getCells`, `getCollidingWords`, `getCollisions`, `isSameBoardWord`, …) and type guards under `src/type-guards/` (`isBoardJson`, `isCellJson`, `isGame`, `isLocale`, …); the class methods are thin wrappers over them, so put new board/word logic in `lib/` and expose it as a method rather than growing `Board.ts`. `Board.getWords()` returns `BoardWord[]` (`{ direction, word, x, y }`), not strings — the same word can appear twice on a board, so a word's identity is its start cell plus direction (`isSameBoardWord`), and `Board.getCollidingWords(word)` returns the perpendicular words crossing it.
|
|
26
26
|
- `configs` — split into **games** (`scrabble`, `superScrabble`, `scrabbleDuel`, `letterLeague`, `crossplay`, `literaki`, `kelimelik`, `wyrazy` — each defines board size, bonuses, rack size, blanks count, bingo bonus) and **languages** (`english`, `french`, …, each spreads a base game config and overrides `locale` + `tiles`). A locale config is one game × one language; e.g. `polishScrabble` = `scrabble` ⊕ Polish tiles + digraphs. Adding a language means a new config here **and** the 15-step checklist under "Add a new language" in `README.md`.
|
|
27
|
-
- `logger` —
|
|
27
|
+
- `logger` — dependency-free event log. `logEvent()` appends one flat JSON object per line to `$HOME/.scrabble-solver/logs/events.txt`: `timestamp`, `type`, then the fields declared for that type in `src/events.ts` — that declaration is the schema of both the file and the CSV export, so fields are append-only and never reordered. `logError()` writes an `error` event — level `error` by default, `warn` when its context says so (expected-and-handled failures like a corrupt dictionary cache) — and mirrors `error`-level ones to stderr (journald on prod); nothing else reaches the console except a line when the events file itself cannot be appended. Silent under `bun test`. `bun run --filter @scrabble-solver/logger export [YYYY]` splits the file into `$HOME/.scrabble-solver/csv/<type>-YYYY.csv` for Metabase. Used server-side by the app + dictionaries; do **not** import from browser code.
|
|
28
28
|
- `constants` — shared primitives (`BLANK`, `BONUS_CHARACTER`, `BONUS_WORD`, …). No runtime dependencies.
|
|
29
29
|
|
|
30
30
|
### App package (`@scrabble-solver/scrabble-solver`)
|
|
31
31
|
|
|
32
|
-
- **Routing**: Next.js Pages Router (`src/pages/`). API routes: `solve`, `verify`, `visit`, `dictionary/[locale]` (binary GADDAG download), and `dictionary/[locale]/[word]` (definitions). The path alias `@/*` resolves to `src/*` (set in `tsconfig.json`).
|
|
32
|
+
- **Routing**: Next.js Pages Router (`src/pages/`). API routes: `solve`, `verify`, `visit`, `dictionary/[locale]` (binary GADDAG download), and `dictionary/[locale]/[word]` (definitions). Every route is `export default withApiLog('<operation>', handler)` (`src/api/withApiLog.ts`): the wrapper resolves the client IP and the User-Agent (capped, since it lands in a never-rotated file), times the request, and on a throw logs an `error` event with a request excerpt and then answers unless the handler had already responded — a `BadRequestError` (what each route's `parseRequest` throws for invalid input) gets 400 and a stackless `warn`-level event, anything else gets 500 and an `error`-level event mirrored to stderr — so a handler holds only the happy path and ends with its own `logEvent(...)` after `send()`. `visit` is a `PUT` whose optional JSON body carries `referrer`/`url` (the landing page, so `?ref=`/`?utm_source=` attribution survives)/`locale`/`game` from the client. The path alias `@/*` resolves to `src/*` (set in `tsconfig.json`).
|
|
33
33
|
- **State**: Redux Toolkit + Redux-Saga. Slices in `src/state/{app,board,cellFilters,dictionary,hoveredTile,hoveredWord,i18n,rack,results,settings,solve,verify}`, each exporting `<name>Slice` (reducer + actions) and selectors. The root saga in `state/sagas.ts` reacts to slice actions: `submit` → call SDK → write results back. `solve`, `verify`, and `dictionary` use `takeLatest` (only the latest in-flight request resolves); cell/rack edits use `takeEvery`. State is intentionally **not** serializable-checked (`serializableCheck: false`) because slices hold class instances (`Board`, `Tile`). `initialize({ version })` carries the app version (from `getStaticProps`) into the `app` slice; the translations cache is keyed on it. Two slices drive board highlighting and they are mutually exclusive: `results.candidate` (a solver result being previewed) and `hoveredWord` (a created word hovered or selected in the words table) — each one's saga clears the other, and both funnel into the same `searchDictionary` helper in `state/sagas.ts` that fills the dictionary panel. The `verify` slice mirrors `results`: alongside `validWords`/`invalidWords` (both `VerifiedWord[]`) it owns the words table's own `query` and `sort`, toggled through the shared `lib/getNextSort`.
|
|
34
34
|
- **Board render budget**: `Cell`/`Tile` render 225+ times at once and must not subscribe to the store — every value they need flows down from `BoardPure` as memo-friendly props, and event handlers read state at event time via `useTypedStore().getState()` (see `Cell.tsx`). Anything added to a cell that subscribes via `useSelector` multiplies by 225 and shows up directly in TBT. A cell's `highlighted` flag is the OR of every highlight source (result candidate, hovered rack/remaining tile character, hovered created word) and is computed in `BoardPure`, so a new highlight means a new `boolean[][]` prop selector — never a lookup inside `Cell`.
|
|
35
35
|
- **Tables**: `src/components/Table/` holds the shared table chrome — `Header`, `HeaderButton` (generic over the column-id enum; sort state and `onSort` arrive as props), `Row`, `Cell`, and `Search` (the controlled RegExp filter input, formerly `ResultsInput`). Two tables use it: `components/Results` (solver results) and `modals/WordsModal/components/WordsTable` (created words). The kit owns chrome only — column widths stay in each table's own `*.module.scss`, and the single column that absorbs the leftover width is marked with the `primary` prop rather than a class. `Row` derives `cursor: pointer` from whether it was given an `onClick`, so a row never advertises a click it will ignore; that is also why `Result` passes handlers through conditionally instead of defaulting them to `noop`. Both tables read as one behaviour matrix over layout × input, and the two axes mean different things: the **layout** decides where the footer buttons live (compact shows *Preview*, plus *Insert* for results; desktop shows neither), while **touch** decides what a repeated click on the already-picked row does (previews it, since a touch user has no other way to get back to the board). Between them sits the highlight's lifetime, and both tables answer it identically: only on desktop non-touch does the pointer own it (hover and focus pick, leaving the list clears, clicking adds nothing) — everywhere else it is sticky, so clicking a row picks it and hovering one does nothing at all. `Table`'s `.row` therefore highlights on `:focus-visible`, never `:focus`: a row left focused by a click must stop looking picked the moment another row takes over. That also means a row's picked state has to come from an explicit `highlighted` prop (`highlightedIndex`), never from focus — the results sidebar passes `selectResultCandidateIndex` for exactly that reason. Both tables virtualise with `react-window` and keep their sort/filter state in their own slice (`state/results/*`, `state/verify/*`); each slice's `lib.ts` owns a `Record<ColumnId, ComparatorFactory>` that the shared `lib/createSortComparator` collapses into one comparator (`lib/getNextSort` toggles the direction, `lib/createCoordinatesComparator` + `lib/getCoordinates` cover the coordinates column). Neither table filters rows out — `groupResults`/`groupWords` float the matches to the top and render the rest dimmed and `aria-hidden`, so the row count never depends on the query. Only the words table has an empty case: with no created words it swaps the list for an `EmptyState` and hides the `Search` input.
|
|
@@ -37,7 +37,7 @@ The GADDAG itself lives outside this repo, in [`@kamilmielnik/gaddag`](https://g
|
|
|
37
37
|
- **Persistence**: settings, board, and rack auto-persist to `localStorage` via `store2` under the `scrabble-solver` namespace. The `useLocalStorage` hook (mounted in `pages/index.tsx`) subscribes to the three slices and writes them out on every change (skipping each effect's first post-hydration run, which would only echo what was just read) — **adding a new field to `SettingsState` is enough; you don't need to touch any save code** (PR #321, Apr 2026). Boot state is deterministic (SSR/hydration must match — issue #412): persisted settings/board/rack are applied post-mount by `hydratePersistedState` in the `initialize()` saga, guarded so a throw leaves the deterministic defaults and `app.hydrated` still fires (`finally`) — otherwise persistence would silently die for the session. The `state/localStorage.ts` getters treat corrupt/mismatched entries as absent and remove them; the saga skips no-op `init` dispatches so an empty-storage boot changes no state identities and re-renders nothing. The active locale's translations are also cached (`translations` key, keyed by app version + locale) and hydrated synchronously so a returning user's first post-hydration paint is already translated; the entry is also discarded when it is missing any key of the statically bundled English object (`hasEveryTranslation`), so a new `TranslationKey` shipped without a version bump can't strand a returning user on a stale cache (#452). When changing settings shape, add a migration block (see `migrateLegacySettings` for the pattern — dated comment with introduction date and life expectancy).
|
|
38
38
|
- **Service worker**: built by `WorkboxPlugin.InjectManifest` from `src/service-worker/index.ts` to `public/service-worker.js`. It precaches the app shell and routes navigations to it — solving and verifying offline is the `src/solver-worker/` web worker's job, not the service worker's. Only generated in production builds (`!isServer && !dev`). The precache excludes `.css` — every page inlines its stylesheets (see `_document.tsx`), so the emitted CSS files are never requested.
|
|
39
39
|
- **i18n**: `src/i18n/languages/<lang>.json` (8 languages, mapped to `Locale` in `i18n.ts`). English is statically bundled; every other locale is a dynamic-import chunk (`loadTranslations`), loaded into the `i18n` slice — `selectTranslations` falls back to the complete English object, so missing keys are impossible mid-load. All locales preload on idle/first intent (`preloadTranslationsWhenIdle`), which is what makes switching languages apply instantly; the active locale is additionally cached in localStorage (see Persistence). The `LOCALE_FEATURES` registry in `src/i18n/constants.ts` carries per-locale UI metadata: `direction` ('ltr' | 'rtl'), `comma`/`separator` glyphs (Latin vs Arabic), language `label`/`name`, and `consonants`/`vowels` flags that drive the auto-group-tiles UI; flag icons live separately in `src/i18n/localeIcons.ts` (`LOCALE_ICONS`) so they stay out of the main bundle. The `useDirection` hook applies `direction` to `<html dir>`. Add a new locale by extending both maps plus the i18n JSON dictionary plus a `Flag<XX>.svg` icon. `useTranslate()` is the lookup hook.
|
|
40
|
-
- **Layout**: all layout sizing lives in CSS custom properties (`src/styles/variables.scss`: `--cell-size`, `--rack-tile-size`, `--max-board-width`, …), with the config-dependent inputs (`--board-cols`, `--board-rows`, `--rack-size`) set on `:root` twice: before first paint by an inline script in `_document.tsx` (which reads persisted settings, also sets `dir`/`lang`, and adds the `config-pending` class that hides `main` until hydration applies a persisted non-default game — see `CONFIG_PENDING_CLASS`), and post-hydration by an effect in `pages/index.tsx`. There is no JS layout hook — components size themselves via these variables (the board grid templates use `repeat(var(--board-cols), …)` so the held board has its final geometry pre-hydration); JS reads viewport state only through `useMediaQuery`. The compact/desktop split is `
|
|
40
|
+
- **Layout**: all layout sizing lives in CSS custom properties (`src/styles/variables.scss`: `--cell-size`, `--rack-tile-size`, `--max-board-width`, …), with the config-dependent inputs (`--board-cols`, `--board-rows`, `--rack-size`) set on `:root` twice: before first paint by an inline script in `_document.tsx` (which reads persisted settings, also sets `dir`/`lang`, and adds the `config-pending` class that hides `main` until hydration applies a persisted non-default game — see `CONFIG_PENDING_CLASS`), and post-hydration by an effect in `pages/index.tsx`. There is no JS layout hook — components size themselves via these variables (the board grid templates use `repeat(var(--board-cols), …)` so the held board has its final geometry pre-hydration); JS reads viewport state only through `useMediaQuery`. The compact/desktop split is `useMediaQuery('<l')` (1200px) — `showResultsInModal`, `showCompactControls`, `isCompactLayout` and `selectsFirstWord` are all that same query; SCSS that has to agree with it must use `>=l`, never `>l`, or the boundary pixel lands on the wrong side of the split. `Modal`'s footer is not size-gated at all — a modal shows one by passing `footer`, and it's up to that modal to decide when (`WordsModal` renders its *Preview* button only below the breakpoint, `ResultsModal` closes itself above it). **Boot-path DOM rules**: never read computed style in mount effects (forced reflow — compare against inline style or known defaults instead), and guard same-value writes to `<html>` attributes/variables (they invalidate style/paint document-wide and destabilize LCP). The active modal in `pages/index.tsx` is tracked as a `Record<Modal, boolean>` patched through a single `patchModals` callback; each modal mounts on first open and stays mounted (`mountedModals` latch), because a `next/dynamic({ssr: false})` component starts its chunk request during render.
|
|
41
41
|
- **Styling**: SCSS modules with a shared design-token system. When one module's class overrides another component's styles (e.g. `Actions` restyling `Button`), it must win on **specificity**, not stylesheet order — webpack's CSS emission order shifts with the import graph (this flipped once and widened a square button). Class names are type-checked (#16): `scripts/generate-scss-types.ts` writes a git-ignored sibling `.d.ts` for every `*.module.scss` — the `build` and `type-check` scripts run it up front, `dev` runs it in watch mode, and its sass options mirror `sassOptions` in `next.config.js`. `src/@types/scss.d.ts` only covers side-effect imports of non-module stylesheets, deliberately exporting nothing so an import of an ungenerated module fails the type-check. SCSS tokens live in `src/styles/_tokens.scss`; the same values are re-exported to TS via `:export` in `variables.module.scss`. Concrete JS constants live in `src/parameters/index.ts` (e.g. `BREAKPOINTS`, `COLOR_BLUE`, `TRANSITION_DURATION`) — that file is the only place that should read from `variables.module.scss`. Update `_tokens.scss` first → expose via `variables.module.scss` `:export` block → consume via `parameters/`. This was added in PR #228 (Apr 2026); before it, JS-side colors and breakpoints were hard-coded duplicates. Responsive helpers come from `include-media`.
|
|
42
42
|
- **SVGs**: imported as React components via `@svgr/webpack` (configured in `next.config.js`), typed by `src/@types/svg.d.ts`.
|
|
43
43
|
- **Service worker registration**: production-only. Registered by `serviceWorkerManager.ts` from the index page, deferred until load + idle (`waitForIdle`) so the install's precache downloads never compete with page resources (an eager install once caused Lighthouse's robots.txt fetch to time out). Playwright blocks registration entirely via `serviceWorkers: 'block'` in `playwright.config.ts`, and each test gets a fresh browser context, so e2e tests need no service-worker or localStorage cleanup.
|
|
@@ -71,7 +71,7 @@ Hot reload only works for the `scrabble-solver` package. Edits to any other pack
|
|
|
71
71
|
## Testing notes
|
|
72
72
|
|
|
73
73
|
- Unit tests run on **Bun's test runner**, not Jest. The API is Jest-compatible (`describe`/`it`/`expect`), which is why the oxlint config still loads the `jest` plugin for rules like `no-focused-tests`.
|
|
74
|
-
- Only `dictionaries`, `solver`, `word-definitions`, and `scrabble-solver` have a `test` script. Tests are auto-discovered under `src/` matching `*.test.ts(x)`. The 180s timeout is needed because some solver tests build a real `Gaddag` from a downloaded dictionary.
|
|
74
|
+
- Only `dictionaries`, `logger`, `solver`, `word-definitions`, and `scrabble-solver` have a `test` script. Tests are auto-discovered under `src/` matching `*.test.ts(x)`. The 180s timeout is needed because some solver tests build a real `Gaddag` from a downloaded dictionary.
|
|
75
75
|
- `bunfig.toml` + `bun.test.preload.ts` register a SCSS loader stub (returns a `Proxy` whose keys are their own names) so component tests can import `*.scss` modules without a real compiler. If you add other non-JS imports to test-touched code (images, etc.), extend the preload.
|
|
76
76
|
- Each package's `tsconfig.json` excludes `**/*.test.ts` from the build output. Tests are not part of published packages.
|
|
77
77
|
- Playwright: specs in `e2e/` (`app.spec.ts`, `bugs/`, `features/`), shared page helpers in `e2e/lib/` (selectors return `Locator`s, actions take `page` first). Two base URLs are in play: `playwright.config.ts` defaults to `http://localhost:3000` (matches `bun run dev`); `test-playwright:run` and CI set `PLAYWRIGHT_BASE_URL=http://localhost:3333` (matches `bun start`). Pick the script that matches the server you're actually running. The config blocks service workers and runs Chromium only; browser binaries come from `bunx playwright install chromium`.
|
|
@@ -81,7 +81,7 @@ Hot reload only works for the `scrabble-solver` package. Edits to any other pack
|
|
|
81
81
|
- **Linting**: `oxlint` (Rust-based ESLint replacement) configured in `.oxlintrc.json`. Type-aware rules require `oxlint-tsgolint`. Adding a new top-level JS config file usually means adding it to `ignorePatterns`. The oxlint config still loads the `jest` plugin and `jest` global because Bun's test runner mirrors the Jest API; do not remove them.
|
|
82
82
|
- **Formatting**: `oxfmt` covers `*.{js,ts,tsx,scss}`.
|
|
83
83
|
- **Install layout**: `bunfig.toml` pins `[install] linker = "hoisted"`. With Bun's isolated layout, `next build` fails — webpack bundles `unzipper` from `word-lists` and errors on its uninstalled optional `@aws-sdk/client-s3` dependency. Don't remove it.
|
|
84
|
-
- **TypeScript**: stable TypeScript 7 (`typescript@^7.0.2`), whose `tsc` is the native Go compiler. It runs every package `build`, the app's `type-check`, and `next build` — the latter because Next.js runs the project-local `tsc` CLI (TS7 has no JS compiler API). Next 16.3.0 turned that on by default, so the `experimental.useTypeScriptCli` flag that pinned the app to the 16.3 preview line is gone from `next.config.js`; set it to `false` only to opt back into the JS compiler API, which TS7 cannot serve. The interim `@typescript/native-preview` (`tsgo`) setup from PR #422 was removed (Aug 2026). Root `tsconfig.json` sets `types: ["bun"]` for global test-runner types and excludes `e2e` and `playwright.config.ts` (covered by `e2e/tsconfig.json` instead); library packages extend it and additionally exclude `**/*.test.ts` from emitted output.
|
|
84
|
+
- **TypeScript**: stable TypeScript 7 (`typescript@^7.0.2`), whose `tsc` is the native Go compiler. It runs every package `build`, the app's `type-check`, and `next build` — the latter because Next.js runs the project-local `tsc` CLI (TS7 has no JS compiler API). Next 16.3.0 turned that on by default, so the `experimental.useTypeScriptCli` flag that pinned the app to the 16.3 preview line is gone from `next.config.js`; set it to `false` only to opt back into the JS compiler API, which TS7 cannot serve. The interim `@typescript/native-preview` (`tsgo`) setup from PR #422 was removed (Aug 2026). Root `tsconfig.json` sets `types: ["bun"]` for global test-runner types and excludes `e2e` and `playwright.config.ts` (covered by `e2e/tsconfig.json` instead); library packages extend it and additionally exclude `**/*.test.ts` from emitted output. A package's `tsconfig.json` covers `src/` only, so TypeScript living outside it needs its own `noEmit` config and its own `tsc` invocation or nothing type-checks it — `packages/logger/scripts/tsconfig.json` is the pattern (`module: preserve` because the scripts are ESM run by bun, while the package itself emits CJS), invoked from the package `build` so CI catches it. `packages/solver/benchmarks/` is still uncovered.
|
|
85
85
|
- **Next.js**: built with `--webpack` flag explicitly (the default Turbopack is intentionally not used). `next.config.js` registers `@svgr/webpack` for SVG-as-component imports and the Workbox `InjectManifest` plugin for the service worker, and stubs out Next's unconditionally-bundled ES2019-2022 polyfills (`polyfill-module`) with `empty-module.js` via `NormalModuleReplacementPlugin` — every browserslist target (`package.json` `browserslist`: Chrome/Edge/Firefox ≥ 100, Safari ≥ 15.4, …) supports those natively, and browserslist alone does not control that chunk. SCSS load paths are extended to `./src` and `node_modules/include-media/dist`.
|
|
86
86
|
- **Nx**: `nx.json` only defines a `build` target with `dependsOn: ["^build"]` and `cache: true`. It is used purely for dependency-aware build ordering and caching — there are no Nx generators or executors.
|
|
87
87
|
|
|
@@ -111,7 +111,8 @@ The `Deploy` GitHub workflow (`workflow_dispatch` only) SSHs into the production
|
|
|
111
111
|
The app reads/writes user data outside the project directory:
|
|
112
112
|
|
|
113
113
|
- `$HOME/.scrabble-solver/dictionaries/` — cached serialized `Gaddag`s (binary), one per locale, refreshed when older than 1 day.
|
|
114
|
-
- `$HOME/.scrabble-solver/logs/
|
|
114
|
+
- `$HOME/.scrabble-solver/logs/events.txt` — the JSON-lines event log (append-only, never rotated). Installs from before the event log (Aug 2026) may still hold winston's `all.log`/`error.log` next to it; nothing reads or writes them anymore.
|
|
115
|
+
- `$HOME/.scrabble-solver/csv/` — per-type yearly CSV exports written by `bun run --filter @scrabble-solver/logger export`.
|
|
115
116
|
|
|
116
117
|
The `bunx scrabble-solver@latest` entry point (`bin/scrabble-solver.js`) just `cd`s to the package root and runs `bun start`. The app then serves on http://localhost:3333.
|
|
117
118
|
|
|
@@ -119,6 +120,7 @@ The `bunx scrabble-solver@latest` entry point (`bin/scrabble-solver.js`) just `c
|
|
|
119
120
|
|
|
120
121
|
Look here when something seems set up oddly — the reason is usually one of these recent changes. Reference issue numbers, not dates, when grepping git log.
|
|
121
122
|
|
|
123
|
+
- **Event log** (Aug 2026). `packages/logger` dropped winston (whose `prettyPrint` had been writing unparseable multi-line text with no rotation — 500 MB in 4 months on prod) for a typed JSON-lines event log plus a CSV export for Metabase. Request logging moved out of the routes into `withApiLog`; entries record outcomes (`ms`, result counts, 200/304, bytes) instead of raw request headers, the `visit` ping gained a body, and the old `all.log`/`error.log` are simply left behind.
|
|
122
124
|
- **#450 — Created-words table** (Aug 2026, issues #179/#427/#452). The words modal became a virtualised, sortable, RegExp-filterable table that shares `src/components/Table/` with the results table (`ResultsInput` moved there as `Search`). Picking a row — by hover where the pointer owns the highlight, by click everywhere else — highlights that word on the board and looks it up in the dictionary together with the words it crosses. Below the `l` breakpoint the first word is selected automatically (the modal covers the board, so the in-modal dictionary is the payoff) and the footer offers *Preview*, the only exit from the modal that leaves the highlight standing — every other way out clears it. On touch, re-tapping the already-picked row previews it too. The compact-layout result-candidate picker stopped going dead while nothing is picked: it stays enabled whenever fresh results exist and shows a `results.select` placeholder, so *next* picks the first result. `Board.getWords()` returns `BoardWord[]` instead of `string[]`, `/api/verify` and the solver worker answer with `VerifiedWord[]`, and the `types` package's free functions and type guards moved into `src/lib/` and `src/type-guards/` (`readResultCells.ts` → `lib/getCells.ts` + `lib/getCollisions.ts`).
|
|
123
125
|
- **#447 — Lighthouse 100** (Aug 2026, issue #412). The index page is fully server-rendered (the old `isClient` client-only gate is gone), so everything at boot must be SSR/hydration-safe and deterministic. The JS layout layer (`useAppLayoutValue`, `AppLayoutContext`) was deleted in favor of CSS variables; stylesheets are inlined into the HTML by `InlineCssHead` in `_document.tsx` (the emitted `.css` files exist but nothing requests them); a pre-paint inline script in `_document.tsx` applies persisted board dimensions/direction and holds `main` hidden until hydration when the persisted game differs from the default. Board cells are store-subscription-free (see "Board render budget"), modals mount on first open, tooltips/service-worker/dictionary/translations work is deferred past load or idle, and per-locale i18n chunks replaced the bundled all-locales map (with an idle preload and a version-keyed localStorage cache for instant/flash-free language behavior). `transliteration` was replaced with Unicode normalization, all barrel files were removed for tree-shaking, and Next's bundled polyfills are stubbed out. Perf invariants to preserve: no store subscriptions in `Cell`/`Tile`, no computed-style reads or same-value `<html>` writes in boot effects, nothing heavy before load+idle, and hydration must not change state identities on an empty-storage boot.
|
|
124
126
|
- **#164 — GADDAG solver** (Aug 2026). `@kamilmielnik/trie` was dropped everywhere in favor of the external `@kamilmielnik/gaddag` package; `solve()` was rewritten as anchor-based GADDAG move generation (~40-60× faster). Disk-cached dictionaries changed from serialized-trie `.txt` to binary `.gaddag` files, and `/api/dictionary/[locale]` now serves `application/octet-stream`, gzipped when the client accepts it (compressed once per in-memory dictionary via a `WeakMap` cache — the route parses `Accept-Encoding` q-values so `gzip;q=0` gets identity and `*` counts as accepting, evicts failed compression promises from the cache, and sets `Vary: Accept-Encoding`). Responses are `Cache-Control: no-cache`, so clients revalidate with `If-None-Match` and unchanged re-downloads become 304s — the `ETag` comes from Next's built-in per-payload generation (it overrides any hand-set `ETag` on `send()`, so don't set one). The solver worker deserializes with `Gaddag.deserialize` (the browser undoes `Content-Encoding` before the Cache API stores the body) and memoizes the deserialized `Gaddag` per locale, keyed on the cached response's `ETag`/`Date`/`Content-Length`, until `revalidateDictionary` replaces the entry.
|