part 28
A move that must change nothing: Kastgard into its own folder
On 31 October 2026, ninety-one game files moved into js/realms/kastgard/ and the player noticed nothing. What came before (tests, tools, the table of packages), what move.js does not rewrite and what moved only on paper.
On 31 October 2026 at 3:46 the move of Kastgard was merged into the game (version 0.64.5). Ninety-one game files, which until then lay in js/core/, js/data/, js/ui/ and js/worker/, have had their place in js/realms/kastgard/ since then. The player noticed nothing, and that was the requirement: no stored key, no ID, no rule and no value in the golden records changed. The move itself turned out to be short (five commits), but it stood on preparation from 28 October: the tests and the tools were sorted by packages, the tools stopped assembling paths by hand, and the table of packages learned the relation "a game builds on a game". After the move the whole suite has 3,268 tests (3,267 passed, one is skipped), and the list of 120 breaches of the direction of dependencies is the same as before it.
Why the move, and what must not change in it
According to the note in docs/KASTGARD-GAMES.md (part 2.1), my model of 30 October 2026 is this: Kastgard is a realm with two games. The first, stacks, is a game with dice and territories with free play and a configurator; the Principalities[1] mode is only a preset of that configurator. The second, campaign, builds on stacks. The minigames and Puzzle Labs are not games, but content that the games register. The article From one game to more games describes the physical move of the files (step M9) as git mv; this record shows what was "only" hidden under it.
The move concerns only paths. No stored ID or key changed, nor Record.RULES_VERSION (2) or Rules.GEN_VERSION (1). The folder is called kastgard, but the runtime ID of the realm stayed stacks; the table of packages writes it as realms: { "stacks": "kastgard" }. That is why the move is proved by comparing the old and the new output, not by a new test of the rules. That was so on 31 October 2026; since 6 November 2026 the ID of the realm is kastgard, and the table of packages no longer needs this entry (the story is in the article How Audamir is put together).
First the tests and the tools
According to DESIGN §72, on 29 October 2026 I asked for tests/ and tools/ to be sorted the way js/ is sorted, and for a change in one realm or game not to run the tests of all the others. It is a separate goal (a shorter cycle of edits), but its side effect is that the layout of the tests and the tools mirrors js/, so moving a package is then a renaming of folder prefixes. All five steps were merged on 30 October 2026:
| Step | Merged | What | Numbers |
|---|---|---|---|
| T1a (§72) | 01:20 | tests into the folder of the package | 194 moves, 1,607 references in 246 files, the names of all 2,793 tests the same |
| T1b (§73) | 03:47 | the runner tools/test.js --changed | a change of the rules of Colony: 24 of 186 files, 49 s of 296 s |
| T2 (§74) | 07:16 | tools into tools/<package>/ | 79 files |
| T1c (§75) | 08:42 | tests mirror the folders of js/ | 127 files, the test of Colony split into four |
| T3 (§76) | 11:04 | tests of the tools and fixtures | tools.test.js (35 tests, 157 s) into 27 files (38 s), 17 fixtures |
The runner is a shortcut for the work in between; **npm test remains the gate** before a merge. Two things from these steps later turned out to matter for Kastgard too. At the first mutation of Colony the runner skipped one test, because the tool assembled the path from a variable and nowhere stood a file name (§73.4): the same blind spot that move.js later ran into. And a fake DOM draws the seeds of the games from the name of the test file (§72.3), so a renamed test would play different games; the layout guard therefore checks that the names are unique.
The table of packages and move.js
The base has stood since 28 October 2026 (DESIGN §60, merged at 19:49, version 0.51.2). The tool tools/move.js moves files with git mv (so that git blame passes through the renaming) and rewrites the references: a path written in full, require, importScripts, the <script> tag and the item in sw.js; a relative path it evaluates from the place where the file was, and writes from the place where it is. Then it checks what it did: no old path remains, no working relative path points nowhere, index.html, sw.js and the workers name existing files, and the scripts are in an order in which each UMD wrapper finds its globals. The table tools/packages-table.json writes each file of js/ into exactly one package, and tools/packages.js checks the direction of dependencies. The pilot was Colony: 9 files, 126 references in 31 files.
The pilot found a mistake that no test was looking for (§60.5). js/i18n/boot.js named the language packages for the Web Worker with a fixed ../../. From js/realms/colony/worker/ this path leads to no file, importScripts throws an error, the worker dies and the page quietly returns to the main thread: a slower game, about which nothing reports. The worker now reads its place from its own address. For Kastgard boot.js needed no change; only a test for a worker six folders deep was added.
The tools do not look for a file by its path
The move would have broken the tools that assembled a path from constants. DESIGN §94 (merged 31 October at 1:29) counted them: 74 tools name about 640 files of js/. A trial run of move.js --dry-run --package stacks printed 80 lines "check by hand", of which 64 were in 20 tools.
The solution is the helper tools/paths.js: a tool asks for a file by its key, and the table tells it where it lies. The key is the path under the folder of the package, core/rules.js for js/core/rules.js and also for js/realms/kastgard/games/stacks/core/rules.js. The layer belongs to the key, because **14 basic names in js/ are not unique** (campaign.js lies in core/, data/ and ui/). The helper is strict: an unknown key, an ambiguous name, and a file that the table leads to and the disk does not have, are errors. The same functions can work over another tree (Paths.at(root)), for example over the git archive of an old commit when proving migrations.
The layout guard of the tools and the runner of the tests found out what a tool uses from the string js/… in its source; a tool with Paths.require('core/rules.js') has no such path, and both would have gone blind, so they learned to read the calls of Paths too. Proof: 74 tools were run on the old and the new code with the same arguments; of 111 compared outputs and files, 103 were the same (89 byte for byte), and 8 differed for a reason that was read in each case (five carry time, another a temporary file's name, a commit abbreviation or a relative path). In the trial on a copy of the tree, where stacks and campaign were moved by the real move.js, not a single line of a tool had to be fixed.
The order of steps K1 to K6 and why
The move did not go straight. Each step had to stay verifiable on its own, so first what the paths are computed by was changed, and only then the files themselves:
- K3: the tools got the helper
tools/paths.jsand no longer write paths by hand. Without itmove.jswould have left 109 assembled paths in 25 files. - K1 and K2: the table of packages learned that a game may build on another game (
buildsOn), and the campaign stopped reading the manifest of its own realm. Without K1 the check of the direction would have shown 122 edges. - K4 and K5: the tests and the tools moved into folders that mirror
js/, and 91 files were classified into the table. The table refuses a game whose test folders do not mirror its folder injs/. - K6: only now was the code of
js/itself moved, bymove.js --packagefor each package.CACHEinsw.jswas raised once (v72 → v73).
The plan was tried before the merge on a copy of the repository (git archive, the real move.js), and after each step three guards were run. Seven faults of the plan were found. The most important: formatTable forgot a field it did not know (buildsOn); a table written this way would have turned ten edges from the campaign to the game into breaches. The move also needed a quiet window: during K4 to K6 no other change could touch these files, index.html, sw.js or the table.
What move.js does not rewrite
move.js rewrites paths that are written in full: a path in require and importScripts, the <script> tags and the items in sw.js. It does not see paths that are assembled from parts, URLs, templates or the number of ... This is how these were found:
- **Number of
..:** 20 tests get the root of the repository bypath.join(__dirname, '..', '..', '..'). After the move six..are needed. - An address instead of a path:
tools/smoke.jsasked for/js/worker/gen-worker.js. - Depth of a worker: two tests of the worker of the analysis looked for
importScriptsin a fixedjs/worker/. - File name assembled in a test: seven tests now ask for the file by a key of
tools/paths.js; five read a fixed path and failed in the first run. - Toy tree: the tool in
test-runner.test.jsand in parts of two other tests rewrote a name where it is a real file, and elsewhere it did not. The change was reverted. - Wrong guess of the kind of change:
tools/version.jsknew a new screen only as a file injs/ui/; without a fix, a new screen of the game would count as a "fix". - The helper itself:
Paths.dirOfbetween K4 and K6 reported the folder of a package whose files still lay injs/. Fixed. - Small things: the header
Used by:inboard-events.jsand empty folders after the move.
Intentionally, the documents were not changed: they kept the old paths. The same goes for the catalogues in js/i18n/ and for the older articles of the blog, for example From one game to more games. The conversion of an old path into a new one is in docs/KASTGARD-MOVED.md.
The direction of dependencies and the ratchet
The rule (DESIGN §95.2): **a package may use core and itself; a realm may use its games; a game may use the games it builds on (buildsOn); nothing else.** Campaign builds on Stacks, so campaign → stacks is a lawful edge (after K1 there are 10 of them: K1 added one, because section-campaign.js now names ruleset-stacks.js). The game, however, may not use its own realm, and therefore K1 cut two edges into the manifest and the table of modes.
The list of breaches, which may only shrink (in the tools called ratchet, in Slovak západka), did not change in the move. This is what node tools/packages.js --check of 31 October 2026 looks like:
The numbers 120 and 22 are the same before the move and after it: the move added no breach and fixed none. Of them, 112 edges (core into Kastgard: 65 + 45 + 2) are the work of step M8b, that is the division of the mixed files of the platform. Four edges from stacks to campaign form a real cycle: legend.js names CampaignView and Bonuses, while ui/campaign.js names Legend. The packages kastgard and kastgard/campaign are marked clean (each new breach fails by itself, which is stricter than the ratchet); kastgard/stacks is not clean until those four edges are removed. The report optional says that 49 edges (45 from core, 4 from Stacks) lead into the campaign from a package that is not optional; until that number is 0, a build of the game cannot leave the campaign out. It is a report, not a check.
Proof that nothing changed
According to DESIGN §95.4, after each step and on the whole, the following was checked:
| Layer | Result |
|---|---|
| three guards after each step | exit 0, 120 edges in 22 files |
baseline.js, calibrate.js --verify | all 188 fingerprints of the game match; 18 measurements of the calibration identical, 0 changed |
replay.js --golden, --golden-maps | the golden records written again are byte for byte the same; git status shows no change in tests/fixtures/ |
| 16 migration tools on the data of the old code | all "as promised"; colony-id-migration fails even without the move, with the same text |
build.js --check | both builds twice byte for byte; the text is old except for the paths (28 lines from K1, one header, the hash of the cache); the three workers identical after the paths are normalised |
| panel Browser, real clicks | the hub of Kastgard (8 tiles); the game in the mode Principalities saved by the old code on 30 October is restored and the AI finishes it; the campaign, 18 minigames, Puzzle Labs, daily challenges, backup and restore |
network and sw.js | gen-worker.js and puzzle-worker.js from js/realms/kastgard/games/stacks/worker/; the worker of the analysis finished the golden record; 195 items of sw.js, all return 200 |
| the whole suite | the first run after the move: 5 failures (tests reading a file of js/ by a fixed path); after the fix 3,268 tests, 3,267 passed, 1 skipped, twice in a row |
What remains and what moved only on paper
It must be said openly: only the paths moved, nothing else. The model "a realm with two games" has not yet reached the code that runs the game. The manifest of the realm has no list games; the code still speaks of a section (Realms.registerSection, the tag section: on 94 items of content), and "game" is for now in the table of packages and in the folders. Minigames, puzzles and lessons lie in the same three data files as before (18 minigames = 13 + 5, 28 puzzles = 5 + 23, 7 lessons = 6 + 1), only in games/stacks/data/. According to DESIGN §95.6 and docs/KASTGARD-GAMES.md, the following remains:
- K7: the content that the game registers. The minigames of the campaign stand on the 5th, 7th, 10th, 13th and 17th place of eighteen, so the analysis recommends a field
afterand a test that compares the list item by item; - K8: the shared hubs Minigames and Puzzle Labs, once the first content for a game other than Stacks appears;
- K9: presets (Principalities, templates, random game) as data, according to the analysis, in a shape shared with the presets of Lines;
- K11: an optional renaming of "section" to "game" in the identifiers;
- M8b: the mentioned 112 edges from
core, which would makekastgard/stacksa clean package and the campaign optional; - division of the language catalogues by packages; the parts of Kastgard should be born already in
js/realms/kastgard/…/i18n/.
Step K10, the renaming of the mode to Principalities, was merged five minutes after the move (31 October at 3:51, DESIGN §97). It is a displayed name and nothing else; the ID classic stays.
What I take from it
- The move is the last step, not the first.
git mvis the smallest part of it; the rest is prepared so that it can be verified. - A tool that rewrites must not forget.
formatTablequietly dropped a field it did not know; only a trial run on a copy of the tree found it, before any merge. - The same classes of blind spots return at every step: an assembled path, a URL with a slash, depth in
.., a tool reading an old commit, a text recorded in a fixture, a toy tree in a test. The table above is a checklist for the next move. - The number 120 did not change, and that is the result. Cleaning the edges is further work, and it is done at the final places of the files, not twice.
Sources and notes
- Model and order:
docs/KASTGARD-GAMES.md(parts 2.1, 3 and 6),docs/DESIGN.md§95 (findings and proofs) anddocs/KASTGARD-MOVED.md(references thatmove.jsdoes not rewrite). - Numbers:
node tools/packages.js --checkof 31 October 2026 (game 0.65.6),git logandgit diff -M --name-statusfor steps K4 and K6. - Plan, not a finished thing: steps K7 to K11 and M8b of
docs/KASTGARD-GAMES.md.
What could not be verified: the proofs (188 fingerprints, calibration, 3,268 tests, migrations, the build, the browser panel) were not run again while writing, and were taken from DESIGN §95.4. The numbers of files, renamings and edges were verified by reading. Whether the player "noticed nothing" cannot be verified: the equality of the outputs was proved, not the observation of people.
Notes
- Up to version 0.65.6 of 31 October 2026 it was called Classic. ↩︎