@dreamboard-games/workspace-codegen 0.1.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 (73) hide show
  1. package/LICENSE +89 -0
  2. package/NOTICE +1 -0
  3. package/dist/hex-geometry.d.ts +2 -0
  4. package/dist/hex-geometry.js +49 -0
  5. package/dist/index.d.ts +13 -0
  6. package/dist/index.js +22 -0
  7. package/dist/manifest-contract.d.ts +14 -0
  8. package/dist/manifest-contract.js +4897 -0
  9. package/dist/manifest-validation.d.ts +6 -0
  10. package/dist/manifest-validation.js +506 -0
  11. package/dist/ownership.d.ts +31 -0
  12. package/dist/ownership.js +86 -0
  13. package/dist/preset-card-sets.d.ts +5 -0
  14. package/dist/preset-card-sets.js +135 -0
  15. package/dist/seeds.d.ts +6 -0
  16. package/dist/seeds.js +766 -0
  17. package/ownership.json +51 -0
  18. package/package.json +46 -0
  19. package/src/__fixtures__/sdk-types/invalid-card-properties-extra-key.ts +62 -0
  20. package/src/__fixtures__/sdk-types/invalid-card-properties-missing-required.ts +60 -0
  21. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-enum.ts +61 -0
  22. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-nested.ts +61 -0
  23. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-scalar.ts +61 -0
  24. package/src/__fixtures__/sdk-types/invalid-card-visibility.ts +40 -0
  25. package/src/__fixtures__/sdk-types/invalid-container-card-set-manifest.ts +62 -0
  26. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-array-item.ts +43 -0
  27. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-player-id.ts +43 -0
  28. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-resource-id.ts +43 -0
  29. package/src/__fixtures__/sdk-types/invalid-die-home-per-player-zone-no-owner.ts +35 -0
  30. package/src/__fixtures__/sdk-types/invalid-die-seed-type-id.ts +31 -0
  31. package/src/__fixtures__/sdk-types/invalid-die-visibility.ts +28 -0
  32. package/src/__fixtures__/sdk-types/invalid-generic-board-edge-id.ts +38 -0
  33. package/src/__fixtures__/sdk-types/invalid-generic-board-nested-field.ts +45 -0
  34. package/src/__fixtures__/sdk-types/invalid-hex-edge-field-edge-id.ts +47 -0
  35. package/src/__fixtures__/sdk-types/invalid-hex-vertex-field-vertex-id.ts +47 -0
  36. package/src/__fixtures__/sdk-types/invalid-manifest.ts +143 -0
  37. package/src/__fixtures__/sdk-types/invalid-piece-fields-extra-key.ts +62 -0
  38. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-card-id.ts +61 -0
  39. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-enum.ts +61 -0
  40. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-scalar.ts +61 -0
  41. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-zone-id.ts +61 -0
  42. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-container-no-owner.ts +48 -0
  43. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-edge-no-owner.ts +47 -0
  44. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-space-no-owner.ts +42 -0
  45. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-vertex-no-owner.ts +49 -0
  46. package/src/__fixtures__/sdk-types/invalid-piece-seed-type-id.ts +30 -0
  47. package/src/__fixtures__/sdk-types/invalid-piece-visibility.ts +28 -0
  48. package/src/__fixtures__/sdk-types/invalid-slot-host-manifest.ts +47 -0
  49. package/src/__fixtures__/sdk-types/invalid-slot-id-manifest.ts +49 -0
  50. package/src/__fixtures__/sdk-types/invalid-square-board-edge-id.ts +47 -0
  51. package/src/__fixtures__/sdk-types/invalid-square-board-space-id.ts +47 -0
  52. package/src/__fixtures__/sdk-types/invalid-square-board-vertex-id.ts +49 -0
  53. package/src/__fixtures__/sdk-types/invalid-square-container-field-space-id.ts +59 -0
  54. package/src/__fixtures__/sdk-types/invalid-square-container-host-space-id.ts +49 -0
  55. package/src/__fixtures__/sdk-types/invalid-square-relation-field-scalar.ts +48 -0
  56. package/src/__fixtures__/sdk-types/invalid-square-relation-space-id.ts +48 -0
  57. package/src/__fixtures__/sdk-types/invalid-square-space-fields-enum.ts +44 -0
  58. package/src/__fixtures__/sdk-types/invalid-square-space-fields-extra-key.ts +45 -0
  59. package/src/__fixtures__/sdk-types/valid-die-type-omits-sides.ts +29 -0
  60. package/src/__fixtures__/sdk-types/valid-manifest-omits-board-templates.ts +19 -0
  61. package/src/__fixtures__/sdk-types/valid-manifest.ts +612 -0
  62. package/src/__fixtures__/sdk-types/valid-player-scoped-seed-homes.ts +59 -0
  63. package/src/authoring-benchmark.test.ts +362 -0
  64. package/src/hex-geometry.ts +69 -0
  65. package/src/index.ts +64 -0
  66. package/src/manifest-contract.test.ts +1764 -0
  67. package/src/manifest-contract.ts +6581 -0
  68. package/src/manifest-validation.test.ts +393 -0
  69. package/src/manifest-validation.ts +795 -0
  70. package/src/ownership.ts +127 -0
  71. package/src/preset-card-sets.ts +169 -0
  72. package/src/sdk-types-authoring.test.ts +361 -0
  73. package/src/seeds.ts +800 -0
@@ -0,0 +1,59 @@
1
+ import { defineTopologyManifest } from "@dreamboard/sdk-types";
2
+
3
+ defineTopologyManifest({
4
+ players: {
5
+ minPlayers: 2,
6
+ maxPlayers: 2,
7
+ optimalPlayers: 2,
8
+ },
9
+ cardSets: [],
10
+ zones: [
11
+ {
12
+ id: "main-hand",
13
+ name: "Main Hand",
14
+ scope: "perPlayer",
15
+ },
16
+ ],
17
+ boardTemplates: [],
18
+ boards: [
19
+ {
20
+ id: "player-grid",
21
+ name: "Player Grid",
22
+ layout: "square",
23
+ scope: "perPlayer",
24
+ spaces: [{ id: "cell-a", row: 0, col: 0 }],
25
+ relations: [],
26
+ containers: [],
27
+ edges: [],
28
+ vertices: [],
29
+ },
30
+ ],
31
+ pieceTypes: [{ id: "meeple", name: "Meeple" }],
32
+ pieceSeeds: [
33
+ {
34
+ id: "worker-a",
35
+ typeId: "meeple",
36
+ ownerId: "player-1",
37
+ home: {
38
+ type: "space",
39
+ boardId: "player-grid",
40
+ spaceId: "cell-a",
41
+ },
42
+ },
43
+ ],
44
+ dieTypes: [{ id: "d6", name: "D6", sides: 6 }],
45
+ dieSeeds: [
46
+ {
47
+ id: "die-a",
48
+ typeId: "d6",
49
+ ownerId: "player-1",
50
+ home: {
51
+ type: "zone",
52
+ zoneId: "main-hand",
53
+ },
54
+ },
55
+ ],
56
+ resources: [],
57
+ setupOptions: [],
58
+ setupProfiles: [],
59
+ } as const);
@@ -0,0 +1,362 @@
1
+ import { lstat, mkdir, mkdtemp, readFile, rm, symlink } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { expect, test } from "bun:test";
6
+ import type { GameTopologyManifest } from "@dreamboard/sdk-types";
7
+ import { generateAuthoritativeFiles } from "./index.js";
8
+ import { validateManifestAuthoring } from "./manifest-validation.js";
9
+ import { materializeManifest } from "../../../apps/dreamboard-cli/src/services/project/manifest-authoring.ts";
10
+
11
+ const repoRoot = path.resolve(import.meta.dir, "../../..");
12
+ const benchmarkRoot = path.join(repoRoot, "examples", "board-contract-lab");
13
+ const thingsInRingsRoot = path.join(repoRoot, "examples", "things-in-rings");
14
+ const pnpmBinary = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
15
+ const miseBinary = process.platform === "win32" ? "mise.cmd" : "mise";
16
+ const WORKSPACE_NODE_MODULES = path.join(
17
+ repoRoot,
18
+ "apps",
19
+ "dreamboard-cli",
20
+ "node_modules",
21
+ );
22
+ const UI_SDK_NODE_MODULES = path.join(
23
+ repoRoot,
24
+ "packages",
25
+ "ui-sdk",
26
+ "node_modules",
27
+ );
28
+ const SDK_TYPES_PACKAGE_ROOT = path.join(repoRoot, "packages", "sdk-types");
29
+ const EXPECTED_UI_FRAMEWORK_TSCONFIG = `{
30
+ "compilerOptions": {
31
+ "target": "ES2020",
32
+ "module": "ESNext",
33
+ "moduleResolution": "bundler",
34
+ "jsx": "react-jsx",
35
+ "strict": true,
36
+ "esModuleInterop": true,
37
+ "skipLibCheck": true,
38
+ "paths": {
39
+ "@dreamboard/manifest-contract": [
40
+ "../shared/manifest-contract.ts"
41
+ ],
42
+ "@dreamboard/ui-contract": [
43
+ "../shared/generated/ui-contract.ts"
44
+ ],
45
+ "@shared/*": [
46
+ "../shared/*"
47
+ ]
48
+ }
49
+ },
50
+ "include": [
51
+ "./**/*.ts",
52
+ "./**/*.tsx",
53
+ "../shared/**/*.ts"
54
+ ],
55
+ "exclude": [
56
+ "node_modules",
57
+ "dist"
58
+ ]
59
+ }
60
+ `;
61
+
62
+ async function loadBenchmarkManifest(): Promise<GameTopologyManifest> {
63
+ return materializeManifest(benchmarkRoot) as Promise<GameTopologyManifest>;
64
+ }
65
+
66
+ async function ensureSymlink(targetPath: string, linkPath: string) {
67
+ try {
68
+ await lstat(linkPath);
69
+ return;
70
+ } catch {
71
+ // Fall through and create the symlink.
72
+ }
73
+
74
+ await mkdir(path.dirname(linkPath), { recursive: true });
75
+ await symlink(
76
+ targetPath,
77
+ linkPath,
78
+ process.platform === "win32" ? "junction" : "dir",
79
+ );
80
+ }
81
+
82
+ function collectComponentHomeTypes(
83
+ manifest: GameTopologyManifest,
84
+ ): Set<string> {
85
+ const homes = new Set<string>();
86
+
87
+ for (const cardSet of manifest.cardSets) {
88
+ if (cardSet.type !== "manual") {
89
+ continue;
90
+ }
91
+ for (const card of cardSet.cards) {
92
+ if (card.home?.type) {
93
+ homes.add(card.home.type);
94
+ }
95
+ }
96
+ }
97
+
98
+ for (const component of [...manifest.pieceSeeds, ...manifest.dieSeeds]) {
99
+ if (component.home?.type) {
100
+ homes.add(component.home.type);
101
+ }
102
+ }
103
+
104
+ return homes;
105
+ }
106
+
107
+ function decodeOutput(buffer: Uint8Array<ArrayBufferLike>): string {
108
+ return new TextDecoder().decode(buffer);
109
+ }
110
+
111
+ function runNode20ExampleScript(exampleRoot: string, scriptName: string) {
112
+ return Bun.spawnSync({
113
+ cmd: [
114
+ miseBinary,
115
+ "exec",
116
+ "node@20",
117
+ "--",
118
+ pnpmBinary,
119
+ "--dir",
120
+ exampleRoot,
121
+ scriptName,
122
+ ],
123
+ cwd: repoRoot,
124
+ stdout: "pipe",
125
+ stderr: "pipe",
126
+ });
127
+ }
128
+
129
+ function throwTypecheckError(
130
+ label: string,
131
+ result: ReturnType<typeof Bun.spawnSync>,
132
+ ): never {
133
+ throw new Error(
134
+ [
135
+ `Typecheck failed for ${label}`,
136
+ "",
137
+ "stdout:",
138
+ decodeOutput(result.stdout),
139
+ "",
140
+ "stderr:",
141
+ decodeOutput(result.stderr),
142
+ ].join("\n"),
143
+ );
144
+ }
145
+
146
+ async function formatWithRepoPrettier(
147
+ relativePath: string,
148
+ content: string,
149
+ ): Promise<string> {
150
+ const tempRoot = await mkdtemp(
151
+ path.join(os.tmpdir(), "db-authoring-benchmark-"),
152
+ );
153
+ const tempFile = path.join(tempRoot, relativePath);
154
+
155
+ await Bun.write(tempFile, content);
156
+
157
+ try {
158
+ const result = Bun.spawnSync({
159
+ cmd: [
160
+ pnpmBinary,
161
+ "exec",
162
+ "prettier",
163
+ "--log-level",
164
+ "warn",
165
+ "--write",
166
+ tempFile,
167
+ ],
168
+ cwd: repoRoot,
169
+ stdout: "pipe",
170
+ stderr: "pipe",
171
+ });
172
+
173
+ if (result.exitCode !== 0) {
174
+ throw new Error(
175
+ [
176
+ `Prettier failed for generated benchmark file ${relativePath}`,
177
+ "",
178
+ "stdout:",
179
+ decodeOutput(result.stdout),
180
+ "",
181
+ "stderr:",
182
+ decodeOutput(result.stderr),
183
+ ].join("\n"),
184
+ );
185
+ }
186
+
187
+ return await readFile(tempFile, "utf8");
188
+ } finally {
189
+ await rm(tempRoot, { recursive: true, force: true });
190
+ }
191
+ }
192
+
193
+ test("board-contract-lab covers the stricter authored manifest surfaces", async () => {
194
+ const manifest = await loadBenchmarkManifest();
195
+ const validation = validateManifestAuthoring(manifest);
196
+
197
+ expect(validation.errors).toEqual([]);
198
+ expect(new Set(manifest.boards.map((board) => board.layout))).toEqual(
199
+ new Set(["generic", "hex", "square"]),
200
+ );
201
+ expect(new Set(manifest.boards.map((board) => board.scope))).toEqual(
202
+ new Set(["shared", "perPlayer"]),
203
+ );
204
+ expect(
205
+ new Set(
206
+ manifest.boards
207
+ .map((board) => board.templateId)
208
+ .filter((templateId): templateId is string => Boolean(templateId)),
209
+ ),
210
+ ).toEqual(
211
+ new Set(["market-template", "frontier-template", "player-mat-template"]),
212
+ );
213
+ expect(new Set(manifest.cardSets.map((cardSet) => cardSet.type))).toEqual(
214
+ new Set(["manual", "preset"]),
215
+ );
216
+ expect(collectComponentHomeTypes(manifest)).toEqual(
217
+ new Set(["zone", "space", "container", "edge", "vertex", "slot"]),
218
+ );
219
+ expect(manifest.setupOptions.map((option) => option.id)).toEqual([
220
+ "map",
221
+ "market",
222
+ "crew",
223
+ ]);
224
+ expect(manifest.setupProfiles.map((profile) => profile.id)).toEqual([
225
+ "standard-expedition",
226
+ "river-draft",
227
+ ]);
228
+ });
229
+
230
+ test("board-contract-lab authoritative generated files stay in sync with workspace-codegen", async () => {
231
+ const manifest = await loadBenchmarkManifest();
232
+ const authoritativeFiles = generateAuthoritativeFiles(manifest);
233
+
234
+ expect(Object.keys(authoritativeFiles).sort()).toEqual([
235
+ "app/index.ts",
236
+ "app/tsconfig.framework.json",
237
+ "shared/generated/ui-contract.ts",
238
+ "shared/manifest-contract.ts",
239
+ "shared/manifest-literals.ts",
240
+ "shared/manifest-runtime.ts",
241
+ "shared/manifest-types.ts",
242
+ "ui/tsconfig.framework.json",
243
+ ]);
244
+
245
+ for (const [relativePath, expected] of Object.entries(authoritativeFiles)) {
246
+ const filePath = path.join(benchmarkRoot, relativePath);
247
+ const actual = await readFile(filePath, "utf8");
248
+ const formattedExpected = await formatWithRepoPrettier(
249
+ relativePath,
250
+ expected,
251
+ );
252
+ expect(actual).toBe(formattedExpected);
253
+ }
254
+ }, 20_000);
255
+
256
+ test("authoritative ui framework tsconfig uses the scaffold format", async () => {
257
+ const authoritativeFiles = generateAuthoritativeFiles(
258
+ await loadBenchmarkManifest(),
259
+ );
260
+
261
+ expect(authoritativeFiles["ui/tsconfig.framework.json"]).toBe(
262
+ EXPECTED_UI_FRAMEWORK_TSCONFIG,
263
+ );
264
+ });
265
+
266
+ test("generated ui-contract.ts exposes a typed useHexBoardView wrapper", async () => {
267
+ const manifest = await loadBenchmarkManifest();
268
+ const authoritativeFiles = generateAuthoritativeFiles(manifest);
269
+
270
+ const uiContract = authoritativeFiles["shared/generated/ui-contract.ts"];
271
+ if (!uiContract) {
272
+ throw new Error(
273
+ "Missing shared/generated/ui-contract.ts in codegen output.",
274
+ );
275
+ }
276
+
277
+ // Imports static topology and exports the workspace-typed helper.
278
+ expect(uiContract).toContain("staticBoards");
279
+ expect(uiContract).toContain("createHexBoardView");
280
+ expect(uiContract).toContain("export function useHexBoardView");
281
+
282
+ // Constrains the boardId argument to the manifest's hex-board ids and
283
+ // narrows TSpaceView.id to that board's space-id type.
284
+ expect(uiContract).toContain("export type HexBoardId");
285
+ expect(uiContract).toContain("keyof typeof hexStaticBoards");
286
+ expect(uiContract).toContain("Id extends HexBoardId");
287
+ expect(uiContract).toContain("BoardSpaceIdOf");
288
+ });
289
+
290
+ test("generated ui-contract.ts exposes typed interaction form wrappers", async () => {
291
+ const manifest = await loadBenchmarkManifest();
292
+ const authoritativeFiles = generateAuthoritativeFiles(manifest);
293
+
294
+ const uiContract = authoritativeFiles["shared/generated/ui-contract.ts"];
295
+ if (!uiContract) {
296
+ throw new Error(
297
+ "Missing shared/generated/ui-contract.ts in codegen output.",
298
+ );
299
+ }
300
+
301
+ expect(uiContract).toContain("InteractionForm as InteractionFormGeneric");
302
+ expect(uiContract).toContain("export type InteractionFieldRenderers");
303
+ expect(uiContract).toContain(
304
+ "export type InteractionFormProps<Key extends InteractionKey>",
305
+ );
306
+ expect(uiContract).toContain("export function InteractionForm");
307
+ expect(uiContract).toContain("export function InteractionField");
308
+ });
309
+
310
+ test("generated ui-contract.ts exposes typed Radix-style primitives without GameShell wrappers", async () => {
311
+ const manifest = await loadBenchmarkManifest();
312
+ const authoritativeFiles = generateAuthoritativeFiles(manifest);
313
+
314
+ const uiContract = authoritativeFiles["shared/generated/ui-contract.ts"];
315
+ if (!uiContract) {
316
+ throw new Error(
317
+ "Missing shared/generated/ui-contract.ts in codegen output.",
318
+ );
319
+ }
320
+
321
+ expect(uiContract).toContain("export const UI = createDreamboardUI");
322
+ expect(uiContract).toContain("export const Interaction = UI.Interaction");
323
+ expect(uiContract).toContain("export const Prompt = UI.Prompt");
324
+ expect(uiContract).toContain("export const Zone = UI.Zone");
325
+ expect(uiContract).toContain("export const Board = UI.Board");
326
+ expect(uiContract).not.toContain("WorkspaceGameShell");
327
+ expect(uiContract).not.toContain("GameShell as GameShellGeneric");
328
+ expect(uiContract).not.toContain("defineMarketSurface");
329
+ expect(uiContract).not.toContain("definePlayerCardsSurface");
330
+ });
331
+
332
+ test("board-contract-lab manifest authoring typechecks", async () => {
333
+ await ensureSymlink(
334
+ WORKSPACE_NODE_MODULES,
335
+ path.join(benchmarkRoot, "node_modules"),
336
+ );
337
+ await ensureSymlink(
338
+ SDK_TYPES_PACKAGE_ROOT,
339
+ path.join(benchmarkRoot, "node_modules", "@dreamboard", "sdk-types"),
340
+ );
341
+ await ensureSymlink(
342
+ UI_SDK_NODE_MODULES,
343
+ path.join(benchmarkRoot, "ui", "node_modules"),
344
+ );
345
+
346
+ const result = runNode20ExampleScript(benchmarkRoot, "typecheck:manifest");
347
+
348
+ if (result.exitCode !== 0) {
349
+ throwTypecheckError(`${benchmarkRoot} manifest`, result);
350
+ }
351
+ }, 60_000);
352
+
353
+ test("generated app contracts typecheck under Node 20 without a larger heap", async () => {
354
+ const examples = [thingsInRingsRoot, benchmarkRoot];
355
+
356
+ for (const exampleRoot of examples) {
357
+ const result = runNode20ExampleScript(exampleRoot, "typecheck:app");
358
+ if (result.exitCode !== 0) {
359
+ throwTypecheckError(`${exampleRoot} app`, result);
360
+ }
361
+ }
362
+ }, 120_000);
@@ -0,0 +1,69 @@
1
+ import type { HexSpaceSpec, HexVertexRef } from "@dreamboard/sdk-types";
2
+
3
+ const HEX_CORNERS = ["ne-e", "e-se", "se-sw", "sw-w", "w-nw", "nw-ne"] as const;
4
+ type HexCorner = (typeof HEX_CORNERS)[number];
5
+
6
+ const HEX_CORNER_OFFSETS: Record<HexCorner, readonly [number, number, number]> =
7
+ {
8
+ "ne-e": [2, -1, -1],
9
+ "e-se": [1, -2, 1],
10
+ "se-sw": [-1, -1, 2],
11
+ "sw-w": [-2, 1, 1],
12
+ "w-nw": [-1, 2, -1],
13
+ "nw-ne": [1, 1, -2],
14
+ };
15
+
16
+ function cubeFromAxial(
17
+ space: Pick<HexSpaceSpec, "q" | "r">,
18
+ ): readonly [number, number, number] {
19
+ const x = space.q;
20
+ const z = space.r;
21
+ return [x, -x - z, z] as const;
22
+ }
23
+
24
+ function cornerGeometryKey(
25
+ space: Pick<HexSpaceSpec, "q" | "r">,
26
+ corner: HexCorner,
27
+ ): string {
28
+ const [x, y, z] = cubeFromAxial(space);
29
+ const [dx, dy, dz] = HEX_CORNER_OFFSETS[corner];
30
+ return `${3 * x + dx},${3 * y + dy},${3 * z + dz}`;
31
+ }
32
+
33
+ export function resolveHexVertexGeometryKey(
34
+ ref: HexVertexRef,
35
+ spacesById: ReadonlyMap<string, HexSpaceSpec>,
36
+ ): string {
37
+ const resolvedSpaces = [...ref.spaces]
38
+ .sort((left, right) => left.localeCompare(right))
39
+ .map((spaceId) => {
40
+ const space = spacesById.get(spaceId);
41
+ if (!space) {
42
+ throw new Error(
43
+ `Hex vertex ref references unknown space '${spaceId}'.`,
44
+ );
45
+ }
46
+ return space;
47
+ });
48
+ const keyCounts = new Map<string, number>();
49
+ for (const space of resolvedSpaces) {
50
+ for (const corner of HEX_CORNERS) {
51
+ const key = cornerGeometryKey(space, corner);
52
+ keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
53
+ }
54
+ }
55
+ const candidates = [...keyCounts.entries()]
56
+ .filter(([, count]) => count === resolvedSpaces.length)
57
+ .map(([key]) => key)
58
+ .sort((left, right) => left.localeCompare(right));
59
+ if (candidates.length !== 1) {
60
+ throw new Error(
61
+ `Hex vertex ref spaces '${ref.spaces.join(", ")}' do not resolve to exactly one shared vertex.`,
62
+ );
63
+ }
64
+ const vertexKey = candidates[0];
65
+ if (vertexKey === undefined) {
66
+ throw new Error("internal: expected exactly one hex vertex geometry key");
67
+ }
68
+ return vertexKey;
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { GameTopologyManifest } from "@dreamboard/sdk-types";
2
+ import {
3
+ generateManifestContractSources,
4
+ materializeManifestTable,
5
+ } from "./manifest-contract.js";
6
+ import { validateManifestAuthoring } from "./manifest-validation.js";
7
+ import {
8
+ AUTHORITATIVE_GENERATED_FILES,
9
+ isAllowedGamePath,
10
+ isAuthoritativeGeneratedPath,
11
+ isCliStaticPath,
12
+ isDynamicSeedPath,
13
+ isLibraryPath,
14
+ PRESERVED_USER_FILES,
15
+ SEED_FILES,
16
+ SEED_FILE_PATTERNS,
17
+ WORKSPACE_CODEGEN_OWNERSHIP,
18
+ } from "./ownership.js";
19
+ import {
20
+ generateFrameworkFiles,
21
+ generateSeedFiles,
22
+ isFrameworkOwnedSetupProfilesSeed,
23
+ SETUP_PROFILES_SEED_MARKER,
24
+ } from "./seeds.js";
25
+ export {
26
+ materializeCardSet,
27
+ materializePresetCardSet,
28
+ } from "./preset-card-sets.js";
29
+ export { materializeManifestTable };
30
+
31
+ export {
32
+ AUTHORITATIVE_GENERATED_FILES,
33
+ isAllowedGamePath,
34
+ isAuthoritativeGeneratedPath,
35
+ isCliStaticPath,
36
+ isDynamicSeedPath,
37
+ isLibraryPath,
38
+ PRESERVED_USER_FILES,
39
+ SEED_FILES,
40
+ SEED_FILE_PATTERNS,
41
+ WORKSPACE_CODEGEN_OWNERSHIP,
42
+ };
43
+
44
+ export function generateAuthoritativeFiles(
45
+ manifest: GameTopologyManifest,
46
+ ): Record<string, string> {
47
+ return {
48
+ ...generateManifestContractSources(manifest),
49
+ ...generateFrameworkFiles(manifest),
50
+ };
51
+ }
52
+
53
+ export { generateSeedFiles };
54
+ export { isFrameworkOwnedSetupProfilesSeed, SETUP_PROFILES_SEED_MARKER };
55
+ export { validateManifestAuthoring };
56
+
57
+ export function generateDynamicGeneratedFiles(
58
+ manifest: GameTopologyManifest,
59
+ ): Record<string, string> {
60
+ return {
61
+ ...generateAuthoritativeFiles(manifest),
62
+ ...generateSeedFiles(manifest),
63
+ };
64
+ }