@cubingcenter/scrambler 0.7.0 → 0.8.0

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.
Files changed (126) hide show
  1. package/README.md +15 -4
  2. package/dist/core/api.d.ts.map +1 -1
  3. package/dist/core/api.js +10 -2
  4. package/dist/puzzles/cube2/solver/browser-loader.d.ts +4 -7
  5. package/dist/puzzles/cube2/solver/browser-loader.d.ts.map +1 -1
  6. package/dist/puzzles/cube2/solver/browser-loader.js +42 -14
  7. package/dist/puzzles/cube2/solver/generated-assets.d.ts.map +1 -1
  8. package/dist/puzzles/cube2/solver/generated-assets.js +8 -3
  9. package/dist/puzzles/cube3/solver/browser-loader.d.ts.map +1 -1
  10. package/dist/puzzles/cube3/solver/browser-loader.js +42 -20
  11. package/dist/puzzles/cube3/solver/generated-assets.d.ts.map +1 -1
  12. package/dist/puzzles/cube3/solver/generated-assets.js +9 -3
  13. package/dist/puzzles/cube3/solver/phase1.d.ts +19 -0
  14. package/dist/puzzles/cube3/solver/phase1.d.ts.map +1 -1
  15. package/dist/puzzles/cube3/solver/phase1.js +29 -6
  16. package/dist/puzzles/cube3/solver/phase2.d.ts +28 -0
  17. package/dist/puzzles/cube3/solver/phase2.d.ts.map +1 -1
  18. package/dist/puzzles/cube3/solver/phase2.js +56 -15
  19. package/dist/puzzles/cube3/solver/tables.d.ts.map +1 -1
  20. package/dist/puzzles/cube3/solver/tables.js +14 -12
  21. package/dist/puzzles/cube4/browser-worker-pool.d.ts +82 -0
  22. package/dist/puzzles/cube4/browser-worker-pool.d.ts.map +1 -0
  23. package/dist/puzzles/cube4/browser-worker-pool.js +205 -0
  24. package/dist/puzzles/cube4/browser-worker.d.ts +2 -0
  25. package/dist/puzzles/cube4/browser-worker.d.ts.map +1 -0
  26. package/dist/puzzles/cube4/browser-worker.js +31 -0
  27. package/dist/puzzles/cube4/index.d.ts +7 -0
  28. package/dist/puzzles/cube4/index.d.ts.map +1 -1
  29. package/dist/puzzles/cube4/index.js +4 -0
  30. package/dist/puzzles/cube4/moves.d.ts.map +1 -1
  31. package/dist/puzzles/cube4/moves.js +122 -30
  32. package/dist/puzzles/cube4/reduction/browser-loader.d.ts +26 -1
  33. package/dist/puzzles/cube4/reduction/browser-loader.d.ts.map +1 -1
  34. package/dist/puzzles/cube4/reduction/browser-loader.js +212 -48
  35. package/dist/puzzles/cube4/reduction/center2.d.ts +24 -0
  36. package/dist/puzzles/cube4/reduction/center2.d.ts.map +1 -1
  37. package/dist/puzzles/cube4/reduction/center2.js +99 -1
  38. package/dist/puzzles/cube4/reduction/finish3.d.ts +53 -0
  39. package/dist/puzzles/cube4/reduction/finish3.d.ts.map +1 -0
  40. package/dist/puzzles/cube4/reduction/finish3.js +393 -0
  41. package/dist/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
  42. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts +5 -3
  43. package/dist/puzzles/cube4/reduction/hc-pdb.d.ts.map +1 -1
  44. package/dist/puzzles/cube4/reduction/hc-pdb.js +30 -12
  45. package/dist/puzzles/cube4/reduction/node-loader.d.ts +10 -1
  46. package/dist/puzzles/cube4/reduction/node-loader.d.ts.map +1 -1
  47. package/dist/puzzles/cube4/reduction/node-loader.js +170 -36
  48. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts +28 -0
  49. package/dist/puzzles/cube4/reduction/pair-transversal.d.ts.map +1 -0
  50. package/dist/puzzles/cube4/reduction/pair-transversal.js +107 -0
  51. package/dist/puzzles/cube4/reduction/phase3-search.d.ts +33 -30
  52. package/dist/puzzles/cube4/reduction/phase3-search.d.ts.map +1 -1
  53. package/dist/puzzles/cube4/reduction/phase3-search.js +211 -104
  54. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts +28 -0
  55. package/dist/puzzles/cube4/reduction/rotation-fix.d.ts.map +1 -0
  56. package/dist/puzzles/cube4/reduction/rotation-fix.js +212 -0
  57. package/dist/puzzles/cube4/reduction/solve.d.ts +72 -13
  58. package/dist/puzzles/cube4/reduction/solve.d.ts.map +1 -1
  59. package/dist/puzzles/cube4/reduction/solve.js +374 -92
  60. package/dist/puzzles/cube4/scramble-cache.d.ts +93 -0
  61. package/dist/puzzles/cube4/scramble-cache.d.ts.map +1 -0
  62. package/dist/puzzles/cube4/scramble-cache.js +150 -0
  63. package/dist/puzzles/cube4/scrambler.d.ts +112 -18
  64. package/dist/puzzles/cube4/scrambler.d.ts.map +1 -1
  65. package/dist/puzzles/cube4/scrambler.js +271 -83
  66. package/dist/puzzles/cube4/worker-pool.d.ts +139 -0
  67. package/dist/puzzles/cube4/worker-pool.d.ts.map +1 -0
  68. package/dist/puzzles/cube4/worker-pool.js +405 -0
  69. package/dist/puzzles/cube4/worker-protocol.d.ts +58 -0
  70. package/dist/puzzles/cube4/worker-protocol.d.ts.map +1 -0
  71. package/dist/puzzles/cube4/worker-protocol.js +58 -0
  72. package/dist/puzzles/cube4/worker.d.ts +26 -0
  73. package/dist/puzzles/cube4/worker.d.ts.map +1 -0
  74. package/dist/puzzles/cube4/worker.js +49 -0
  75. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts +4 -8
  76. package/dist/puzzles/pyraminx/solver/browser-loader.d.ts.map +1 -1
  77. package/dist/puzzles/pyraminx/solver/browser-loader.js +44 -15
  78. package/dist/puzzles/pyraminx/solver/generated-assets.d.ts.map +1 -1
  79. package/dist/puzzles/pyraminx/solver/generated-assets.js +8 -3
  80. package/dist/puzzles/shared/compressed-fetch.d.ts +37 -1
  81. package/dist/puzzles/shared/compressed-fetch.d.ts.map +1 -1
  82. package/dist/puzzles/shared/compressed-fetch.js +226 -31
  83. package/dist/puzzles/shared/load-once.d.ts +16 -0
  84. package/dist/puzzles/shared/load-once.d.ts.map +1 -0
  85. package/dist/puzzles/shared/load-once.js +11 -0
  86. package/dist/puzzles/skewb/solver/browser-loader.d.ts +4 -11
  87. package/dist/puzzles/skewb/solver/browser-loader.d.ts.map +1 -1
  88. package/dist/puzzles/skewb/solver/browser-loader.js +44 -16
  89. package/dist/puzzles/skewb/solver/generated-assets.d.ts.map +1 -1
  90. package/dist/puzzles/skewb/solver/generated-assets.js +8 -3
  91. package/dist/puzzles/sq1/solver/browser-loader.d.ts.map +1 -1
  92. package/dist/puzzles/sq1/solver/browser-loader.js +58 -26
  93. package/dist-cjs/core/api.js +10 -2
  94. package/dist-cjs/puzzles/cube2/solver/browser-loader.js +42 -14
  95. package/dist-cjs/puzzles/cube2/solver/generated-assets.js +8 -3
  96. package/dist-cjs/puzzles/cube3/solver/browser-loader.js +42 -20
  97. package/dist-cjs/puzzles/cube3/solver/generated-assets.js +9 -3
  98. package/dist-cjs/puzzles/cube3/solver/phase1.js +29 -6
  99. package/dist-cjs/puzzles/cube3/solver/phase2.js +56 -15
  100. package/dist-cjs/puzzles/cube3/solver/tables.js +14 -12
  101. package/dist-cjs/puzzles/cube4/browser-worker-pool.js +211 -0
  102. package/dist-cjs/puzzles/cube4/index.js +21 -1
  103. package/dist-cjs/puzzles/cube4/moves.js +122 -30
  104. package/dist-cjs/puzzles/cube4/reduction/browser-loader.js +212 -48
  105. package/dist-cjs/puzzles/cube4/reduction/center2.js +102 -2
  106. package/dist-cjs/puzzles/cube4/reduction/finish3.js +398 -0
  107. package/dist-cjs/puzzles/cube4/reduction/generated/hc-manifest.json +6 -0
  108. package/dist-cjs/puzzles/cube4/reduction/hc-pdb.js +31 -12
  109. package/dist-cjs/puzzles/cube4/reduction/node-loader.js +171 -36
  110. package/dist-cjs/puzzles/cube4/reduction/pair-transversal.js +112 -0
  111. package/dist-cjs/puzzles/cube4/reduction/phase3-search.js +211 -104
  112. package/dist-cjs/puzzles/cube4/reduction/rotation-fix.js +216 -0
  113. package/dist-cjs/puzzles/cube4/reduction/solve.js +374 -88
  114. package/dist-cjs/puzzles/cube4/scramble-cache.js +156 -0
  115. package/dist-cjs/puzzles/cube4/scrambler.js +271 -80
  116. package/dist-cjs/puzzles/cube4/worker-pool.js +447 -0
  117. package/dist-cjs/puzzles/cube4/worker-protocol.js +62 -0
  118. package/dist-cjs/puzzles/pyraminx/solver/browser-loader.js +44 -15
  119. package/dist-cjs/puzzles/pyraminx/solver/generated-assets.js +8 -3
  120. package/dist-cjs/puzzles/shared/compressed-fetch.js +229 -31
  121. package/dist-cjs/puzzles/shared/load-once.js +14 -0
  122. package/dist-cjs/puzzles/skewb/solver/browser-loader.js +44 -16
  123. package/dist-cjs/puzzles/skewb/solver/generated-assets.js +8 -3
  124. package/dist-cjs/puzzles/sq1/solver/browser-loader.js +58 -26
  125. package/docs/development.md +19 -7
  126. package/package.json +4 -3
@@ -1,23 +1,74 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Browser loader for the cube4 reduction tables (the committed .bin files).
4
- * The manifests are per-phase (`center1-manifest.json`, ...).
4
+ * The manifests are per-phase (`center1-manifest.json`, ...) plus the shared
5
+ * `hc-manifest.json`, which also seals the optional `c3_pair_dist.bin`.
6
+ *
7
+ * Integrity — identical to node-loader.ts: every downloaded .bin is verified
8
+ * against the manifest sha256 + expected byte length (on the DECOMPRESSED
9
+ * payload, so .br/.gz variants and the raw file are checked the same way).
10
+ *
11
+ * Optional tables (`hc` + `c3PairDist`), NEVER silent:
12
+ * - `hc-manifest.json` present → its entries must download and verify, else
13
+ * the whole load REJECTS (node throws too). There is no silent degradation
14
+ * to an unpruned search: it would change the scramble produced for the
15
+ * same seed, breaking node/browser determinism.
16
+ * - `hc-manifest.json` absent (404) → the optional tables were not deployed;
17
+ * `c3_pair_dist.bin` is then probed to tell "not deployed" (both absent →
18
+ * skip, like node) apart from "stale/half-deployed assets" (bin present,
19
+ * manifest missing → REJECT with an actionable message — node refuses the
20
+ * same asset set).
21
+ * There is deliberately no "degraded" flag: the only valid outcomes are
22
+ * "fully loaded tables" or "loud failure", so both platforms always compute
23
+ * the same scrambles.
24
+ *
25
+ * Caching: concurrent loads share ONE in-flight promise (one fetch pass per
26
+ * table). The promise is kept after success; when it rejects it is purged so
27
+ * the next call can retry instead of being stuck on a poisoned cache.
5
28
  */
6
29
  Object.defineProperty(exports, "__esModule", { value: true });
7
30
  exports.getCachedCube4Tables = getCachedCube4Tables;
8
31
  exports.clearCachedCube4Tables = clearCachedCube4Tables;
9
32
  exports.loadCube4ReductionTablesBrowser = loadCube4ReductionTablesBrowser;
33
+ const center3_js_1 = require("./center3.js");
10
34
  const edge3_js_1 = require("./edge3.js");
11
35
  const compressed_fetch_js_1 = require("../../shared/compressed-fetch.js");
12
36
  const hc_pdb_js_1 = require("./hc-pdb.js");
13
37
  const GENERATED_DIRECTORY = "puzzles/cube4/reduction/generated";
14
38
  const CACHE_NAME = "cubingcenter-scrambler-tables-v1";
39
+ const TABLE_MEMORY_BUDGET_BYTES = 150 * 1024 * 1024;
40
+ /**
41
+ * Byte length of `c3_pair_dist.bin`: CENTER3_STATES x 144 nibble entries
42
+ * (index = ct3 * 144 + f * 12 + h, see phase3-search.ts), nibble-packed.
43
+ * Kept in sync with node-loader.ts.
44
+ */
45
+ const C3_PAIR_DIST_BYTES = (center3_js_1.CENTER3_STATES * 144) / 2;
15
46
  let cachedTables;
47
+ let cachedTablesPromise;
48
+ function assertCube4ReductionTableBudget(tables) {
49
+ const bytes = tables.center1.distance.byteLength +
50
+ tables.center1.moveMaps.byteLength +
51
+ tables.center2.ctMove.byteLength +
52
+ tables.center2.rlMove.byteLength +
53
+ tables.center2.pruning.byteLength +
54
+ (tables.center2.pruningWide?.byteLength ?? 0) +
55
+ tables.center3.moveTable.byteLength +
56
+ tables.center3.pruning.byteLength +
57
+ tables.edge3.distances.byteLength +
58
+ (tables.hc?.moves.byteLength ?? 0) +
59
+ (tables.hc?.distances.byteLength ?? 0) +
60
+ (tables.hc?.coordinates.byteLength ?? 0) +
61
+ (tables.c3PairDist?.byteLength ?? 0);
62
+ if (bytes > TABLE_MEMORY_BUDGET_BYTES) {
63
+ throw new Error(`Cube4 reduction tables use ${(bytes / (1024 * 1024)).toFixed(1)} MiB, above the 150 MiB table budget.`);
64
+ }
65
+ }
16
66
  function getCachedCube4Tables() {
17
67
  return cachedTables;
18
68
  }
19
69
  function clearCachedCube4Tables() {
20
70
  cachedTables = undefined;
71
+ cachedTablesPromise = undefined;
21
72
  }
22
73
  function resolveBaseUrl(provided) {
23
74
  if (provided) {
@@ -32,36 +83,95 @@ function resolveBaseUrl(provided) {
32
83
  }
33
84
  return ".";
34
85
  }
35
- async function loadCube4ReductionTablesBrowser(baseUrl) {
36
- if (cachedTables) {
37
- return cachedTables;
86
+ function loadCube4ReductionTablesBrowser(baseUrl) {
87
+ if (cachedTablesPromise) {
88
+ // One shared in-flight promise: concurrent calls trigger exactly one
89
+ // fetch pass (dedup), and the resolved value stays cached afterwards.
90
+ return cachedTablesPromise;
38
91
  }
39
92
  const resolved = resolveBaseUrl(baseUrl);
40
- const [center1, center2, center3, edge3, hc] = await Promise.all([
41
- loadCenter1(resolved),
42
- loadCenter2(resolved),
43
- loadCenter3(resolved),
44
- loadEdge3(resolved),
45
- loadHc(resolved)
46
- ]);
47
- cachedTables = { center1, center2, center3, edge3 };
48
- if (hc)
49
- cachedTables.hc = hc;
50
- return cachedTables;
93
+ const promise = (async () => {
94
+ const [center1, center2, center3, edge3, optional] = await Promise.all([
95
+ loadCenter1(resolved),
96
+ loadCenter2(resolved),
97
+ loadCenter3(resolved),
98
+ loadEdge3(resolved),
99
+ loadOptionalTables(resolved)
100
+ ]);
101
+ const tables = { center1, center2, center3, edge3 };
102
+ if (optional.hc)
103
+ tables.hc = optional.hc;
104
+ if (optional.c3PairDist)
105
+ tables.c3PairDist = optional.c3PairDist;
106
+ assertCube4ReductionTableBudget(tables);
107
+ return tables;
108
+ })();
109
+ cachedTablesPromise = promise;
110
+ promise.then((value) => {
111
+ cachedTables = value;
112
+ }, () => {
113
+ // A failed load must not poison the cache: purge the rejected promise
114
+ // so a later call retries from scratch.
115
+ if (cachedTablesPromise === promise)
116
+ cachedTablesPromise = undefined;
117
+ });
118
+ return promise;
119
+ }
120
+ function requireEntry(entry, manifestUrl, label) {
121
+ if (!entry || typeof entry.file !== "string" || typeof entry.checksum !== "string") {
122
+ throw new Error(`Invalid table manifest ${manifestUrl}: entry "${label}" must declare { file, checksum } — ` +
123
+ "the deployed assets are stale; sync them with pnpm tables:web:sync.");
124
+ }
125
+ return entry;
51
126
  }
52
- async function fetchJson(baseUrl, file) {
127
+ /**
128
+ * Expected byte length of a table file from its manifest entry.
129
+ * `packing` matches how each table is serialized — keep in sync with
130
+ * node-loader.ts.
131
+ */
132
+ function expectedBytes(entry, packing) {
133
+ if (typeof entry.bytes === "number")
134
+ return entry.bytes;
135
+ if (typeof entry.range !== "number")
136
+ return undefined;
137
+ if (packing === "byte")
138
+ return entry.range;
139
+ if (packing === "nibble")
140
+ return Math.ceil(entry.range / 2);
141
+ return entry.range * 4;
142
+ }
143
+ async function fetchVerified(baseUrl, entry, packing, expectedLength) {
144
+ const length = expectedLength ?? expectedBytes(entry, packing);
145
+ return (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, entry.file, {
146
+ cacheName: CACHE_NAME,
147
+ compressed: entry.compressed !== false,
148
+ expectedSha256: entry.checksum,
149
+ ...(length !== undefined ? { expectedBytes: length } : {})
150
+ });
151
+ }
152
+ async function fetchManifest(baseUrl, file) {
53
153
  const url = `${baseUrl}/${GENERATED_DIRECTORY}/${file}`;
54
154
  const response = await fetch(url);
55
155
  if (!response.ok) {
56
- throw new Error(`Failed to load ${file}: ${response.status} ${response.statusText}`);
156
+ throw new Error(`Failed to load table manifest ${url}: ${response.status} ${response.statusText}`);
157
+ }
158
+ const text = await response.text();
159
+ try {
160
+ return JSON.parse(text);
161
+ }
162
+ catch (error) {
163
+ // Always name the URL: a bare JSON.parse error is unactionable.
164
+ throw new Error(`Invalid table manifest JSON: ${url}: ${error instanceof Error ? error.message : String(error)}`);
57
165
  }
58
- return response.json();
59
166
  }
60
167
  async function loadCenter1(baseUrl) {
61
- const manifest = await fetchJson(baseUrl, "center1-manifest.json");
168
+ const manifestUrl = `${baseUrl}/${GENERATED_DIRECTORY}/center1-manifest.json`;
169
+ const manifest = await fetchManifest(baseUrl, "center1-manifest.json");
170
+ const distanceTable = requireEntry(manifest.distanceTable, manifestUrl, "distanceTable");
171
+ const moveMaps = requireEntry(manifest.moveMaps, manifestUrl, "moveMaps");
62
172
  const [distanceBuffer, moveMapsBuffer] = await Promise.all([
63
- (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.distanceTable.file, { cacheName: CACHE_NAME, compressed: manifest.distanceTable.compressed !== false }),
64
- (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.moveMaps.file, { cacheName: CACHE_NAME, compressed: manifest.moveMaps.compressed !== false })
173
+ fetchVerified(baseUrl, distanceTable, "nibble"),
174
+ fetchVerified(baseUrl, moveMaps, "int32")
65
175
  ]);
66
176
  return {
67
177
  distance: new Uint8Array(distanceBuffer),
@@ -69,23 +179,30 @@ async function loadCenter1(baseUrl) {
69
179
  };
70
180
  }
71
181
  async function loadCenter2(baseUrl) {
72
- const manifest = await fetchJson(baseUrl, "center2-manifest.json");
182
+ const manifestUrl = `${baseUrl}/${GENERATED_DIRECTORY}/center2-manifest.json`;
183
+ const manifest = await fetchManifest(baseUrl, "center2-manifest.json");
184
+ const ctMove = requireEntry(manifest.ctMove, manifestUrl, "ctMove");
185
+ const rlMove = requireEntry(manifest.rlMove, manifestUrl, "rlMove");
186
+ const pruningTable = requireEntry(manifest.pruningTable, manifestUrl, "pruningTable");
187
+ const pruningWideEntry = manifest.pruningWideTable
188
+ ? requireEntry(manifest.pruningWideTable, manifestUrl, "pruningWideTable")
189
+ : undefined;
73
190
  const [ctMoveBuffer, rlMoveBuffer, pruningBuffer] = await Promise.all([
74
- (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.ctMove.file, { cacheName: CACHE_NAME, compressed: manifest.ctMove.compressed !== false }),
75
- (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.rlMove.file, { cacheName: CACHE_NAME, compressed: manifest.rlMove.compressed !== false }),
76
- (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.pruningTable.file, { cacheName: CACHE_NAME, compressed: manifest.pruningTable.compressed !== false })
191
+ fetchVerified(baseUrl, ctMove, "int32"),
192
+ fetchVerified(baseUrl, rlMove, "int32"),
193
+ fetchVerified(baseUrl, pruningTable, "nibble")
77
194
  ]);
78
- const ctMove = new Int32Array(ctMoveBuffer);
79
- const rlMove = new Int32Array(rlMoveBuffer);
195
+ const ctMoveValues = new Int32Array(ctMoveBuffer);
196
+ const rlMoveValues = new Int32Array(rlMoveBuffer);
80
197
  const pruningPacked = new Uint8Array(pruningBuffer);
81
198
  const pruning = new Uint8Array(450450);
82
199
  for (let i = 0; i < 450450; i++) {
83
200
  const byte = pruningPacked[i >> 1];
84
201
  pruning[i] = (i & 1) === 0 ? byte & 0x0f : byte >> 4;
85
202
  }
86
- const tables = { ctMove, rlMove, pruning };
87
- if (manifest.pruningWideTable) {
88
- const wideBuffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.pruningWideTable.file, { cacheName: CACHE_NAME, compressed: manifest.pruningWideTable.compressed !== false });
203
+ const tables = { ctMove: ctMoveValues, rlMove: rlMoveValues, pruning };
204
+ if (pruningWideEntry) {
205
+ const wideBuffer = await fetchVerified(baseUrl, pruningWideEntry, "nibble");
89
206
  const widePacked = new Uint8Array(wideBuffer);
90
207
  const pruningWide = new Uint8Array(450450);
91
208
  for (let i = 0; i < 450450; i++) {
@@ -97,10 +214,13 @@ async function loadCenter2(baseUrl) {
97
214
  return tables;
98
215
  }
99
216
  async function loadCenter3(baseUrl) {
100
- const manifest = await fetchJson(baseUrl, "center3-manifest.json");
217
+ const manifestUrl = `${baseUrl}/${GENERATED_DIRECTORY}/center3-manifest.json`;
218
+ const manifest = await fetchManifest(baseUrl, "center3-manifest.json");
219
+ const moveTable = requireEntry(manifest.moveTable, manifestUrl, "moveTable");
220
+ const pruningTable = requireEntry(manifest.pruningTable, manifestUrl, "pruningTable");
101
221
  const [moveTableBuffer, pruningBuffer] = await Promise.all([
102
- (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.moveTable.file, { cacheName: CACHE_NAME, compressed: manifest.moveTable.compressed !== false }),
103
- (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.pruningTable.file, { cacheName: CACHE_NAME, compressed: manifest.pruningTable.compressed !== false })
222
+ fetchVerified(baseUrl, moveTable, "int32"),
223
+ fetchVerified(baseUrl, pruningTable, "byte")
104
224
  ]);
105
225
  return {
106
226
  moveTable: new Int32Array(moveTableBuffer),
@@ -108,24 +228,68 @@ async function loadCenter3(baseUrl) {
108
228
  };
109
229
  }
110
230
  async function loadEdge3(baseUrl) {
111
- const manifest = await fetchJson(baseUrl, "edge3-manifest.json");
112
- const distancesBuffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.distances.file, { cacheName: CACHE_NAME, compressed: manifest.distances.compressed !== false });
231
+ const manifestUrl = `${baseUrl}/${GENERATED_DIRECTORY}/edge3-manifest.json`;
232
+ const manifest = await fetchManifest(baseUrl, "edge3-manifest.json");
233
+ const distances = requireEntry(manifest.distances, manifestUrl, "distances");
234
+ // `range` here already counts packed nibble BYTES (classCount / 2), unlike
235
+ // the other manifests — derive the length from classCount instead.
236
+ const length = typeof manifest.classCount === "number" ? Math.ceil(manifest.classCount / 2) : expectedBytes(distances, "byte");
237
+ const distancesBuffer = await fetchVerified(baseUrl, distances, "nibble", length);
113
238
  // Keep nibble-packed (see node-loader.ts): all readers handle both formats.
114
- const distances = new Uint8Array(distancesBuffer);
115
- return { distances, distanceOfEdge: (0, edge3_js_1.edge3DistanceLookup)(distances) };
239
+ const table = new Uint8Array(distancesBuffer);
240
+ return { distances: table, distanceOfEdge: (0, edge3_js_1.edge3DistanceLookup)(table) };
116
241
  }
117
- async function loadHc(baseUrl) {
118
- try {
119
- const manifest = await fetchJson(baseUrl, "hc-manifest.json");
120
- const distancesBuffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.distances.file, { cacheName: CACHE_NAME, compressed: manifest.distances.compressed !== false });
121
- const dense = (0, hc_pdb_js_1.buildHcDense)(new Uint8Array(distancesBuffer));
122
- if (manifest.moveTable) {
123
- const moveBuffer = await (0, compressed_fetch_js_1.fetchTableBuffer)(baseUrl, GENERATED_DIRECTORY, manifest.moveTable.file, { cacheName: CACHE_NAME, compressed: manifest.moveTable.compressed !== false });
124
- dense.moves = new Int32Array(moveBuffer);
242
+ /**
243
+ * The two optional PDB tables sealed in `hc-manifest.json`. Semantics mirror
244
+ * node-loader.ts exactly (see the header comment): present → verified or
245
+ * reject; absent → probe the c3 bin so stale deployments fail loudly.
246
+ */
247
+ async function loadOptionalTables(baseUrl) {
248
+ const manifestUrl = `${baseUrl}/${GENERATED_DIRECTORY}/hc-manifest.json`;
249
+ const response = await fetch(manifestUrl);
250
+ let manifest;
251
+ if (response.ok) {
252
+ const text = await response.text();
253
+ try {
254
+ manifest = JSON.parse(text);
255
+ }
256
+ catch (error) {
257
+ throw new Error(`Invalid table manifest JSON: ${manifestUrl}: ${error instanceof Error ? error.message : String(error)}`);
125
258
  }
126
- return dense;
127
259
  }
128
- catch {
129
- return undefined;
260
+ else if (response.status !== 404) {
261
+ throw new Error(`Failed to load table manifest ${manifestUrl}: ${response.status} ${response.statusText}`);
262
+ }
263
+ const result = {};
264
+ if (manifest?.c3PairDist !== undefined) {
265
+ const entry = requireEntry(manifest.c3PairDist, manifestUrl, "c3PairDist");
266
+ if (typeof entry.bytes === "number" && entry.bytes !== C3_PAIR_DIST_BYTES) {
267
+ throw new Error(`Invalid table manifest ${manifestUrl}: c3PairDist.bytes is ${entry.bytes}, expected ` +
268
+ `${C3_PAIR_DIST_BYTES} — sync the assets with pnpm tables:web:sync.`);
269
+ }
270
+ const buffer = await fetchVerified(baseUrl, entry, "nibble", C3_PAIR_DIST_BYTES);
271
+ result.c3PairDist = new Uint8Array(buffer);
272
+ }
273
+ else {
274
+ // Mirror node's fs check: bin present but unsealed = broken deployment.
275
+ const probeUrl = `${baseUrl}/${GENERATED_DIRECTORY}/c3_pair_dist.bin`;
276
+ const probe = await fetch(probeUrl);
277
+ if (probe.ok) {
278
+ throw new Error(`c3_pair_dist.bin is deployed (${probeUrl}) but hc-manifest.json has no "c3PairDist" checksum ` +
279
+ `entry (${manifestUrl}) — refusing to load tables without integrity checks (node fails on the ` +
280
+ "same assets). Sync the assets: pnpm tables:web:sync.");
281
+ }
282
+ }
283
+ if (manifest) {
284
+ const distances = requireEntry(manifest.distances, manifestUrl, "distances");
285
+ const packed = new Uint8Array(await fetchVerified(baseUrl, distances, "nibble"));
286
+ const dense = (0, hc_pdb_js_1.buildHcDense)(packed);
287
+ if (manifest.moveTable !== undefined) {
288
+ const moveTable = requireEntry(manifest.moveTable, manifestUrl, "moveTable");
289
+ const moveBuffer = await fetchVerified(baseUrl, moveTable, "int32");
290
+ dense.moves = new Int32Array(moveBuffer);
291
+ }
292
+ result.hc = dense;
130
293
  }
294
+ return result;
131
295
  }
@@ -20,12 +20,14 @@
20
20
  * The phase-1 → phase-2 bridge (alignment + encode) is in `reduction.ts`.
21
21
  */
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.ALIGNMENT_ROTATIONS = exports.CENTER2_STATES = exports.CENTER2_RL_STATES = exports.CENTER2_CT_STATES = exports.CENTER2_GOAL_RL = exports.CENTER2_SEARCH_MOVES_WIDE = exports.CENTER2_SEARCH_MOVES = exports.CENTER2_MOVES = void 0;
23
+ exports.CENTER2_CHIRAL_STATES = exports.CENTER2_CHIRAL_CT_STATES = exports.ALIGNMENT_ROTATIONS = exports.CENTER2_STATES = exports.CENTER2_RL_STATES = exports.CENTER2_CT_STATES = exports.CENTER2_GOAL_RL = exports.CENTER2_SEARCH_MOVES_WIDE = exports.CENTER2_SEARCH_MOVES = exports.CENTER2_MOVES = void 0;
24
24
  exports.wingPermutationParity = wingPermutationParity;
25
25
  exports.encodeCenter2 = encodeCenter2;
26
26
  exports.isCenter2Goal = isCenter2Goal;
27
27
  exports.center2Bridge = center2Bridge;
28
28
  exports.generateCenter2Tables = generateCenter2Tables;
29
+ exports.encodeCenter2ChiralCt = encodeCenter2ChiralCt;
30
+ exports.generateCenter2ChiralTables = generateCenter2ChiralTables;
29
31
  exports.solveCenter2 = solveCenter2;
30
32
  exports.solveCenter2All = solveCenter2All;
31
33
  exports.getWingMoveMaps = getWingMoveMaps;
@@ -55,6 +57,12 @@ exports.CENTER2_GOAL_RL = [0, 18, 28, 46, 54, 56];
55
57
  exports.CENTER2_CT_STATES = 6435;
56
58
  exports.CENTER2_RL_STATES = 70;
57
59
  exports.CENTER2_STATES = exports.CENTER2_CT_STATES * exports.CENTER2_RL_STATES;
60
+ const CENTER2_GOAL_FLAGS = (() => {
61
+ const flags = new Uint8Array(exports.CENTER2_RL_STATES);
62
+ for (const rl of exports.CENTER2_GOAL_RL)
63
+ flags[rl] = 1;
64
+ return flags;
65
+ })();
58
66
  // ct positions: U(0-3), D(12-15), F(8-11), B(20-23) — the reference's face
59
67
  // order {U, D, F, B}, in this repo's face-major numbering.
60
68
  const CT_POS = [0, 1, 2, 3, 12, 13, 14, 15, 8, 9, 10, 11, 20, 21, 22, 23];
@@ -118,7 +126,7 @@ function encodeCenter2(state) {
118
126
  return { ct: ctRank, rl: rlRank };
119
127
  }
120
128
  function isCenter2Goal(ct, rl) {
121
- return ct === 0 && exports.CENTER2_GOAL_RL.includes(rl);
129
+ return ct === 0 && CENTER2_GOAL_FLAGS[rl] === 1;
122
130
  }
123
131
  /**
124
132
  * The phase-2 coordinate is only valid for states where exactly 8 of the
@@ -306,6 +314,98 @@ function generateCenter2Tables() {
306
314
  }
307
315
  return { ctMove, rlMove, pruning, pruningWide };
308
316
  }
317
+ // ---------------------------------------------------------------------------
318
+ // Chiral (absolute-ct) phase-2 tables
319
+ // ---------------------------------------------------------------------------
320
+ /**
321
+ * The ct rank above is RELATIVE to ct[15], so `ct == 0` accepts both
322
+ * assignments of the two non-R/L axes to the U/D and F/B face-pairs. Only
323
+ * one of them survives the downstream chirality gate (`axisDet`: the U, R, F
324
+ * axes must form a right-handed frame, i.e. U/D must hold axis (a + 2) % 3
325
+ * when R/L holds axis a). The chiral coordinate ranks the ABSOLUTE pattern
326
+ * "which of the 16 U/D/F/B positions hold the U/D-bound axis" (C(16,8) =
327
+ * 12,870) and its pruning measures the distance to the chirality-correct
328
+ * goals only, so the phase-2 search never walks wrong-chirality goal paths.
329
+ * Same goal set as `ct == 0 && axisDet == 1`; generated at runtime (~1.6 MB).
330
+ */
331
+ exports.CENTER2_CHIRAL_CT_STATES = 12870;
332
+ exports.CENTER2_CHIRAL_STATES = exports.CENTER2_CHIRAL_CT_STATES * exports.CENTER2_RL_STATES;
333
+ function rankSubset16(bits) {
334
+ let idx = 0;
335
+ let r = 8;
336
+ for (let i = 15; i >= 0 && r > 0; i--) {
337
+ if (bits[i] === 1) {
338
+ idx += CNK[i][r];
339
+ r--;
340
+ }
341
+ }
342
+ return idx;
343
+ }
344
+ function unrankSubset16(idx) {
345
+ const bits = new Uint8Array(16);
346
+ let r = 8;
347
+ for (let i = 15; i >= 0 && r > 0; i--) {
348
+ if (idx >= CNK[i][r]) {
349
+ idx -= CNK[i][r];
350
+ bits[i] = 1;
351
+ r--;
352
+ }
353
+ }
354
+ return bits;
355
+ }
356
+ /** Chiral ct rank of a coordinate-invariant (bridged) phase-2 state. */
357
+ function encodeCenter2ChiralCt(state) {
358
+ const rlAxis = state.centers[RL_POS[0]] % 3;
359
+ const udAxis = (rlAxis + 2) % 3;
360
+ const bits = new Uint8Array(16);
361
+ for (let i = 0; i < 16; i++)
362
+ bits[i] = state.centers[CT_POS[i]] % 3 === udAxis ? 1 : 0;
363
+ return rankSubset16(bits);
364
+ }
365
+ function generateCenter2ChiralTables(rlMove) {
366
+ const moveMaps = buildMovePositionMaps();
367
+ const nMoves = exports.CENTER2_MOVES.length;
368
+ const ctMove = new Uint16Array(exports.CENTER2_CHIRAL_CT_STATES * nMoves);
369
+ const full = new Uint8Array(24);
370
+ const bits = new Uint8Array(16);
371
+ for (let ct = 0; ct < exports.CENTER2_CHIRAL_CT_STATES; ct++) {
372
+ const pattern = unrankSubset16(ct);
373
+ full.fill(0);
374
+ for (let i = 0; i < 16; i++)
375
+ full[CT_POS[i]] = pattern[i];
376
+ for (let m = 0; m < nMoves; m++) {
377
+ for (let i = 0; i < 16; i++)
378
+ bits[i] = full[moveMaps[m * 24 + CT_POS[i]]];
379
+ ctMove[ct * nMoves + m] = rankSubset16(bits);
380
+ }
381
+ }
382
+ const goalBits = new Uint8Array(16);
383
+ for (let i = 0; i < 8; i++)
384
+ goalBits[i] = 1; // CT_POS[0..7] = U and D faces
385
+ const goalCt = rankSubset16(goalBits);
386
+ const pruning = new Uint8Array(exports.CENTER2_CHIRAL_STATES).fill(0xff);
387
+ const queue = new Int32Array(exports.CENTER2_CHIRAL_STATES);
388
+ let tail = 0;
389
+ for (const rl of exports.CENTER2_GOAL_RL) {
390
+ const idx = goalCt * exports.CENTER2_RL_STATES + rl;
391
+ pruning[idx] = 0;
392
+ queue[tail++] = idx;
393
+ }
394
+ for (let head = 0; head < tail; head++) {
395
+ const idx = queue[head];
396
+ const ct = (idx / exports.CENTER2_RL_STATES) | 0;
397
+ const rl = idx - ct * exports.CENTER2_RL_STATES;
398
+ const d = pruning[idx] + 1;
399
+ for (let m = 0; m < exports.CENTER2_SEARCH_MOVES.length; m++) {
400
+ const next = ctMove[ct * nMoves + m] * exports.CENTER2_RL_STATES + rlMove[rl * nMoves + m];
401
+ if (pruning[next] === 0xff) {
402
+ pruning[next] = d;
403
+ queue[tail++] = next;
404
+ }
405
+ }
406
+ }
407
+ return { ctMove, pruning, goalCt };
408
+ }
309
409
  // The reference's cancellation rule (ckmv over move2std): skip the same base
310
410
  // move, or the same axis-class when the previous move's standard index is
311
411
  // larger than the current's.