@dreamboard-games/cli 0.1.30-alpha.16 → 0.1.30-alpha.18

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 (80) hide show
  1. package/README.md +3 -1
  2. package/dist/agent-verifier/agent-workspace-verifier.mjs +12 -12
  3. package/dist/agent-verifier/{chunk-LKQ557TJ.mjs → chunk-334H4LE4.mjs} +3 -3
  4. package/dist/agent-verifier/{chunk-H3XNWKJU.mjs → chunk-7LFDFXLS.mjs} +2 -2
  5. package/dist/agent-verifier/{chunk-DWLTCUUX.mjs → chunk-7MAOGFFP.mjs} +6 -6
  6. package/dist/agent-verifier/{chunk-CO3BRUD6.mjs → chunk-AG5J3SMN.mjs} +11 -3
  7. package/dist/agent-verifier/chunk-AG5J3SMN.mjs.map +1 -0
  8. package/dist/agent-verifier/{chunk-AXXUGU7Q.mjs → chunk-AQ6UQHPT.mjs} +4 -30
  9. package/dist/agent-verifier/chunk-AQ6UQHPT.mjs.map +1 -0
  10. package/dist/agent-verifier/{chunk-A67WUYN2.mjs → chunk-B42OHJNY.mjs} +3 -3
  11. package/dist/agent-verifier/{chunk-V6AQDR7W.mjs → chunk-HUBV22JQ.mjs} +3 -3
  12. package/dist/agent-verifier/chunk-HUBV22JQ.mjs.map +1 -0
  13. package/dist/agent-verifier/{chunk-5GCZZ6NW.mjs → chunk-JB7VXCMB.mjs} +2 -2
  14. package/dist/agent-verifier/{chunk-WAFBU5U7.mjs → chunk-OJFZVGEL.mjs} +38 -13
  15. package/dist/agent-verifier/chunk-OJFZVGEL.mjs.map +1 -0
  16. package/dist/agent-verifier/{chunk-G2ECODRB.mjs → chunk-PLXXH5LY.mjs} +2 -2
  17. package/dist/agent-verifier/{chunk-655VJLXA.mjs → chunk-PWPOLHTW.mjs} +9 -12
  18. package/dist/agent-verifier/chunk-PWPOLHTW.mjs.map +1 -0
  19. package/dist/agent-verifier/{chunk-DPYC2NDB.mjs → chunk-RCYO6HWW.mjs} +2 -2
  20. package/dist/agent-verifier/{chunk-NFL3Z4Z7.mjs → chunk-RP7ZWFVH.mjs} +12 -8
  21. package/dist/agent-verifier/{chunk-NFL3Z4Z7.mjs.map → chunk-RP7ZWFVH.mjs.map} +1 -1
  22. package/dist/agent-verifier/{compile-4VSMC275.mjs → compile-VOBO2I6D.mjs} +12 -12
  23. package/dist/agent-verifier/{global-config-6UGFPLDA.mjs → global-config-L7PLLUK5.mjs} +3 -3
  24. package/dist/agent-verifier/{keychain-backend-BQLW5VEC.mjs → keychain-backend-UF3Z26JM.mjs} +1 -1
  25. package/dist/agent-verifier/keychain-backend-UF3Z26JM.mjs.map +1 -0
  26. package/dist/agent-verifier/{local-files-WPHUV6GU.mjs → local-files-DAFIR7SN.mjs} +4 -4
  27. package/dist/agent-verifier/{materialize-workspace-CV2JNXLU.mjs → materialize-workspace-PAC75NSP.mjs} +6 -6
  28. package/dist/agent-verifier/{reducer-native-test-harness-GY2CCQWN.mjs → reducer-native-test-harness-HSXRUGOR.mjs} +8 -8
  29. package/dist/agent-verifier/{static-scaffold-DJJRKMNB.mjs → static-scaffold-KSOTKJNQ.mjs} +4 -4
  30. package/dist/agent-verifier/{sync-YWK4SPWV.mjs → sync-MQJJEZAA.mjs} +13 -13
  31. package/dist/agent-verifier/{test-LQAGEQLY.mjs → test-R6HC6CYZ.mjs} +11 -11
  32. package/dist/agent-verifier/{workspace-codegen-4IWICKLB.mjs → workspace-codegen-SPPVHURX.mjs} +3 -3
  33. package/dist/authoring-compatibility-internal.js +1 -1
  34. package/dist/{chunk-2R4L2YDX.js → chunk-2WB3DYW4.js} +17 -8
  35. package/dist/chunk-2WB3DYW4.js.map +1 -0
  36. package/dist/{chunk-PW7D2W5S.js → chunk-2XMBZPL5.js} +45 -23
  37. package/dist/{chunk-PW7D2W5S.js.map → chunk-2XMBZPL5.js.map} +1 -1
  38. package/dist/{chunk-AVOAT522.js → chunk-J3CWZHY7.js} +4 -30
  39. package/dist/chunk-J3CWZHY7.js.map +1 -0
  40. package/dist/{global-config-NLGAFSRU.js → global-config-VQWFTIAV.js} +2 -2
  41. package/dist/index.js +5 -5
  42. package/dist/internal.js +3 -3
  43. package/dist/{keychain-backend-47LZ5IX5.js → keychain-backend-GO34KGTG.js} +1 -1
  44. package/dist/keychain-backend-GO34KGTG.js.map +1 -0
  45. package/package.json +1 -1
  46. package/release/authoring-release-set.json +4 -4
  47. package/skills/dreamboard/SKILL.md +8 -0
  48. package/skills/dreamboard/references/building-your-first-game.md +37 -15
  49. package/skills/dreamboard/references/canonical-concepts.md +74 -0
  50. package/skills/dreamboard/references/game-interface.md +15 -2
  51. package/skills/dreamboard/references/manifest-authoring.md +8 -0
  52. package/skills/dreamboard/references/reducer.md +47 -2
  53. package/skills/dreamboard/references/rule-authoring.md +10 -0
  54. package/skills/dreamboard/references/testing.md +7 -3
  55. package/dist/agent-verifier/chunk-655VJLXA.mjs.map +0 -1
  56. package/dist/agent-verifier/chunk-AXXUGU7Q.mjs.map +0 -1
  57. package/dist/agent-verifier/chunk-CO3BRUD6.mjs.map +0 -1
  58. package/dist/agent-verifier/chunk-V6AQDR7W.mjs.map +0 -1
  59. package/dist/agent-verifier/chunk-WAFBU5U7.mjs.map +0 -1
  60. package/dist/agent-verifier/keychain-backend-BQLW5VEC.mjs.map +0 -1
  61. package/dist/chunk-2R4L2YDX.js.map +0 -1
  62. package/dist/chunk-AVOAT522.js.map +0 -1
  63. package/dist/keychain-backend-47LZ5IX5.js.map +0 -1
  64. /package/dist/agent-verifier/{chunk-LKQ557TJ.mjs.map → chunk-334H4LE4.mjs.map} +0 -0
  65. /package/dist/agent-verifier/{chunk-H3XNWKJU.mjs.map → chunk-7LFDFXLS.mjs.map} +0 -0
  66. /package/dist/agent-verifier/{chunk-DWLTCUUX.mjs.map → chunk-7MAOGFFP.mjs.map} +0 -0
  67. /package/dist/agent-verifier/{chunk-A67WUYN2.mjs.map → chunk-B42OHJNY.mjs.map} +0 -0
  68. /package/dist/agent-verifier/{chunk-5GCZZ6NW.mjs.map → chunk-JB7VXCMB.mjs.map} +0 -0
  69. /package/dist/agent-verifier/{chunk-G2ECODRB.mjs.map → chunk-PLXXH5LY.mjs.map} +0 -0
  70. /package/dist/agent-verifier/{chunk-DPYC2NDB.mjs.map → chunk-RCYO6HWW.mjs.map} +0 -0
  71. /package/dist/agent-verifier/{compile-4VSMC275.mjs.map → compile-VOBO2I6D.mjs.map} +0 -0
  72. /package/dist/agent-verifier/{global-config-6UGFPLDA.mjs.map → global-config-L7PLLUK5.mjs.map} +0 -0
  73. /package/dist/agent-verifier/{local-files-WPHUV6GU.mjs.map → local-files-DAFIR7SN.mjs.map} +0 -0
  74. /package/dist/agent-verifier/{materialize-workspace-CV2JNXLU.mjs.map → materialize-workspace-PAC75NSP.mjs.map} +0 -0
  75. /package/dist/agent-verifier/{reducer-native-test-harness-GY2CCQWN.mjs.map → reducer-native-test-harness-HSXRUGOR.mjs.map} +0 -0
  76. /package/dist/agent-verifier/{static-scaffold-DJJRKMNB.mjs.map → static-scaffold-KSOTKJNQ.mjs.map} +0 -0
  77. /package/dist/agent-verifier/{sync-YWK4SPWV.mjs.map → sync-MQJJEZAA.mjs.map} +0 -0
  78. /package/dist/agent-verifier/{test-LQAGEQLY.mjs.map → test-R6HC6CYZ.mjs.map} +0 -0
  79. /package/dist/agent-verifier/{workspace-codegen-4IWICKLB.mjs.map → workspace-codegen-SPPVHURX.mjs.map} +0 -0
  80. /package/dist/{global-config-NLGAFSRU.js.map → global-config-VQWFTIAV.js.map} +0 -0
@@ -15,14 +15,6 @@ import path2 from "path";
15
15
  import os from "os";
16
16
  import path from "path";
17
17
  import { promises as fs } from "fs";
18
-
19
- // src/build-target.ts
20
- var injectedBuildChannel = true ? "published" : void 0;
21
- var BUILD_CHANNEL = injectedBuildChannel === "published" ? "published" : "development";
22
- var IS_PUBLISHED_BUILD = BUILD_CHANNEL === "published";
23
- var PUBLISHED_ENVIRONMENT = "prod";
24
-
25
- // src/config/credential-store.ts
26
18
  function getCredentialFilePath() {
27
19
  return path.join(os.homedir(), PROJECT_DIR_NAME, "auth.json");
28
20
  }
@@ -121,19 +113,6 @@ var migrationCompleted = false;
121
113
  var backendResolver = defaultBackendResolver;
122
114
  async function defaultBackendResolver() {
123
115
  const override = (process.env.DREAMBOARD_CREDENTIAL_BACKEND ?? "").trim().toLowerCase();
124
- if (IS_PUBLISHED_BUILD) {
125
- if (override && override !== "keychain" && override !== "auto") {
126
- throw new CredentialStoreUnavailableError(
127
- "published builds require the OS credential store"
128
- );
129
- }
130
- const { tryKeychainBackend: tryKeychainBackend2 } = await import("./keychain-backend-47LZ5IX5.js");
131
- const keychain2 = await tryKeychainBackend2();
132
- if (keychain2.available) {
133
- return keychain2.backend;
134
- }
135
- throw new CredentialStoreUnavailableError(keychain2.reason);
136
- }
137
116
  if (override === "file") {
138
117
  return fileCredentialBackend;
139
118
  }
@@ -146,7 +125,7 @@ async function defaultBackendResolver() {
146
125
  if (!useKeychain) {
147
126
  return fileCredentialBackend;
148
127
  }
149
- const { tryKeychainBackend } = await import("./keychain-backend-47LZ5IX5.js");
128
+ const { tryKeychainBackend } = await import("./keychain-backend-GO34KGTG.js");
150
129
  const keychain = await tryKeychainBackend();
151
130
  if (keychain.available) {
152
131
  return keychain.backend;
@@ -155,7 +134,7 @@ async function defaultBackendResolver() {
155
134
  }
156
135
  async function readCredentialBackendPreference() {
157
136
  try {
158
- const { loadGlobalConfig: loadGlobalConfig2 } = await import("./global-config-NLGAFSRU.js");
137
+ const { loadGlobalConfig: loadGlobalConfig2 } = await import("./global-config-VQWFTIAV.js");
159
138
  const config = await loadGlobalConfig2();
160
139
  return config.credentialBackend === "keychain";
161
140
  } catch {
@@ -166,9 +145,7 @@ async function getCredentialBackend() {
166
145
  if (cachedBackend === null) {
167
146
  cachedBackend = await backendResolver();
168
147
  if (!migrationCompleted && cachedBackend.name !== "file") {
169
- await migrateFromFileBackendIfNeeded(cachedBackend, {
170
- failClosed: IS_PUBLISHED_BUILD
171
- });
148
+ await migrateFromFileBackendIfNeeded(cachedBackend);
172
149
  }
173
150
  migrationCompleted = true;
174
151
  }
@@ -306,9 +283,6 @@ async function saveGlobalConfig(config) {
306
283
  }
307
284
 
308
285
  export {
309
- BUILD_CHANNEL,
310
- IS_PUBLISHED_BUILD,
311
- PUBLISHED_ENVIRONMENT,
312
286
  getActiveCredentialBackendName,
313
287
  getStoredSession,
314
288
  setCredentials,
@@ -320,4 +294,4 @@ export {
320
294
  loadGlobalConfig,
321
295
  saveGlobalConfig
322
296
  };
323
- //# sourceMappingURL=chunk-AVOAT522.js.map
297
+ //# sourceMappingURL=chunk-J3CWZHY7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/global-config.ts","../src/config/credential-store.ts"],"sourcesContent":["import os from \"node:os\";\nimport path from \"node:path\";\nimport type { CredentialBackendPreference, GlobalConfig } from \"../types.js\";\nimport { PROJECT_DIR_NAME } from \"../constants.js\";\nimport { ensureDir, readJsonFile } from \"../utils/fs.js\";\nimport { atomicWriteFile } from \"../utils/atomic-file.js\";\nimport { getCredentialFilePath } from \"./credential-store.js\";\n\nfunction normalizeCredentialBackend(\n value: unknown,\n): CredentialBackendPreference | undefined {\n if (value === \"file\" || value === \"keychain\") return value;\n // Tolerate unknown / malformed values rather than refusing to load the\n // whole config - an unrecognised backend name should degrade to \"use\n // the default\" instead of locking the user out of their CLI.\n return undefined;\n}\n\nexport function getGlobalConfigPath(): string {\n return path.join(os.homedir(), PROJECT_DIR_NAME, \"config.json\");\n}\n\n/**\n * Path to the on-disk credential file used by the file backend of\n * `CredentialStore`. Re-exported here to avoid circular / ad-hoc imports\n * in UI surface (`auth status`, `config show`, etc).\n */\nexport function getGlobalAuthPath(): string {\n return getCredentialFilePath();\n}\n\n/**\n * Load non-credential CLI configuration.\n *\n * Note: this function used to also load `authToken` / `refreshToken`\n * from `auth.json` and flatten them onto `GlobalConfig`. That shape\n * enabled the refresh-token-wipe bug: `saveGlobalConfig({ ...config })`\n * without explicit auth fields erased the stored refresh token.\n *\n * Credentials are now owned exclusively by `CredentialStore`. Callers\n * that need them must import `getCredentials()` directly.\n */\nexport async function loadGlobalConfig(): Promise<GlobalConfig> {\n const config = await readJsonFile<GlobalConfig>(getGlobalConfigPath()).catch(\n () => ({}) as GlobalConfig,\n );\n return {\n environment: config.environment,\n credentialBackend: normalizeCredentialBackend(config.credentialBackend),\n };\n}\n\n/**\n * Persist non-credential CLI configuration.\n *\n * This function cannot write credentials, by construction: the\n * `GlobalConfig` type has no credential fields. Credentials must be\n * persisted through `setCredentials` / `clearCredentials` from\n * `credential-store.ts`.\n */\nexport async function saveGlobalConfig(config: GlobalConfig): Promise<void> {\n const configDir = path.join(os.homedir(), PROJECT_DIR_NAME);\n await ensureDir(configDir);\n const normalized: GlobalConfig = {\n environment: config.environment,\n credentialBackend: normalizeCredentialBackend(config.credentialBackend),\n };\n await atomicWriteFile(\n getGlobalConfigPath(),\n `${JSON.stringify(normalized, null, 2)}\\n`,\n { mode: 0o600 },\n );\n}\n","/**\n * Single writer for the long-lived Dreamboard session credentials.\n *\n * Design invariants (enforced at the type level and tested in\n * `credential-store.test.ts`):\n *\n * 1. This module is the ONLY place in the CLI that writes credentials to\n * disk or the OS keychain. `global-config.ts` used to own both the\n * config and the credentials via `saveGlobalConfig`, which made it\n * trivial to wipe a refresh token by accident. The `GlobalConfig` type\n * no longer carries credentials, so attempting to persist one through\n * the config path is a type error.\n *\n * 2. The mutating surface is intentionally narrow:\n * - `setCredentials(c)` for refreshable sessions (both tokens present)\n * - `setAccessOnlySession(accessToken)` for the `auth set` / `config set\n * --token` power-user path, which has no refresh token by\n * construction\n * - `clearCredentials()` wipes the file entirely\n * There is no \"partial update\" API. `Credentials` requires both\n * `accessToken` and `refreshToken`, so it is impossible to persist a\n * half-populated refreshable session.\n *\n * 3. Writes go through `atomicWriteFile` + `withFileLock`, so a crash or\n * interrupt during `dreamboard sync`/`compile` cannot leave `auth.json`\n * truncated, and parallel CLI invocations cannot clobber each other's\n * rotated refresh tokens.\n *\n * 4. The on-disk JSON shape for the file backend is kept backward\n * compatible: we continue to read/write `authToken` + `refreshToken`\n * so existing users are not forced to log in again after this change.\n * A newer `accessToken` key is also accepted for read to ease any\n * future format bump.\n *\n * 5. All builds default to the file backend. The OS keychain is an explicit\n * opt-in through config or `DREAMBOARD_CREDENTIAL_BACKEND=keychain`.\n */\n\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { promises as fs } from \"node:fs\";\nimport { PROJECT_DIR_NAME } from \"../constants.js\";\nimport {\n atomicWriteFile,\n withFileLock,\n type FileLockOptions,\n} from \"../utils/atomic-file.js\";\n\n/**\n * Fully refreshable session. `accessToken` is the Clerk OAuth bootstrap token\n * retained for refresh/exchange compatibility; ordinary API calls use\n * `dreamboardApiToken`.\n */\nexport type Credentials = {\n readonly accessToken: string;\n readonly refreshToken: string;\n readonly tokenExpiresAt?: string;\n readonly dreamboardApiToken?: string;\n readonly dreamboardApiExpiresAt?: string;\n readonly clerkOAuthIssuer?: string;\n readonly clerkOAuthClientId?: string;\n readonly clerkOAuthTokenUrl?: string;\n readonly environment?: string;\n};\n\n/**\n * Raw on-disk snapshot. Either or both fields may be present. The refresh\n * coordinator only acts on snapshots that have both tokens populated.\n */\nexport type StoredSessionSnapshot = {\n readonly accessToken?: string;\n readonly refreshToken?: string;\n readonly tokenExpiresAt?: string;\n readonly dreamboardApiToken?: string;\n readonly dreamboardApiExpiresAt?: string;\n readonly clerkOAuthIssuer?: string;\n readonly clerkOAuthClientId?: string;\n readonly clerkOAuthTokenUrl?: string;\n readonly environment?: string;\n};\n\nexport type CredentialBackendName = \"file\" | \"keychain\";\n\nexport type CredentialBackend = {\n readonly name: CredentialBackendName;\n read(): Promise<StoredSessionSnapshot | null>;\n writeFull(creds: Credentials): Promise<void>;\n writeAccessOnly(accessToken: string): Promise<void>;\n clear(): Promise<void>;\n};\n\nexport type CredentialLockOps = {\n readonly backendName: CredentialBackendName;\n read(): Promise<StoredSessionSnapshot | null>;\n writeFull(creds: Credentials): Promise<void>;\n writeAccessOnly(accessToken: string): Promise<void>;\n clear(): Promise<void>;\n};\n\ntype DiskShape = Partial<{\n clerkAccessToken: string;\n clerkAccessExpiresAt: string;\n accessToken: string;\n authToken: string;\n refreshToken: string;\n tokenExpiresAt: string;\n dreamboardApiToken: string;\n dreamboardApiExpiresAt: string;\n clerkOAuthIssuer: string;\n clerkOAuthClientId: string;\n clerkOAuthTokenUrl: string;\n environment: string;\n}>;\n\nexport function getCredentialFilePath(): string {\n return path.join(os.homedir(), PROJECT_DIR_NAME, \"auth.json\");\n}\n\nfunction getCredentialLockPath(): string {\n return `${getCredentialFilePath()}.lock`;\n}\n\nasync function fileRead(): Promise<StoredSessionSnapshot | null> {\n const filePath = getCredentialFilePath();\n let data: string;\n try {\n data = await fs.readFile(filePath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw err;\n }\n if (data.trim().length === 0) {\n return null;\n }\n let parsed: DiskShape;\n try {\n parsed = JSON.parse(data) as DiskShape;\n } catch {\n return null;\n }\n const accessToken =\n parsed.clerkAccessToken ?? parsed.accessToken ?? parsed.authToken;\n const refreshToken = parsed.refreshToken;\n if (!accessToken && !refreshToken) return null;\n return {\n accessToken: accessToken || undefined,\n refreshToken: refreshToken || undefined,\n tokenExpiresAt:\n parsed.clerkAccessExpiresAt || parsed.tokenExpiresAt || undefined,\n dreamboardApiToken: parsed.dreamboardApiToken || undefined,\n dreamboardApiExpiresAt: parsed.dreamboardApiExpiresAt || undefined,\n clerkOAuthIssuer: parsed.clerkOAuthIssuer || undefined,\n clerkOAuthClientId: parsed.clerkOAuthClientId || undefined,\n clerkOAuthTokenUrl: parsed.clerkOAuthTokenUrl || undefined,\n environment: parsed.environment || undefined,\n };\n}\n\nasync function writeFilePayload(payload: DiskShape): Promise<void> {\n await atomicWriteFile(\n getCredentialFilePath(),\n `${JSON.stringify(payload, null, 2)}\\n`,\n { mode: 0o600 },\n );\n}\n\nasync function fileWriteFull(creds: Credentials): Promise<void> {\n if (!creds.accessToken || !creds.refreshToken) {\n throw new Error(\n \"Refusing to persist credentials with an empty accessToken or refreshToken.\",\n );\n }\n await writeFilePayload({\n clerkAccessToken: creds.accessToken,\n refreshToken: creds.refreshToken,\n clerkAccessExpiresAt: creds.tokenExpiresAt,\n dreamboardApiToken: creds.dreamboardApiToken,\n dreamboardApiExpiresAt: creds.dreamboardApiExpiresAt,\n clerkOAuthIssuer: creds.clerkOAuthIssuer,\n clerkOAuthClientId: creds.clerkOAuthClientId,\n clerkOAuthTokenUrl: creds.clerkOAuthTokenUrl,\n environment: creds.environment,\n });\n}\n\nasync function fileWriteAccessOnly(accessToken: string): Promise<void> {\n if (!accessToken) {\n throw new Error(\"Refusing to persist an empty access token.\");\n }\n await writeFilePayload({ authToken: accessToken });\n}\n\nasync function fileClear(): Promise<void> {\n const filePath = getCredentialFilePath();\n try {\n await fs.unlink(filePath);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n}\n\nexport const fileCredentialBackend: CredentialBackend = {\n name: \"file\",\n read: fileRead,\n writeFull: fileWriteFull,\n writeAccessOnly: fileWriteAccessOnly,\n clear: fileClear,\n};\n\nexport type BackendResolver = () =>\n | CredentialBackend\n | Promise<CredentialBackend>;\n\nexport class CredentialStoreUnavailableError extends Error {\n readonly code = \"CREDENTIAL_STORE_UNAVAILABLE\";\n\n constructor(reason: string) {\n super(`Credential store unavailable: ${reason}`);\n this.name = \"CredentialStoreUnavailableError\";\n }\n}\n\nlet cachedBackend: CredentialBackend | null = null;\nlet migrationCompleted = false;\nlet backendResolver: BackendResolver = defaultBackendResolver;\n\n/**\n * Resolver precedence for all builds:\n *\n * 1. `DREAMBOARD_CREDENTIAL_BACKEND` env var (debugging / CI override).\n * - \"file\" -> force file\n * - \"keychain\" -> force keychain (falls back to file if the native\n * module or the OS keyring is unavailable)\n * - \"auto\" -> same as unset (use config)\n * - unknown -> throw so typos fail loud\n * 2. `credentialBackend` in `~/.dreamboard/config.json`.\n * - \"keychain\" -> opt in to the OS keychain (with file fallback)\n * - \"file\" / unset / malformed -> file\n * 3. Default: file backend.\n *\n * Keychain is opt-in because on macOS the OS login-keychain prompts for\n * the user's password the first time a new binary tries to write to an\n * item, and re-prompts whenever the Node binary signature changes. We\n * would rather ship a zero-prompt default and let users who care about\n * encrypted-at-rest storage enable it.\n *\n * The resolver is async because the keychain probe requires a dynamic\n * `@napi-rs/keyring` import.\n */\nasync function defaultBackendResolver(): Promise<CredentialBackend> {\n const override = (process.env.DREAMBOARD_CREDENTIAL_BACKEND ?? \"\")\n .trim()\n .toLowerCase();\n if (override === \"file\") {\n return fileCredentialBackend;\n }\n if (override && override !== \"keychain\" && override !== \"auto\") {\n // Fail loud on typos rather than silently falling back: this env\n // var exists specifically for users who are debugging auth issues\n // and need to know their override took effect.\n throw new Error(\n `Unknown DREAMBOARD_CREDENTIAL_BACKEND value \"${override}\" (expected \"file\", \"keychain\", or \"auto\").`,\n );\n }\n\n const useKeychain =\n override === \"keychain\" || (await readCredentialBackendPreference());\n if (!useKeychain) {\n return fileCredentialBackend;\n }\n\n const { tryKeychainBackend } = await import(\"./keychain-backend.js\");\n const keychain = await tryKeychainBackend();\n if (keychain.available) {\n return keychain.backend;\n }\n // The user explicitly asked for keychain but the platform can't\n // provide one (no libsecret on Linux, missing native module, etc).\n // Silently degrade to the file backend so the CLI stays usable; the\n // active backend is still visible through `dreamboard auth status`.\n return fileCredentialBackend;\n}\n\nasync function readCredentialBackendPreference(): Promise<boolean> {\n try {\n // Dynamic import to avoid a top-level cycle with `global-config.ts`\n // (which imports `getCredentialFilePath` from this module). Using\n // the async path keeps the cycle purely lazy.\n const { loadGlobalConfig } = await import(\"./global-config.js\");\n const config = await loadGlobalConfig();\n return config.credentialBackend === \"keychain\";\n } catch {\n // If the config file is unreadable or the dynamic import fails\n // (e.g. during early bootstrap), fall back to the file-backed\n // default rather than crashing credential lookups.\n return false;\n }\n}\n\n/**\n * Override which backend is used. Tests use this to inject in-memory\n * backends; production code uses the file-default resolver.\n */\nexport function setCredentialBackendResolver(resolver: BackendResolver): void {\n backendResolver = resolver;\n cachedBackend = null;\n migrationCompleted = false;\n}\n\nexport async function getCredentialBackend(): Promise<CredentialBackend> {\n if (cachedBackend === null) {\n cachedBackend = await backendResolver();\n // One-time migration: if we resolved to a non-file backend and\n // `auth.json` still has credentials from the old layout, copy them\n // over and remove the file. We only do this when the new backend is\n // empty, so repeated migrations cannot stomp a newer keychain\n // session with a stale file session.\n if (!migrationCompleted && cachedBackend.name !== \"file\") {\n await migrateFromFileBackendIfNeeded(cachedBackend);\n }\n migrationCompleted = true;\n }\n return cachedBackend;\n}\n\nasync function migrateFromFileBackendIfNeeded(\n target: CredentialBackend,\n options: { failClosed?: boolean } = {},\n): Promise<void> {\n try {\n const [onDisk, onTarget] = await Promise.all([\n fileCredentialBackend.read(),\n target.read(),\n ]);\n if (!onDisk) return;\n if (onTarget) {\n // Target already has a session - the user has already migrated.\n // Remove the file so it cannot get re-used accidentally.\n await fileCredentialBackend.clear();\n return;\n }\n if (onDisk.accessToken && onDisk.refreshToken) {\n const migrated: Credentials = {\n accessToken: onDisk.accessToken,\n refreshToken: onDisk.refreshToken,\n tokenExpiresAt: onDisk.tokenExpiresAt,\n dreamboardApiToken: onDisk.dreamboardApiToken,\n dreamboardApiExpiresAt: onDisk.dreamboardApiExpiresAt,\n clerkOAuthIssuer: onDisk.clerkOAuthIssuer,\n clerkOAuthClientId: onDisk.clerkOAuthClientId,\n clerkOAuthTokenUrl: onDisk.clerkOAuthTokenUrl,\n environment: onDisk.environment,\n };\n await target.writeFull(migrated);\n await verifyMigratedSession(target, migrated);\n } else if (onDisk.accessToken) {\n await target.writeAccessOnly(onDisk.accessToken);\n const migrated = await target.read();\n if (migrated?.accessToken !== onDisk.accessToken) {\n throw new Error(\"Credential migration verification failed.\");\n }\n } else {\n return;\n }\n await fileCredentialBackend.clear();\n } catch (error) {\n if (options.failClosed) {\n throw new CredentialStoreUnavailableError(\n error instanceof Error ? error.message : String(error),\n );\n }\n // Migration is best-effort. A failure here should not block CLI\n // operation; on next run the file backend is still consulted\n // directly because the keychain backend's `read` returns null and\n // callers fall through to \"missing session\" → login prompt.\n }\n}\n\nasync function verifyMigratedSession(\n target: CredentialBackend,\n expected: Credentials,\n): Promise<void> {\n const migrated = await target.read();\n if (\n migrated?.accessToken !== expected.accessToken ||\n migrated.refreshToken !== expected.refreshToken\n ) {\n throw new Error(\"Credential migration verification failed.\");\n }\n}\n\nexport async function getActiveCredentialBackendName(): Promise<CredentialBackendName> {\n const backend = await getCredentialBackend();\n return backend.name;\n}\n\n/** Loose read: returns whatever is on disk, including access-only sessions. */\nexport async function getStoredSession(): Promise<StoredSessionSnapshot | null> {\n if (process.env.DREAMBOARD_AGENT_TOKEN?.trim()) {\n return null;\n }\n const backend = await getCredentialBackend();\n return backend.read();\n}\n\n/** Strict read: returns a refreshable pair, or null if either token is missing. */\nexport async function getCredentials(): Promise<Credentials | null> {\n const snapshot = await getStoredSession();\n if (!snapshot) return null;\n const { accessToken, refreshToken } = snapshot;\n if (!accessToken || !refreshToken) return null;\n return {\n accessToken,\n refreshToken,\n tokenExpiresAt: snapshot.tokenExpiresAt,\n dreamboardApiToken: snapshot.dreamboardApiToken,\n dreamboardApiExpiresAt: snapshot.dreamboardApiExpiresAt,\n clerkOAuthIssuer: snapshot.clerkOAuthIssuer,\n clerkOAuthClientId: snapshot.clerkOAuthClientId,\n clerkOAuthTokenUrl: snapshot.clerkOAuthTokenUrl,\n environment: snapshot.environment,\n };\n}\n\nexport async function setCredentials(creds: Credentials): Promise<void> {\n await withFileLock(getCredentialLockPath(), async () => {\n const backend = await getCredentialBackend();\n await backend.writeFull(creds);\n });\n}\n\nexport async function setAccessOnlySession(accessToken: string): Promise<void> {\n await withFileLock(getCredentialLockPath(), async () => {\n const backend = await getCredentialBackend();\n await backend.writeAccessOnly(accessToken);\n });\n}\n\nexport async function clearCredentials(): Promise<void> {\n await withFileLock(getCredentialLockPath(), async () => {\n const backend = await getCredentialBackend();\n await backend.clear();\n });\n}\n\n/**\n * Run `fn` while holding the cross-process credential lock. `fn` receives\n * an ops handle that reads/writes the active backend without re-acquiring\n * the lock (avoiding deadlock).\n *\n * This is the only correct way to perform a read-modify-write on stored\n * credentials (e.g. CLI refresh rotation) in the presence of\n * concurrent CLI invocations.\n */\nexport async function withCredentialLock<T>(\n fn: (ops: CredentialLockOps) => Promise<T>,\n options?: FileLockOptions,\n): Promise<T> {\n return withFileLock(\n getCredentialLockPath(),\n async () => {\n const backend = await getCredentialBackend();\n const ops: CredentialLockOps = {\n backendName: backend.name,\n read: () => backend.read(),\n writeFull: (creds) => backend.writeFull(creds),\n writeAccessOnly: (accessToken) => backend.writeAccessOnly(accessToken),\n clear: () => backend.clear(),\n };\n return fn(ops);\n },\n options,\n );\n}\n\n/** Test-only reset of module state. Not exported through the barrel. */\nexport function _resetCredentialStoreForTests(): void {\n cachedBackend = null;\n migrationCompleted = false;\n backendResolver = defaultBackendResolver;\n}\n"],"mappings":"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","names":["os","path","loadGlobalConfig","path","os"]}
@@ -4,7 +4,7 @@ import {
4
4
  getGlobalConfigPath,
5
5
  loadGlobalConfig,
6
6
  saveGlobalConfig
7
- } from "./chunk-AVOAT522.js";
7
+ } from "./chunk-J3CWZHY7.js";
8
8
  import "./chunk-FFO2IJL3.js";
9
9
  import "./chunk-2H7UOFLK.js";
10
10
  export {
@@ -13,4 +13,4 @@ export {
13
13
  loadGlobalConfig,
14
14
  saveGlobalConfig
15
15
  };
16
- //# sourceMappingURL=global-config-NLGAFSRU.js.map
16
+ //# sourceMappingURL=global-config-VQWFTIAV.js.map
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CONFIG_FLAG_ARGS,
4
+ IS_PUBLISHED_BUILD,
4
5
  LOCAL_REGISTRY_URL,
6
+ PUBLISHED_ENVIRONMENT,
5
7
  STALE_CONTRACT_ARTIFACT_CODE,
6
8
  STALE_CONTRACT_ARTIFACT_EXIT_CODE,
7
9
  buildClerkAuthorizationUrl,
@@ -62,7 +64,7 @@ import {
62
64
  toDreamboardApiError,
63
65
  valueOrUndefined,
64
66
  waitForCompiledResultJobSdk
65
- } from "./chunk-2R4L2YDX.js";
67
+ } from "./chunk-2WB3DYW4.js";
66
68
  import {
67
69
  applyWorkspaceCodegen,
68
70
  assertCliStaticScaffoldComplete,
@@ -114,10 +116,8 @@ import {
114
116
  writeSnapshotFromFiles,
115
117
  writeSourceFiles,
116
118
  writeWorkspaceTextFile
117
- } from "./chunk-PW7D2W5S.js";
119
+ } from "./chunk-2XMBZPL5.js";
118
120
  import {
119
- IS_PUBLISHED_BUILD,
120
- PUBLISHED_ENVIRONMENT,
121
121
  clearCredentials,
122
122
  getActiveCredentialBackendName,
123
123
  getGlobalAuthPath,
@@ -127,7 +127,7 @@ import {
127
127
  saveGlobalConfig,
128
128
  setAccessOnlySession,
129
129
  setCredentials
130
- } from "./chunk-AVOAT522.js";
130
+ } from "./chunk-J3CWZHY7.js";
131
131
  import {
132
132
  DEFAULT_LOGIN_TIMEOUT_MS,
133
133
  MANIFEST_FILE,
package/dist/internal.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  resolveConfig,
11
11
  resolveProjectContext,
12
12
  shortHash
13
- } from "./chunk-2R4L2YDX.js";
13
+ } from "./chunk-2WB3DYW4.js";
14
14
  import {
15
15
  applyWorkspaceCodegen,
16
16
  loadManifest,
@@ -19,11 +19,11 @@ import {
19
19
  setLatestCompileAttempt,
20
20
  updateProjectState,
21
21
  writeSnapshot
22
- } from "./chunk-PW7D2W5S.js";
22
+ } from "./chunk-2XMBZPL5.js";
23
23
  import {
24
24
  getStoredSession,
25
25
  loadGlobalConfig
26
- } from "./chunk-AVOAT522.js";
26
+ } from "./chunk-J3CWZHY7.js";
27
27
  import {
28
28
  ENVIRONMENT_CONFIGS,
29
29
  readJsonFile,
@@ -137,4 +137,4 @@ export {
137
137
  _setKeyringModuleForTests,
138
138
  tryKeychainBackend
139
139
  };
140
- //# sourceMappingURL=keychain-backend-47LZ5IX5.js.map
140
+ //# sourceMappingURL=keychain-backend-GO34KGTG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/keychain-backend.ts"],"sourcesContent":["/**\n * OS keychain-backed `CredentialBackend` built on top of `@napi-rs/keyring`.\n *\n * Keychain is an optional storage backend. Users can opt in with\n * `credentialBackend: \"keychain\"` in `~/.dreamboard/config.json`\n * (see `credential-store.ts` for the resolver).\n * When enabled, it gives us:\n * - A refresh token encrypted at rest by the OS (Keychain on macOS,\n * Credential Vault on Windows, Secret Service on Linux).\n * - Protection against other processes running as the same user tailing\n * `~/.dreamboard/auth.json` to scrape the token.\n *\n * This module is loaded optionally: `@napi-rs/keyring` is declared as an\n * `optionalDependencies` entry. If the native binary or OS keyring is\n * unavailable, the resolver falls back to the file backend.\n *\n * One-time migration: when the active backend is keychain and `auth.json` still\n * has tokens, `credential-store.ts` copies them into the keychain, verifies the\n * keychain read, and deletes the file. This is the only path that intentionally\n * mutates both backends.\n */\n\nimport type {\n CredentialBackend,\n Credentials,\n StoredSessionSnapshot,\n} from \"./credential-store.js\";\n\n/** Keychain service id. Shared across all Dreamboard CLI builds. */\nconst KEYCHAIN_SERVICE = \"dreamboard-cli\";\n/**\n * Keychain account id. The `user@host` shape is conventional but we keep\n * it fixed for now because the CLI only cares about \"the session for this\n * OS user\", not per-process sessions.\n */\nconst KEYCHAIN_ACCOUNT = \"session\";\n\ntype EntryInstance = {\n setPassword(value: string): void;\n getPassword(): string | null | undefined;\n deletePassword(): boolean;\n};\n\ntype KeyringModule = {\n Entry: new (service: string, account: string) => EntryInstance;\n};\n\nlet cachedModule: KeyringModule | null | undefined;\n\nasync function loadKeyringModule(): Promise<KeyringModule | null> {\n if (cachedModule !== undefined) return cachedModule;\n try {\n // `@napi-rs/keyring` is an optional dependency. If the native binary is\n // missing on this platform the dynamic import throws; resolver policy in\n // credential-store decides whether that is fatal.\n const mod = (await import(\"@napi-rs/keyring\")) as unknown as KeyringModule;\n cachedModule = mod;\n } catch {\n cachedModule = null;\n }\n return cachedModule;\n}\n\nfunction keychainProbe(entry: EntryInstance): boolean {\n // Some platforms have the module installed but no accessible keyring\n // (e.g. headless Linux without DBus). Touch getPassword to verify we\n // can talk to the service without side effects.\n try {\n entry.getPassword();\n return true;\n } catch {\n return false;\n }\n}\n\ntype KeychainPayload = {\n clerkAccessToken?: string;\n accessToken?: string;\n refreshToken?: string;\n clerkAccessExpiresAt?: string;\n tokenExpiresAt?: string;\n dreamboardApiToken?: string;\n dreamboardApiExpiresAt?: string;\n clerkOAuthIssuer?: string;\n clerkOAuthClientId?: string;\n clerkOAuthTokenUrl?: string;\n environment?: string;\n};\n\nfunction parsePayload(\n raw: string | null | undefined,\n): StoredSessionSnapshot | null {\n if (raw === null || raw === undefined) return null;\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n try {\n const parsed = JSON.parse(trimmed) as KeychainPayload;\n const accessToken = parsed.clerkAccessToken ?? parsed.accessToken;\n if (!accessToken && !parsed.refreshToken) return null;\n return {\n accessToken: accessToken || undefined,\n refreshToken: parsed.refreshToken || undefined,\n tokenExpiresAt:\n parsed.clerkAccessExpiresAt || parsed.tokenExpiresAt || undefined,\n dreamboardApiToken: parsed.dreamboardApiToken || undefined,\n dreamboardApiExpiresAt: parsed.dreamboardApiExpiresAt || undefined,\n clerkOAuthIssuer: parsed.clerkOAuthIssuer || undefined,\n clerkOAuthClientId: parsed.clerkOAuthClientId || undefined,\n clerkOAuthTokenUrl: parsed.clerkOAuthTokenUrl || undefined,\n environment: parsed.environment || undefined,\n };\n } catch {\n return null;\n }\n}\n\nfunction writeFull(entry: EntryInstance, creds: Credentials): void {\n if (!creds.accessToken || !creds.refreshToken) {\n throw new Error(\n \"Refusing to persist credentials with an empty accessToken or refreshToken.\",\n );\n }\n const payload: KeychainPayload = {\n clerkAccessToken: creds.accessToken,\n refreshToken: creds.refreshToken,\n clerkAccessExpiresAt: creds.tokenExpiresAt,\n dreamboardApiToken: creds.dreamboardApiToken,\n dreamboardApiExpiresAt: creds.dreamboardApiExpiresAt,\n clerkOAuthIssuer: creds.clerkOAuthIssuer,\n clerkOAuthClientId: creds.clerkOAuthClientId,\n clerkOAuthTokenUrl: creds.clerkOAuthTokenUrl,\n environment: creds.environment,\n };\n entry.setPassword(JSON.stringify(payload));\n}\n\nfunction writeAccessOnly(entry: EntryInstance, accessToken: string): void {\n if (!accessToken) {\n throw new Error(\"Refusing to persist an empty access token.\");\n }\n const payload: KeychainPayload = { accessToken };\n entry.setPassword(JSON.stringify(payload));\n}\n\nfunction clear(entry: EntryInstance): void {\n try {\n entry.deletePassword();\n } catch {\n // keyring-rs throws when the entry does not exist. That is fine -\n // `clearCredentials` contracts as idempotent.\n }\n}\n\nexport type KeychainAvailability =\n | { available: true; backend: CredentialBackend }\n | { available: false; reason: string };\n\n/**\n * Attempt to construct a keychain-backed `CredentialBackend`. Returns an\n * `available: false` result (with a reason) if the native module, the\n * OS keyring, or the probe fails.\n */\nexport async function tryKeychainBackend(): Promise<KeychainAvailability> {\n const mod = await loadKeyringModule();\n if (!mod) {\n return {\n available: false,\n reason: \"@napi-rs/keyring is not installed for this platform\",\n };\n }\n\n let entry: EntryInstance;\n try {\n entry = new mod.Entry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);\n } catch (err) {\n return {\n available: false,\n reason: `Failed to construct keyring entry: ${String((err as Error).message ?? err)}`,\n };\n }\n\n if (!keychainProbe(entry)) {\n return {\n available: false,\n reason: \"OS keyring is not accessible from this process\",\n };\n }\n\n const backend: CredentialBackend = {\n name: \"keychain\",\n async read() {\n try {\n return parsePayload(entry.getPassword());\n } catch (err) {\n const message = String((err as Error).message ?? err);\n // Transient keychain access errors (e.g. Touch ID prompt\n // cancelled) should not surface as \"session wiped\". Treat the\n // unreadable state as \"no session\" so the caller can fall back\n // to prompting for login.\n if (/no matching entry|not found/i.test(message)) {\n return null;\n }\n throw err;\n }\n },\n async writeFull(creds) {\n writeFull(entry, creds);\n },\n async writeAccessOnly(accessToken) {\n writeAccessOnly(entry, accessToken);\n },\n async clear() {\n clear(entry);\n },\n };\n return { available: true, backend };\n}\n\n/**\n * Test-only escape hatch so unit tests can install a fake keyring module\n * without going through the dynamic import cache.\n */\nexport function _setKeyringModuleForTests(mod: KeyringModule | null): void {\n cachedModule = mod;\n}\n\nexport function _resetKeyringModuleForTests(): void {\n cachedModule = undefined;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamboard-games/cli",
3
- "version": "0.1.30-alpha.16",
3
+ "version": "0.1.30-alpha.18",
4
4
  "description": "Design board games with AI and turn ideas into playable digital prototypes.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,11 +4,11 @@
4
4
  "packages": {
5
5
  "cli": {
6
6
  "name": "@dreamboard-games/cli",
7
- "version": "0.1.30-alpha.16"
7
+ "version": "0.1.30-alpha.18"
8
8
  },
9
9
  "sdk": {
10
10
  "name": "@dreamboard-games/sdk",
11
- "version": "0.4.0-alpha.4"
11
+ "version": "0.4.0-alpha.5"
12
12
  },
13
13
  "apiClient": {
14
14
  "name": "@dreamboard-games/api-client",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "devHost": {
18
18
  "name": "@dreamboard-games/dev-host",
19
- "version": "0.1.30-alpha.16"
19
+ "version": "0.1.30-alpha.18"
20
20
  }
21
21
  },
22
22
  "protocols": {
@@ -34,5 +34,5 @@
34
34
  "portable": true
35
35
  },
36
36
  "packageManager": "pnpm@10.4.1",
37
- "releaseSetId": "sha256:ef5945255c08ccf7dbbff9abbf77fdcc91546bdd3c5703671ef6c22052e23d3f"
37
+ "releaseSetId": "sha256:ef52cd34cbdc789fc58354981361c0ef516e4be7fef48c45a1e7534fd0b32c2e"
38
38
  }
@@ -40,6 +40,8 @@ See [tutorials/building-your-first-game.md](references/building-your-first-game.
40
40
  [references/game-interface.md](references/game-interface.md)
41
41
  - Testing:
42
42
  [references/testing.md](references/testing.md)
43
+ - Canonical concepts:
44
+ [references/canonical-concepts.md](references/canonical-concepts.md)
43
45
 
44
46
  ## Current Scaffold
45
47
 
@@ -99,6 +101,12 @@ Use this order by default:
99
101
  - Keep reducer-owned UI data in views; do not reintroduce the old `shared/ui-args.ts` pattern in new scaffolds.
100
102
  - When a game exposes clickable hands, markets, boards, or prompts, prove the same interaction works through `dreamboard dev` in a browser. A direct scenario submission can pass even when the rendered surface does not collect the input.
101
103
  - For interactive card hands, render generated surfaces such as `handSurface.Hand` and `handSurface.Card` consistently. Do not swap a surface card for a raw `Card` or custom tile based on `me.canAct`; the surface primitive is responsible for disabling unavailable interactions.
104
+ - For scorecards, grids, and compact tracks, use board topology and generated board surfaces. Do not invent a separate sheet model or duplicate cell state outside reducer authority.
105
+ - Derive inventory from `manifest.json` and generated manifest helpers instead of declaring cards, dice, pieces, resources, or boards twice.
106
+ - Use reducer-owned terminal outcomes for final results. Do not infer winners in UI from a `winnerPlayerId` convention or score sorting.
107
+ - Display descriptor availability and reducer-projected disabled reasons. Do not reimplement action legality in React.
108
+ - Model solo procedures and automa rivals as deterministic reducer phases, state transitions, and game events. Do not create fake player seats for non-human behavior.
109
+ - Start from the smallest matching canonical reference game before inventing framework structure. See [references/canonical-concepts.md](references/canonical-concepts.md).
102
110
 
103
111
  ## Editable Surface
104
112
 
@@ -162,7 +162,7 @@ const playerId = manifestContract.ids.playerId;
162
162
 
163
163
  const publicStateSchema = z.object({
164
164
  currentPlayerId: playerId,
165
- winnerPlayerId: playerId.nullable(),
165
+ gameOver: z.boolean(),
166
166
  lastRoll: z.number().int().min(1).max(6).nullable(),
167
167
  scores: z.record(playerId, z.number().int().nonnegative()),
168
168
  });
@@ -182,7 +182,7 @@ export const gameContract = defineGameContract({
182
182
  initial: {
183
183
  public: ({ playerIds }) => ({
184
184
  currentPlayerId: playerIds[0],
185
- winnerPlayerId: null,
185
+ gameOver: false,
186
186
  lastRoll: null,
187
187
  scores: Object.fromEntries(playerIds.map((playerId) => [playerId, 0])),
188
188
  }),
@@ -213,7 +213,7 @@ import type { GameContract } from "./game-contract";
213
213
  export const playerView = defineView<GameContract>()({
214
214
  schema: z.object({
215
215
  currentPlayerId: z.string(),
216
- winnerPlayerId: z.string().nullable(),
216
+ gameOver: z.boolean(),
217
217
  lastRoll: z.number().nullable(),
218
218
  scores: z.record(z.string(), z.number()),
219
219
  isMyTurn: z.boolean(),
@@ -223,10 +223,12 @@ export const playerView = defineView<GameContract>()({
223
223
  project({ state, playerId }) {
224
224
  return {
225
225
  currentPlayerId: state.publicState.currentPlayerId,
226
- winnerPlayerId: state.publicState.winnerPlayerId,
226
+ gameOver: state.publicState.gameOver,
227
227
  lastRoll: state.publicState.lastRoll,
228
228
  scores: state.publicState.scores,
229
- isMyTurn: state.publicState.currentPlayerId === playerId,
229
+ isMyTurn:
230
+ !state.publicState.gameOver &&
231
+ state.publicState.currentPlayerId === playerId,
230
232
  targetScore: 10,
231
233
  turnDieId: "turn-die",
232
234
  };
@@ -251,7 +253,7 @@ const TURN_DIE_ID = "turn-die" as const;
251
253
  const rollDie = defineAction<GameContract>()({
252
254
  params: z.object({}),
253
255
  validate({ state, input }) {
254
- if (state.publicState.winnerPlayerId) {
256
+ if (state.publicState.gameOver) {
255
257
  return {
256
258
  errorCode: "GAME_ALREADY_ENDED",
257
259
  message: "The game has already ended.",
@@ -267,7 +269,7 @@ const rollDie = defineAction<GameContract>()({
267
269
 
268
270
  return null;
269
271
  },
270
- reduce({ state, input, accept }) {
272
+ reduce({ state, input, accept, endGame }) {
271
273
  const nextRoll = (state.hiddenState.rollCount % 6) + 1;
272
274
  const nextScore = state.publicState.scores[input.playerId] + nextRoll;
273
275
  const nextScores = {
@@ -289,13 +291,13 @@ const rollDie = defineAction<GameContract>()({
289
291
  },
290
292
  };
291
293
 
292
- return accept({
294
+ const nextState = {
293
295
  ...state,
294
296
  table: nextTable,
295
297
  publicState: {
296
298
  ...state.publicState,
297
299
  currentPlayerId: nextScore >= TARGET_SCORE ? input.playerId : nextPlayerId,
298
- winnerPlayerId: nextScore >= TARGET_SCORE ? input.playerId : null,
300
+ gameOver: nextScore >= TARGET_SCORE,
299
301
  lastRoll: nextRoll,
300
302
  scores: nextScores,
301
303
  },
@@ -303,7 +305,27 @@ const rollDie = defineAction<GameContract>()({
303
305
  ...state.hiddenState,
304
306
  rollCount: state.hiddenState.rollCount + 1,
305
307
  },
306
- });
308
+ };
309
+
310
+ if (nextScore < TARGET_SCORE) {
311
+ return accept(nextState);
312
+ }
313
+
314
+ const orderedPlayerIds = Object.keys(nextScores).sort();
315
+ const outcome = {
316
+ reason: {
317
+ code: "TARGET_SCORE_REACHED",
318
+ message: `${input.playerId} reached ${TARGET_SCORE} points.`,
319
+ },
320
+ standings: orderedPlayerIds.map((playerId) => ({
321
+ playerId,
322
+ rank: playerId === input.playerId ? 1 : 2,
323
+ result: playerId === input.playerId ? "win" : "loss",
324
+ score: nextScores[playerId] ?? 0,
325
+ })),
326
+ } as const;
327
+
328
+ return endGame(nextState, outcome);
307
329
  },
308
330
  });
309
331
 
@@ -312,7 +334,7 @@ export const takeTurn = definePhase<GameContract>()({
312
334
  state: z.object({}),
313
335
  initialState: () => ({}),
314
336
  enter({ state, accept }) {
315
- if (state.publicState.winnerPlayerId) {
337
+ if (state.publicState.gameOver) {
316
338
  return accept(state);
317
339
  }
318
340
 
@@ -404,8 +426,8 @@ export default function App() {
404
426
 
405
427
  <p>Last roll: {view.lastRoll ?? "not rolled yet"}</p>
406
428
 
407
- {view.winnerPlayerId ? (
408
- <p>Winner: {view.winnerPlayerId}</p>
429
+ {view.gameOver ? (
430
+ <p>Game over. Check the result summary.</p>
409
431
  ) : (
410
432
  <button onClick={() => void rollDie()} disabled={!view.isMyTurn}>
411
433
  Roll die
@@ -464,8 +486,8 @@ export default defineScenario({
464
486
  expect(state.lastRoll).toBe(6);
465
487
  expect(state.scores["player-1"]).toBe(9);
466
488
  expect(state.scores["player-2"]).toBe(12);
467
- expect(state.winnerPlayerId).toBe("player-2");
468
- expect(view("player-2").winnerPlayerId).toBe("player-2");
489
+ expect(state.gameOver).toBe(true);
490
+ expect(view("player-2").gameOver).toBe(true);
469
491
  expect(history().accepted().length).toBe(6);
470
492
  },
471
493
  });
@@ -0,0 +1,74 @@
1
+ <!-- Generated by apps/dreamboard-cli/scripts/sync-skill-docs.ts. -->
2
+ <!-- Source: docs/reference/canonical-concepts.mdx -->
3
+
4
+ # Canonical concepts
5
+
6
+ How coding agents should choose Dreamboard concepts for common board-game patterns.
7
+
8
+ Coding agents should start from the smallest canonical concept that matches the
9
+ designer's rules. Do not invent a parallel model when the SDK already has an
10
+ authoritative one.
11
+
12
+ ## Choose the concept
13
+
14
+ | Designer asks for | Use | Avoid |
15
+ | --- | --- | --- |
16
+ | Roll-and-write scorecards, grids, or compact tracks | Board topology plus `Board.SquareGrid` for square spaces | A separate sheet model, duplicated cell state, or custom target protocol |
17
+ | Ranked winners, ties, score breakdowns, or tie-break evidence | `GameOutcome` with ordered standings | Inferring winners from ad hoc `winnerPlayerId` or score maps |
18
+ | Setup, phase, or action instructions | Reducer-projected guidance and descriptor help | Static UI copy that can drift from reducer authority |
19
+ | Unavailable choices | Descriptor availability and reducer-owned disabled reasons | Recomputing legal actions in React |
20
+ | Solo countdowns or environment procedures | Auto phases and deterministic `GameEvent` output | Fake player seats for the environment |
21
+ | Automa or rival behavior | Deterministic reducer state, system actions, and events | Authenticated bot users or non-human session actors |
22
+ | Existing cards, dice, pieces, boards, or resources | Manifest inventory and generated helpers | Declaring the same inventory again in reducer constants |
23
+
24
+ ## Reference examples
25
+
26
+ Use the closest shipped example before writing new architecture.
27
+
28
+ | Game family | Reference game | Main lesson |
29
+ | --- | --- | --- |
30
+ | Trick-taking | `hearts` | Follow-suit rules and hidden hands |
31
+ | Simultaneous drafting | `simultaneous-card-drafting` | Simultaneous choices and reveal timing |
32
+ | Deck-building market | `deck-building-market` | Shared market, costs, and deck zones |
33
+ | Worker placement | `worker-placement-tableau` | Occupied spaces, action slots, and cleanup |
34
+ | Route/network building | `hex-network-trading` | Hex boards, routes, and resource trades |
35
+ | Roll and write | `roll-and-write-scorecard` | Per-player square boards and mobile marks |
36
+ | Multiplayer ranking | `multiplayer-ranking-and-ties` | Standings, ties, and tie-break rows |
37
+ | Solo puzzle | `solo-countdown-puzzle` | Auto phases and deterministic environment events |
38
+ | Automa rival | `automa-river-rival` | Rival state/actions without a fake seat |
39
+
40
+ ## Scorecards and square boards
41
+
42
+ Model scorecards as boards. The manifest owns the stable square spaces; reducer
43
+ state owns marks, resources, dice results, and legal targets. UI renders the
44
+ board with generated surface bindings and `Board.SquareGrid`.
45
+
46
+ Do not add a second sheet schema for gameplay state. If a mark changes what a
47
+ player can do, it belongs in reducer state and must be submitted through the
48
+ normal action/collector path.
49
+
50
+ ## Outcomes
51
+
52
+ Use `GameOutcome` for terminal results. It carries a reason and ordered
53
+ standings, so ties, losses, scoreless results, score breakdowns, and tie-break
54
+ evidence all survive reducer, host, and product boundaries.
55
+
56
+ Do not decide the winner in UI by sorting scores. React can present standings,
57
+ but reducer authority owns the result.
58
+
59
+ ## Guidance and unavailable actions
60
+
61
+ The reducer should project what the player needs to know now: setup guidance,
62
+ phase objective, action help, and why an action is unavailable. The UI should
63
+ display those descriptors. It should not maintain a separate eligibility engine
64
+ or second set of validation messages.
65
+
66
+ ## Solo and automa procedures
67
+
68
+ Automated game behavior is still game logic. Implement deterministic
69
+ environment and rival steps as reducer phases, state transitions, and
70
+ `GameEvent` entries. Keep session actors for real humans only.
71
+
72
+ Use events to make automated behavior inspectable after reconnects and history
73
+ navigation. A replay should explain what happened without requiring hidden UI
74
+ state.
@@ -379,7 +379,7 @@ explicit props.
379
379
  | `ResourceCounter` | Resource totals |
380
380
  | `CostDisplay` | Cost breakdowns |
381
381
  | `PhaseIndicator` | Current phase or step status |
382
- | `GameEndDisplay` | Final rankings and scores |
382
+ | `OutcomeDialog` / `StandingsTable` | Reducer-owned terminal outcomes |
383
383
  | `DiceRoller` | Reducer-driven dice results |
384
384
  | `Drawer` and related exports | Mobile overflow or detail panels |
385
385
 
@@ -462,6 +462,9 @@ resource affordability, and loading state.
462
462
  />
463
463
  ```
464
464
 
465
+ Use reducer-projected descriptor help and availability reasons for disabled
466
+ actions. Do not disable a button by reimplementing reducer rules in React.
467
+
465
468
  ### `ActionPanel` and `ActionGroup`
466
469
 
467
470
  Use these to keep the action surface grouped by phase or intent.
@@ -502,7 +505,7 @@ data.
502
505
  | Export | Use it for |
503
506
  | --- | --- |
504
507
  | `TrackBoard` | Linear, circular, or branching tracks |
505
- | `SquareGrid` | Chess-like or tile-grid boards |
508
+ | `SquareGrid` / `Board.SquareGrid` | Chess-like, tile-grid, or roll-and-write square boards |
506
509
  | `HexGrid` | Hex maps with tiles, edges, and vertices |
507
510
  | `NetworkGraph` | Nodes, connections, and route graphs |
508
511
  | `ZoneMap` | Region or area control boards |
@@ -532,6 +535,16 @@ Board renderers are still UI code. The reducer should provide:
532
535
  - labels and annotations
533
536
  - grouped piece and card data in the shape the component expects
534
537
 
538
+ For scorecards, use generated board surfaces plus `Board.SquareGrid` instead
539
+ of a separate sheet model. The board topology remains the gameplay target; the
540
+ game UI chooses how to frame and style it.
541
+
542
+ ### OutcomeDialog and StandingsTable
543
+
544
+ Use outcome presentation components for `GameOutcome` data produced by the
545
+ reducer. They should receive the outcome payload and player labels; they should
546
+ not sort scores or decide winners themselves.
547
+
535
548
  ## Exported types
536
549
 
537
550
  Use these root exports when the UI needs to type reducer-owned runtime data.
@@ -14,6 +14,10 @@ manifest should define stable hex spaces such as `cell-01`, `cell-02`, and
14
14
  `cell-03`, while the reducer shuffles terrain pieces and number tokens onto
15
15
  those spaces at runtime.
16
16
 
17
+ For roll-and-write scorecards and compact tracks, use board topology rather
18
+ than a separate sheet model. The manifest defines the stable square spaces and
19
+ the reducer owns marks, dice results, legal targets, and scoring state.
20
+
17
21
  ## Top-level shape
18
22
 
19
23
  ```json
@@ -102,6 +106,10 @@ Use `home` on cards, piece seeds, and die seeds to place authored inventory.
102
106
  | `vertex` | `boardId`, `ref` | Place onto a hex vertex identified by three spaces |
103
107
  | `slot` | `hostComponentId`, `slotId` | Place into a component-owned slot |
104
108
 
109
+ Do not duplicate authored inventory in reducer constants. Cards, dice, pieces,
110
+ resources, zones, and boards should be read from the generated manifest helpers
111
+ or initialized through manifest-backed defaults.
112
+
105
113
  ### `ComponentVisibilitySpec`
106
114
 
107
115
  | Field | Required | Notes |