@vgai/engine 0.5.11 → 0.5.13

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 (152) hide show
  1. package/README.md +6 -5
  2. package/dist/adapter/authoring.d.ts +51 -28
  3. package/dist/adapter/authoring.d.ts.map +1 -1
  4. package/dist/adapter/body-marks.d.ts +59 -0
  5. package/dist/adapter/body-marks.d.ts.map +1 -0
  6. package/dist/adapter/body-marks.js +62 -0
  7. package/dist/adapter/constraint.d.ts +41 -0
  8. package/dist/adapter/constraint.d.ts.map +1 -0
  9. package/dist/adapter/constraint.js +27 -0
  10. package/dist/adapter/hierarchy-marks.d.ts +138 -0
  11. package/dist/adapter/hierarchy-marks.d.ts.map +1 -0
  12. package/dist/adapter/hierarchy-marks.js +148 -0
  13. package/dist/adapter/index.d.ts +1 -1
  14. package/dist/adapter/index.d.ts.map +1 -1
  15. package/dist/adapter/ingest/contract-system-adapters.d.ts +64 -0
  16. package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -0
  17. package/dist/adapter/ingest/contract-system-adapters.js +134 -0
  18. package/dist/adapter/ingest/game-contract.d.ts +87 -9
  19. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  20. package/dist/adapter/ingest/game-contract.js +7 -6
  21. package/dist/adapter/ingest/scene-capture.d.ts +31 -4
  22. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  23. package/dist/adapter/ingest/scene-capture.js +29 -7
  24. package/dist/adapter/ingest/structural-ids.d.ts +1 -1
  25. package/dist/adapter/ingest/structural-ids.js +2 -2
  26. package/dist/adapter/reflection-probe.d.ts +53 -0
  27. package/dist/adapter/reflection-probe.d.ts.map +1 -0
  28. package/dist/adapter/reflection-probe.js +18 -0
  29. package/dist/asset-formats/index.d.ts +4 -10
  30. package/dist/asset-formats/index.d.ts.map +1 -1
  31. package/dist/asset-formats/index.js +3 -8
  32. package/dist/asset-formats/material.d.ts +0 -66
  33. package/dist/asset-formats/material.d.ts.map +1 -1
  34. package/dist/asset-formats/material.js +0 -8
  35. package/dist/asset-formats/mesh.d.ts +1 -2
  36. package/dist/asset-formats/mesh.d.ts.map +1 -1
  37. package/dist/asset-formats/mesh.js +3 -13
  38. package/dist/asset-formats/render-env.d.ts +2 -2
  39. package/dist/asset-parse-error.d.ts +4 -5
  40. package/dist/asset-parse-error.d.ts.map +1 -1
  41. package/dist/asset-parse-error.js +4 -5
  42. package/dist/config.d.ts +7 -0
  43. package/dist/config.d.ts.map +1 -1
  44. package/dist/config.js +7 -0
  45. package/dist/data/curve.d.ts +116 -0
  46. package/dist/data/curve.d.ts.map +1 -0
  47. package/dist/data/curve.js +230 -0
  48. package/dist/data/vite-plugin-data.d.ts +2 -2
  49. package/dist/data/vite-plugin-data.d.ts.map +1 -1
  50. package/dist/data/vite-plugin-data.js +24 -15
  51. package/dist/dev/performance-profiler.d.ts +1 -0
  52. package/dist/dev/performance-profiler.d.ts.map +1 -1
  53. package/dist/dev/performance-profiler.js +25 -2
  54. package/dist/ecs/user-data.d.ts +49 -1
  55. package/dist/ecs/user-data.d.ts.map +1 -1
  56. package/dist/ecs/user-data.js +34 -0
  57. package/dist/index.d.ts +2 -3
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2 -3
  60. package/dist/manifest/index.d.ts +3 -3
  61. package/dist/manifest/index.d.ts.map +1 -1
  62. package/dist/manifest/index.js +2 -2
  63. package/dist/manifest/load.d.ts +15 -67
  64. package/dist/manifest/load.d.ts.map +1 -1
  65. package/dist/manifest/load.js +11 -103
  66. package/dist/manifest/schema.d.ts +18 -103
  67. package/dist/manifest/schema.d.ts.map +1 -1
  68. package/dist/manifest/schema.js +59 -179
  69. package/dist/pixi/index.d.ts +2 -3
  70. package/dist/pixi/index.d.ts.map +1 -1
  71. package/dist/pixi/index.js +2 -3
  72. package/dist/pixi/ingest.d.ts +7 -75
  73. package/dist/pixi/ingest.d.ts.map +1 -1
  74. package/dist/pixi/ingest.js +6 -14
  75. package/dist/pixi/scene-capture.d.ts +2 -2
  76. package/dist/pixi/scene-capture.js +2 -2
  77. package/dist/react/use-data.d.ts +10 -5
  78. package/dist/react/use-data.d.ts.map +1 -1
  79. package/dist/react/use-data.js +15 -15
  80. package/dist/render/viewport-shading.d.ts +1 -1
  81. package/dist/render/viewport-shading.d.ts.map +1 -1
  82. package/dist/render/viewport-shading.js +14 -0
  83. package/dist/runtime/create-runtime.d.ts +12 -0
  84. package/dist/runtime/create-runtime.d.ts.map +1 -1
  85. package/dist/runtime/create-runtime.js +7 -2
  86. package/dist/runtime/mount-game.d.ts +1 -1
  87. package/dist/runtime/mount-game.d.ts.map +1 -1
  88. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  89. package/dist/runtime/mount-manifest.js +5 -1
  90. package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
  91. package/dist/world3d-react/r3f-adapter.js +69 -39
  92. package/dist/world3d-react/renderer-config.d.ts +15 -0
  93. package/dist/world3d-react/renderer-config.d.ts.map +1 -1
  94. package/dist/world3d-react/renderer-config.js +15 -0
  95. package/dist/world3d-react/world-context.d.ts +2 -1
  96. package/dist/world3d-react/world-context.d.ts.map +1 -1
  97. package/dist-config/config.js +140 -20
  98. package/dist-config/config.js.map +4 -4
  99. package/package.json +1 -1
  100. package/schemas/engine-capabilities.json +10 -10
  101. package/schemas/vgai-project.schema.json +21 -76
  102. package/src/adapter/authoring.ts +54 -31
  103. package/src/adapter/body-marks.ts +69 -0
  104. package/src/adapter/constraint.ts +71 -0
  105. package/src/adapter/hierarchy-marks.ts +155 -0
  106. package/src/adapter/index.ts +2 -3
  107. package/src/adapter/ingest/contract-system-adapters.ts +174 -0
  108. package/src/adapter/ingest/game-contract.ts +98 -9
  109. package/src/adapter/ingest/scene-capture.ts +61 -11
  110. package/src/adapter/ingest/structural-ids.ts +2 -2
  111. package/src/adapter/reflection-probe.ts +75 -0
  112. package/src/asset-formats/index.ts +4 -10
  113. package/src/asset-formats/material.ts +0 -11
  114. package/src/asset-formats/mesh.ts +3 -16
  115. package/src/asset-parse-error.ts +4 -5
  116. package/src/config.ts +13 -0
  117. package/src/data/curve.ts +263 -0
  118. package/src/data/vite-plugin-data.ts +48 -14
  119. package/src/dev/performance-profiler.ts +21 -2
  120. package/src/ecs/user-data.ts +50 -0
  121. package/src/index.ts +2 -3
  122. package/src/manifest/index.ts +3 -15
  123. package/src/manifest/load.ts +27 -143
  124. package/src/manifest/schema.ts +65 -223
  125. package/src/pixi/index.ts +2 -15
  126. package/src/pixi/ingest.ts +13 -87
  127. package/src/pixi/scene-capture.ts +2 -2
  128. package/src/react/use-data.ts +15 -20
  129. package/src/render/viewport-shading.ts +16 -1
  130. package/src/runtime/create-runtime.ts +30 -5
  131. package/src/runtime/mount-game.ts +1 -1
  132. package/src/runtime/mount-manifest.ts +5 -1
  133. package/src/world3d-react/r3f-adapter.tsx +73 -39
  134. package/src/world3d-react/renderer-config.ts +31 -0
  135. package/src/world3d-react/world-context.ts +2 -1
  136. package/dist/asset-formats/instances.d.ts +0 -17
  137. package/dist/asset-formats/instances.d.ts.map +0 -1
  138. package/dist/asset-formats/instances.js +0 -31
  139. package/dist/asset-formats/parse.d.ts +0 -20
  140. package/dist/asset-formats/parse.d.ts.map +0 -1
  141. package/dist/asset-formats/parse.js +0 -34
  142. package/dist/pixi/ingest-iframe.d.ts +0 -82
  143. package/dist/pixi/ingest-iframe.d.ts.map +0 -1
  144. package/dist/pixi/ingest-iframe.js +0 -166
  145. package/dist/render/instance-mesh.d.ts +0 -5
  146. package/dist/render/instance-mesh.d.ts.map +0 -1
  147. package/dist/render/instance-mesh.js +0 -19
  148. package/schemas/mat.schema.json +0 -255
  149. package/src/asset-formats/instances.ts +0 -38
  150. package/src/asset-formats/parse.ts +0 -39
  151. package/src/pixi/ingest-iframe.ts +0 -255
  152. package/src/render/instance-mesh.ts +0 -25
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/data/data-ref.ts", "../src/data/data-check-core.ts", "../src/data/vite-plugin-data.ts", "../src/data/data-asset.ts"],
4
- "sourcesContent": ["/**\n * `file#key` refs \u2014 the resolvable half.\n *\n * Cross-asset references (`\"enemies#goblin\"`, `\"tuning#economy.slotGrowth\"`)\n * have been DETECTED and VALIDATED since W5 (`data-check-core.ts`'s\n * `DATA_REF_PATTERN` + `findDanglingDataRefs`, enforced by both `vgai doctor`\n * and the build plugin) \u2014 but nothing could actually RESOLVE one at runtime,\n * and nothing declared a field AS a ref in its schema (detection was by\n * string-shape convention alone). This module closes both halves:\n *\n * - {@link dataRef} \u2014 a typed Zod schema for a ref FIELD. Stamps the emitted\n * JSON Schema with a precise `pattern` (the target stem is baked into the\n * regex, so VS Code validates the prefix for free) and a machine-readable\n * `\"x-vgai-ref\": target` marker (a Zod `.meta()` that survives\n * `toDataJsonSchema`, including inside a table's `additionalProperties` row\n * schema \u2014 no emitter changes needed). The editor's Data panel\n * (`json-schema-fields.ts`'s `'ref'` kind) reads that marker to render a\n * `stem#key` PICKER instead of free text.\n * - {@link getRef} \u2014 lazy, read-time resolution. The caller names the target\n * HANDLE explicitly (`getRef(items, goblin.drop)`), so the resolved type is\n * the handle's own row type \u2014 no stem\u2192handle registry, no load-order\n * problem, no cycle hazard (nothing walks a graph; two refs resolved when\n * you choose to resolve them isn't a cycle), and it's HMR-correct for free\n * (it reads `target.get()` at CALL time, same doctrine as every other data\n * read \u2014 \"read every frame\", never cache across frames).\n *\n * Deliberately NOT built (and the design note this module's history carries):\n * eager resolution inside `defineData` (would change `get()`'s shipped output\n * type and entangle HMR \u2014 an edit to `enemies.data.json` would have to\n * re-notify every OTHER asset that references it), and a global\n * `resolveRef(addressString)` (needs a runtime stem\u2192handle registry that\n * `src/data/assets.ts` \u2014 side-effect-free, config-load-time, schemas-only by\n * its own module doc \u2014 must not become).\n */\n\nimport { z } from 'zod';\nimport type { DataHandle } from './data-asset';\nimport { DATA_REF_PATTERN } from './data-check-core';\n\n/** An asset-stem-tagged `file#key` string \u2014 `` `${Stem}#${string}` ``. */\nexport type DataRef<Stem extends string = string> = `${Stem}#${string}`;\n\n/** Prefix/key-path grammar a ref must match, parameterized by target stem (mirrors `DATA_REF_PATTERN` in `data-check-core.ts`, minus the alternation \u2014 this one is anchored to ONE target). */\nfunction refPattern(target: string): RegExp {\n return new RegExp(`^${target}#[A-Za-z0-9_-]+(?:\\\\.[A-Za-z0-9_-]+)*$`);\n}\n\n/**\n * A Zod schema for a data-asset FIELD that references another asset by\n * `\"<target>#<key>(.<field>)*\"`. Use it in a `.schema.ts`:\n *\n * ```ts\n * export const EnemyRowSchema = z.object({\n * name: z.string(),\n * drop: dataRef('items'), // -> \"items#potion\"\n * });\n * ```\n *\n * The emitted JSON Schema (`toDataJsonSchema`) carries both a precise\n * `pattern` (VS Code validates the target-stem prefix for free) and\n * `\"x-vgai-ref\": target` (the editor's Data panel picker keys off this).\n * Detection by `data-check-core.ts`'s conventional-string calibration still\n * applies at the VALUE level (a `dataRef` field's actual string still has to\n * look like `target#key` to resolve) \u2014 this only makes the field's INTENT\n * declared in the schema, closing the \u00A79.4 \"renamed target file silently\n * un-checks its refs\" hole for anything that adopts it (see\n * `collectDeclaredRefFields`/`findMissingRefTargets` in `data-check-core.ts`).\n */\nexport function dataRef<Stem extends string>(target: Stem): z.ZodType<DataRef<Stem>> {\n return z\n .string()\n .regex(refPattern(target), {\n message:\n `must be a \"${target}#<key>\" reference \u2014 ` +\n `expected the target asset stem \"${target}\", e.g. \"${target}#some-key\".`,\n })\n .meta({ 'x-vgai-ref': target }) as z.ZodType<DataRef<Stem>>;\n}\n\n/** First failing key-path segment, or `null` if the whole path resolves \u2014 same walk `data-check-core.ts`'s `firstUnresolvedSegment` does over parsed JSON, here over a live handle's value. */\nfunction firstMissingSegment(root: unknown, keyPath: readonly string[]): string | null {\n let node: unknown = root;\n for (const segment of keyPath) {\n if (node === null || typeof node !== 'object' || Array.isArray(node) || !(segment in node)) {\n return segment;\n }\n node = (node as Record<string, unknown>)[segment];\n }\n return null;\n}\n\n/**\n * Resolve a `\"target#key(.field)*\"` ref against the named target HANDLE,\n * read live (`target.get()` at call time \u2014 HMR-correct, no caching across\n * frames, same doctrine as any other data read).\n *\n * `ref`'s stem prefix should name `target`'s own asset \u2014 when `target` was\n * built with a `sourcePath` (every `defineData` call populates it), a\n * mismatch throws a teaching error instead of silently walking the wrong\n * handle's data. Without a `sourcePath` (a hand-rolled `DataHandle`, or an\n * older one built before this field existed) the mismatch check is skipped \u2014\n * resolution still proceeds against `target`, since the caller named it\n * explicitly and that's the actual contract.\n *\n * Throws a teaching error naming the ref and the first key-path segment that\n * doesn't resolve (a deleted row, a typo'd key) \u2014 never returns `undefined`\n * silently, matching `parseDataJson`'s \"errors teach\" doctrine (\u00A76.5).\n */\nexport function getRef<T>(target: DataHandle<Record<string, T>>, ref: string): T {\n const m = DATA_REF_PATTERN.exec(ref);\n if (!m) {\n throw new Error(\n `getRef: \"${ref}\" is not a \"file#key\" reference \u2014 ` +\n 'expected a string shaped like \"some-file#some-key\".',\n );\n }\n const refStem = m[1] as string;\n const keyPathStr = m[2] as string;\n const targetStem = target.sourcePath\n ?.split('\\\\')\n .join('/')\n .split('/')\n .pop()\n ?.replace(/\\.data\\.json$/, '');\n if (targetStem !== undefined && targetStem !== refStem) {\n throw new Error(\n `getRef: ref \"${ref}\" names target asset \"${refStem}\", but the handle passed in is ` +\n `\"${targetStem}\" (${target.sourcePath}). Pass the handle ` +\n `\"${refStem}#...\" actually names \u2014 getRef(${refStem}, ...), not getRef(${targetStem}, ...).`,\n );\n }\n const keyPath = keyPathStr.split('.');\n const root = target.get();\n const missing = firstMissingSegment(root, keyPath);\n if (missing !== null) {\n throw new Error(\n `getRef: dangling ref \"${ref}\" \u2014 \"${missing}\" does not exist` +\n (targetStem ? ` in ${targetStem}.data.json` : '') +\n '. Fix the key or remove the ref (refs are \"file#key\" strings).',\n );\n }\n let node: unknown = root;\n for (const segment of keyPath) node = (node as Record<string, unknown>)[segment];\n return node as T;\n}\n", "/**\n * Data-asset integrity core \u2014 the PURE half of W5 enforcement (\"dangling\n * `file#key` ref\" / \u00A76.7 build-path validation). No filesystem, no vite, no\n * zod: plain parsed-JSON in, findings out, so BOTH consumers \u2014 `vgai\n * doctor`'s data checks (`packages/editor/src/doctor/data-checks.ts`) and the\n * build-time plugin (`./vite-plugin-data.ts`) \u2014 share one definition of \"what\n * is a ref\" and \"what counts as dangling\" and can never drift apart.\n *\n * What is a ref (\u00A72.2): a cross-asset reference is a plain string\n * `\"file#key\"` \u2014 optionally with a field path, `\"tuning#economy.slotGrowth\"`\n * (file `#` key `.` field-path). There is no branded-ref Zod helper shipped\n * yet (W1 deferred it), so ref DETECTION is by convention, tuned against\n * false positives: a string is treated as a ref IFF it matches\n * {@link DATA_REF_PATTERN} AND its `file` prefix names a data asset that\n * actually exists in the project (`src/data/<file>.data.json`). Strings that\n * merely contain `#` \u2014 hex colors (`\"#ff0000\"`, no prefix), URLs with\n * fragments (`/` and `:` never match), musical pitches (`\"C#4\"` only matches\n * if a `C.data.json` exists) \u2014 never fire. The flip side is deliberate and\n * honest: a ref whose file prefix names a NONEXISTENT asset is\n * indistinguishable from an arbitrary string and is NOT flagged (\"error if\n * refs exist yet\" \u2014 the check only bites once the target file exists).\n *\n * `dataRef` (`./data-ref.ts`) now exists, closing that flip side for anyone\n * who adopts it: a `dataRef(target)` field's EMITTED schema carries\n * `\"x-vgai-ref\": target` \u2014 a declared intent, independent of whether the\n * target file currently exists. {@link collectDeclaredRefFields} /\n * {@link findMissingRefTargets} below walk that (zod-free \u2014 plain emitted\n * JSON Schema in, findings out, same purity contract as the rest of this\n * module) to catch the \u00A79.4 hole for DECLARED refs: renaming/deleting a\n * target file used to silently un-check every ref into it; now a declared\n * ref's target is checked by NAME, not by \"does a string happen to look\n * like one right now.\" Undeclared conventional-string refs keep the\n * calibrated behavior above, unchanged.\n */\n\n/**\n * `file#key(.field)*` \u2014 prefix must look like an asset stem (letters,\n * digits, `_`, `-`), key path is dot-separated segments of the same alphabet.\n */\nexport const DATA_REF_PATTERN = /^([A-Za-z0-9_-]+)#([A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)*)$/;\n\n/** One ref-shaped string found while walking a data file's parsed JSON. */\nexport interface DataRefOccurrence {\n /** Asset stem of the data file the ref string was found IN (e.g. `\"tuning\"`). */\n readonly inAsset: string;\n /** JSON path of the string inside that file (e.g. `\"rows.goblin.drops[2]\"`). */\n readonly atPath: string;\n /** The full ref string, verbatim (e.g. `\"enemies#goblin\"`). */\n readonly ref: string;\n /** Parsed target asset stem (`\"enemies\"`). */\n readonly targetAsset: string;\n /** Parsed dot-separated key path into the target (`[\"goblin\"]`). */\n readonly targetKeyPath: readonly string[];\n}\n\n/** One dangling ref: the target FILE exists but the key path does not resolve in it. */\nexport interface DanglingRefFinding extends DataRefOccurrence {\n /** The first key-path segment that failed to resolve, for the teaching message. */\n readonly missingSegment: string;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * Walk one data file's parsed JSON and collect every ref-shaped string whose\n * file prefix names an asset in `assetNames`. The top-level `\"$schema\"`\n * interchange line is skipped (it is wiring, not data \u2014 same special-casing\n * as `parseDataJson`'s strip).\n */\nexport function collectDataRefs(\n inAsset: string,\n json: unknown,\n assetNames: ReadonlySet<string>,\n): DataRefOccurrence[] {\n const out: DataRefOccurrence[] = [];\n visitForRefs(json, '', true, (node, path) => {\n const occurrence = asRefOccurrence(inAsset, node, path, assetNames);\n if (occurrence) out.push(occurrence);\n });\n return out;\n}\n\n/** Depth-first walk over parsed JSON, calling `onString` for every string leaf with its JSON path. */\nfunction visitForRefs(\n node: unknown,\n path: string,\n topLevel: boolean,\n onString: (value: string, path: string) => void,\n): void {\n if (typeof node === 'string') {\n onString(node, path);\n return;\n }\n if (Array.isArray(node)) {\n node.forEach((item, i) => {\n visitForRefs(item, `${path}[${i}]`, false, onString);\n });\n return;\n }\n if (isPlainObject(node)) {\n for (const [key, value] of Object.entries(node)) {\n if (topLevel && key === '$schema') continue;\n visitForRefs(value, path ? `${path}.${key}` : key, false, onString);\n }\n }\n}\n\n/** The string is a ref iff it matches the pattern AND its file prefix names a real asset. */\nfunction asRefOccurrence(\n inAsset: string,\n value: string,\n path: string,\n assetNames: ReadonlySet<string>,\n): DataRefOccurrence | null {\n const m = DATA_REF_PATTERN.exec(value);\n if (!m || !assetNames.has(m[1] as string)) return null;\n return {\n inAsset,\n atPath: path || '(root)',\n ref: value,\n targetAsset: m[1] as string,\n targetKeyPath: (m[2] as string).split('.'),\n };\n}\n\n/**\n * Find every dangling `file#key(.field)*` ref across a project's data assets.\n *\n * @param files parsed `.data.json` payloads keyed by asset stem (the\n * filename minus `.data.json` \u2014 the same name the ref's prefix uses, \u00A72.2).\n * Pass only files that parsed as JSON; unparseable files are already an\n * error in their own right and can't be resolved against anyway.\n * @returns one finding per ref whose target FILE is in `files` but whose key\n * path does not fully resolve inside it (a table row that doesn't exist, a\n * singleton field path that walks off the object).\n */\nexport function findDanglingDataRefs(files: ReadonlyMap<string, unknown>): DanglingRefFinding[] {\n const assetNames = new Set(files.keys());\n const findings: DanglingRefFinding[] = [];\n for (const [name, json] of files) {\n for (const occurrence of collectDataRefs(name, json, assetNames)) {\n const missing = firstUnresolvedSegment(files.get(occurrence.targetAsset), occurrence);\n if (missing !== null) findings.push({ ...occurrence, missingSegment: missing });\n }\n }\n return findings;\n}\n\n/** The first key-path segment that fails to resolve in the target payload, or `null` if the whole path resolves. */\nfunction firstUnresolvedSegment(target: unknown, ref: DataRefOccurrence): string | null {\n let node: unknown = target;\n for (const segment of ref.targetKeyPath) {\n if (!isPlainObject(node) || !(segment in node)) return segment;\n node = node[segment];\n }\n return null;\n}\n\n/** One `dataRef` field declared in an emitted JSON Schema. */\nexport interface DeclaredRefField {\n /** Dot-separated field path within the schema (e.g. `\"drop\"`, `\"loot.dropTable\"`). A table's row-schema fields are reported WITHOUT a row-key segment \u2014 the declaration applies to every row alike. */\n readonly fieldPath: string;\n /** The asset stem the field's `dataRef(...)` call named. */\n readonly targetStem: string;\n}\n\n/** One declared ref whose target stem names no asset that currently exists. */\nexport interface MissingRefTargetFinding {\n readonly fieldPath: string;\n readonly targetStem: string;\n}\n\n/** Loosely-typed view of an emitted JSON Schema node \u2014 walked structurally, never through Zod (this module stays zod-free). */\ninterface JsonSchemaNode {\n properties?: Record<string, unknown>;\n additionalProperties?: unknown;\n 'x-vgai-ref'?: unknown;\n}\n\nfunction asSchemaNode(value: unknown): JsonSchemaNode | null {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as JsonSchemaNode)\n : null;\n}\n\n/**\n * Walk an emitted data-asset JSON Schema (`toDataJsonSchema`'s output, or the\n * committed `.schema.json` twin \u2014 same shape either way) and collect every\n * field stamped `\"x-vgai-ref\": <target>` by `dataRef` (`./data-ref.ts`).\n * Handles both blessed shapes (spec \u00A72.1): a singleton's `properties`, and a\n * table's `additionalProperties` row schema \u2014 walked with the SAME field\n * path (a table's declared ref applies uniformly to every row, there is no\n * per-row schema variation). Nested `properties` inside a ref-declaring\n * field are not walked further (a ref leaf has no children); nested groups\n * elsewhere recurse via their own `properties`.\n */\nexport function collectDeclaredRefFields(schema: unknown): DeclaredRefField[] {\n const out: DeclaredRefField[] = [];\n walkForDeclaredRefs(schema, '', out);\n return out;\n}\n\nfunction walkForDeclaredRefs(raw: unknown, path: string, out: DeclaredRefField[]): void {\n const node = asSchemaNode(raw);\n if (!node) return;\n\n if (typeof node['x-vgai-ref'] === 'string') {\n out.push({ fieldPath: path || '(root)', targetStem: node['x-vgai-ref'] });\n return; // a ref leaf \u2014 nothing further to walk under it\n }\n\n if (isPlainObject(node.properties)) {\n for (const [key, child] of Object.entries(node.properties)) {\n if (key === '$schema') continue;\n walkForDeclaredRefs(child, path ? `${path}.${key}` : key, out);\n }\n }\n // A table's row schema (`z.record(rowSchema)` emits an object-valued\n // `additionalProperties`) \u2014 same field-path grammar, no row-key segment.\n if (isPlainObject(node.additionalProperties)) {\n walkForDeclaredRefs(node.additionalProperties, path, out);\n }\n}\n\n/**\n * Every declared ref field whose target stem names no asset in\n * `assetNames` \u2014 the \u00A79.4 fix: a `dataRef('items')` field stays flagged if\n * `items.data.json` is renamed or deleted, independent of whether any\n * CURRENT value happens to look like an `items#...` string (the gap the\n * conventional-string calibration above deliberately leaves open).\n */\nexport function findMissingRefTargets(\n declared: readonly DeclaredRefField[],\n assetNames: ReadonlySet<string>,\n): MissingRefTargetFinding[] {\n return declared\n .filter((d) => !assetNames.has(d.targetStem))\n .map((d) => ({ fieldPath: d.fieldPath, targetStem: d.targetStem }));\n}\n", "/**\n * `vgaiDataCheck` \u2014 build-path validation for data assets and project tools (the\n * enforcement half of W5). A Vite plugin for the PROJECT's own `vite.config.ts`\n * (the template wires it; the editor dev server boots Vite from the ENGINE's\n * config and never runs this \u2014 its surface is covered by `vgai doctor` instead).\n * Build-only (`apply: 'build'`): in dev, `defineData`'s parse-on-load already\n * fails loud in the console.\n *\n * Three checks, all failing the build loud:\n *\n * 1. **Schema integrity** (`buildStart`): every registered asset's\n * `.data.json` is read fresh from disk and validated through\n * {@link parseDataJson} \u2014 the EXACT parse `defineData` runs at load (\u00A79.1:\n * an emitted-JSON-Schema validator here could disagree with runtime; the\n * Zod schema itself never can). The schemas arrive EXECUTABLE because\n * `vite.config.ts` statically imports the project's data-asset registry\n * (`src/data/assets.ts` in the template) \u2014 Vite's config loader bundles\n * config-relative TS imports, which is what makes this the one build-side\n * place project Zod schemas can run.\n * 2. **Ref integrity** (`buildStart`): every `file#key(.field)*` string in\n * every `src/data/*.data.json` (registered or not) must resolve \u2014 shared\n * definition with `vgai doctor` via {@link findDanglingDataRefs}, so the\n * two surfaces can never disagree about what \"dangling\" means. A second,\n * narrower ref check runs alongside it for REGISTERED assets only: any\n * field declared with `dataRef(target)` (`./data-ref.ts`) whose `target`\n * names no asset currently on disk fails loud too (\u00A79.4's \"renamed the\n * target file\" hole, closed for declared refs \u2014 see\n * `collectDeclaredRefFields`/`findMissingRefTargets` in `data-check-core.ts`).\n * 3. **Tools never ship** (`generateBundle`): no emitted chunk may contain a\n * module from `src/tools/` or a `*.tool.*` file (\u00A74: tester serves and\n * standalone builds strip tools entirely; W4 made this true by\n * construction \u2014 nothing imports tools \u2014 and this check PINS it against\n * the day some game module imports a tool \"just for a helper\").\n *\n * Unregistered data files (a `.data.json` with no entry in the passed\n * `assets` list) are a WARNING, not an error: they still get ref-integrity\n * and runtime parse-on-load, but no build-time schema check \u2014 the warning\n * names the registry file to fix.\n */\n\nimport { readdirSync, readFileSync, statSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport type { Plugin } from 'vite';\nimport type { z } from 'zod';\nimport { parseDataJson, toDataJsonSchema } from './data-asset';\nimport {\n collectDeclaredRefFields,\n findDanglingDataRefs,\n findMissingRefTargets,\n} from './data-check-core';\n\n/** One registered data asset: its executable Zod schema + the project-relative path of its `.data.json`. */\nexport interface DataCheckAsset {\n readonly schema: z.ZodType;\n /** e.g. `'src/data/tuning.data.json'` \u2014 read fresh from disk at buildStart. */\n readonly sourcePath: string;\n}\n\nexport interface VgaiDataCheckOptions {\n readonly assets: readonly DataCheckAsset[];\n /** Project root the `sourcePath`s resolve against. Default: `process.cwd()` (vite build's cwd). */\n readonly root?: string;\n}\n\n/** Recursively collect `*.data.json` under `dir` (absent dir \u2192 empty \u2014 a project with no data assets builds fine). */\nfunction findDataFiles(dir: string): string[] {\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return [];\n }\n const out: string[] = [];\n for (const entry of entries) {\n const full = join(dir, entry);\n if (statSync(full).isDirectory()) out.push(...findDataFiles(full));\n else if (entry.endsWith('.data.json')) out.push(full);\n }\n return out;\n}\n\n/** `src/data/tuning.data.json` \u2192 `tuning` (normalizing either slash direction). */\nfunction assetStem(sourcePath: string): string {\n const rel = sourcePath.split('\\\\').join('/');\n return (\n rel\n .split('/')\n .pop()\n ?.replace(/\\.data\\.json$/, '') ?? rel\n );\n}\n\n/**\n * Parse every data file in the project once \u2014 the registered subset gets\n * schema validation, the whole set feeds ref integrity. Unparseable files\n * become errors; unregistered files become warnings naming the registry.\n *\n * DUPLICATE-STEM GUARD: `findDataFiles` walks `src/data/` recursively, so two\n * files in different subdirectories can share a stem (e.g.\n * `src/data/enemies.data.json` and `src/data/legacy/enemies.data.json`) \u2014 the\n * exact identity `file#key` refs AND the registry's `sourcePath` lookup both\n * address by stem alone (\u00A72.2). Without this guard `parsedByStem.set` would\n * silently last-wins the collision, and check 1 below could validate a\n * REGISTERED asset's schema against a totally unrelated file's JSON. Detected\n * here (one error per colliding path, naming every path) and excluded from\n * `parsedByStem` entirely \u2014 any resolution against a duplicate stem would be\n * an arbitrary, possibly-wrong pick, so neither check 1 nor check 2 (ref\n * integrity) resolves anything through it once it's flagged.\n */\ninterface ParsedDataFiles {\n parsedByStem: Map<string, unknown>;\n /** Stems the duplicate-stem guard already flagged \u2014 check 1 skips its own\n * redundant \"not found on disk\" for these (the duplicate-stem error\n * already explains why the stem has no single resolvable payload). */\n ambiguousStems: Set<string>;\n}\n\nfunction parseAllDataFiles(\n root: string,\n registered: ReadonlySet<string>,\n errors: string[],\n warnings: string[],\n): ParsedDataFiles {\n const parsedByStem = new Map<string, unknown>();\n const pathsByStem = new Map<string, string[]>();\n for (const abs of findDataFiles(join(root, 'src', 'data'))) {\n const rel = relative(root, abs).split('\\\\').join('/');\n const stem = assetStem(rel);\n const paths = pathsByStem.get(stem);\n if (paths) paths.push(rel);\n else pathsByStem.set(stem, [rel]);\n\n try {\n parsedByStem.set(stem, JSON.parse(readFileSync(abs, 'utf-8')));\n } catch (err) {\n errors.push(\n `\"${rel}\" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,\n );\n continue;\n }\n if (!registered.has(rel)) {\n warnings.push(\n `\"${rel}\" is not registered in the data-asset registry (src/data/assets.ts), so it gets ` +\n 'no build-time schema check (runtime parse-on-load still applies). Register it there \u2014 ' +\n 'the template ships the worked example.',\n );\n }\n }\n\n const ambiguousStems = new Set<string>();\n for (const [stem, paths] of pathsByStem) {\n if (paths.length < 2) continue;\n ambiguousStems.add(stem);\n errors.push(\n `asset stem \"${stem}\" is claimed by ${paths.length} files (${paths.join(', ')}) \u2014 ` +\n '\"file#key\" refs and the data-asset registry address a file by its stem alone (\u00A72.2), so ' +\n 'this is an unresolvable identity collision. Rename one \u2014 asset stems must be unique ' +\n 'across the whole project, not just per directory.',\n );\n parsedByStem.delete(stem); // arbitrary otherwise \u2014 neither check below may resolve through it\n }\n\n return { parsedByStem, ambiguousStems };\n}\n\n/**\n * The fs half of checks 1 + 2, extracted from the plugin hook so it is\n * directly unit-testable against a fixture folder (no rollup context needed).\n */\nexport function collectDataCheckProblems(options: VgaiDataCheckOptions): {\n errors: string[];\n warnings: string[];\n} {\n const root = options.root ?? process.cwd();\n const errors: string[] = [];\n const warnings: string[] = [];\n const registered = new Set(options.assets.map((a) => a.sourcePath.split('\\\\').join('/')));\n const { parsedByStem, ambiguousStems } = parseAllDataFiles(root, registered, errors, warnings);\n\n // Check 1 \u2014 registered assets validate through the exact runtime parse.\n for (const asset of options.assets) {\n const rel = asset.sourcePath.split('\\\\').join('/');\n if (!parsedByStem.has(assetStem(rel))) {\n // An AMBIGUOUS stem already has its own duplicate-stem error above,\n // which fully explains why there's no single payload to validate\n // against \u2014 a second \"not found on disk\" here would just be\n // confusing noise about a file that plainly IS on disk.\n if (!ambiguousStems.has(assetStem(rel))) {\n errors.push(\n `registered data asset \"${rel}\" was not found on disk \u2014 fix the path in the registry ` +\n '(src/data/assets.ts) or restore the file.',\n );\n }\n continue;\n }\n try {\n parseDataJson(asset.schema, parsedByStem.get(assetStem(rel)), rel);\n } catch (err) {\n errors.push(err instanceof Error ? err.message : String(err));\n }\n }\n\n // Check 2 \u2014 no dangling refs anywhere.\n for (const dangling of findDanglingDataRefs(parsedByStem)) {\n errors.push(\n `dangling data ref in \"src/data/${dangling.inAsset}.data.json\" at ${dangling.atPath}: ` +\n `\"${dangling.ref}\" \u2014 \"${dangling.missingSegment}\" does not exist in ` +\n `src/data/${dangling.targetAsset}.data.json. Fix the key or remove the ref ` +\n '(refs are \"file#key\" strings).',\n );\n }\n\n // Check 2b \u2014 every declared `dataRef(target)` field (registered assets\n // only, same set check 1 already validates) must name a target that's\n // actually on disk \u2014 the \u00A79.4 fix: this fires even if NO current value\n // happens to look like a ref, unlike check 2 above (see\n // `data-check-core.ts`'s module doc).\n const assetNames = new Set(parsedByStem.keys());\n for (const asset of options.assets) {\n const rel = asset.sourcePath.split('\\\\').join('/');\n const declared = collectDeclaredRefFields(toDataJsonSchema(asset.schema));\n for (const missing of findMissingRefTargets(declared, assetNames)) {\n errors.push(\n `\"${rel}\" declares field \"${missing.fieldPath}\" as dataRef('${missing.targetStem}'), but ` +\n `no \"src/data/${missing.targetStem}.data.json\" exists. Restore/rename the target file, ` +\n `or update the dataRef('${missing.targetStem}') call in the schema.`,\n );\n }\n }\n\n return { errors, warnings };\n}\n\n/** True iff a bundled module id is tool code that must never ship (\u00A74). Pure, unit-tested. */\nexport function isToolModuleId(id: string): boolean {\n const normalized = id.split('\\\\').join('/');\n return /\\/src\\/tools\\//.test(normalized) || /\\.tool\\.[tj]sx?(\\?|$)/.test(normalized);\n}\n\n/**\n * Build-path validation plugin \u2014 see the module doc. Wire it in the project's\n * `vite.config.ts`, passing the data-asset registry:\n *\n * ```ts\n * import { vgaiDataCheck } from '@vgai/engine/data/vite-plugin-data';\n * import { dataAssets } from './src/data/assets';\n * export default defineConfig({\n * plugins: [vgaiDataCheck({\n * root: __dirname,\n * assets: dataAssets.map((a) => ({ schema: a.schema, sourcePath: `src/data/${a.name}.data.json` })),\n * })],\n * });\n * ```\n */\nexport function vgaiDataCheck(options: VgaiDataCheckOptions): Plugin {\n return {\n name: 'vgai:data-check',\n apply: 'build',\n buildStart() {\n const { errors, warnings } = collectDataCheckProblems(options);\n for (const warning of warnings) this.warn(warning);\n if (errors.length > 0) {\n // ONE error carrying every problem \u2014 fail loud with the full picture,\n // not a fix-one-rebuild-see-the-next loop.\n this.error(\n `data-asset validation failed (${errors.length} problem${errors.length === 1 ? '' : 's'}` +\n `):\\n${errors.map((e) => ` - ${e}`).join('\\n')}`,\n );\n }\n },\n generateBundle(_outputOptions, bundle) {\n const leaked = new Set<string>();\n for (const output of Object.values(bundle)) {\n if (output.type !== 'chunk') continue;\n for (const id of Object.keys(output.modules)) {\n if (isToolModuleId(id)) leaked.add(id.split('\\\\').join('/'));\n }\n }\n if (leaked.size > 0) {\n this.error(\n 'project tool code reached the game build \u2014 tools are editor-only and must never ship ' +\n 'to players. Remove every game-code import of these ' +\n `modules (tools may import game code, never the reverse):\\n${[...leaked]\n .map((id) => ` - ${id}`)\n .join('\\n')}`,\n );\n }\n },\n };\n}\n", "/**\n * Data assets \u2014 W1 (\u00A72.3 consumption API).\n *\n * A data asset is designer-tunable game data: a JSON file in the project's\n * `src/data/`, governed by a Zod schema (`*.schema.ts`, engineer-owned) whose\n * emitted JSON Schema (`*.schema.json`, see {@link toDataJsonSchema}) gives\n * VS Code validation via the data file's `\"$schema\"` first line. Data assets\n * are immutable at runtime \u2014 definitions, not state (\u00A72.1).\n *\n * The blessed consumption shape, scaffolded working-at-birth by the project\n * template (`src/data/` there is the worked example \u2014 \u00A76.2):\n *\n * ```ts\n * import raw from './tuning.data.json';\n * import { TuningSchema } from './tuning.schema';\n * import { defineData } from '@engine/data/data-asset';\n *\n * export const tuning = defineData(TuningSchema, raw, 'src/data/tuning.data.json');\n * // Vite HMR needs the literal dep path in THIS module (static analysis) \u2014\n * // this one line is the entire live-tuning wiring (['default'] not .default:\n * // strict tsconfigs index-signature ModuleNamespace):\n * import.meta.hot?.accept('./tuning.data.json', (m) => tuning.hotSwap(m?.['default']));\n * ```\n *\n * Game code reads `tuning.get()` inside its update loop (never cache fields\n * across frames) or `subscribe()`s; an edit to the `.data.json` \u2014 from\n * VS Code, an agent, or the future editor Data tab (W2) \u2014 flows through Vite\n * HMR into the running game with no remount. React roots/tools get a\n * `useData` hook with W3's tool-hooks package (deferred with it: the engine\n * core stays React-free).\n */\n\nimport { z } from 'zod';\n\n/**\n * A live handle to one data asset (\u00A72.3). `get`/`subscribe` are the whole\n * runtime surface; `hotSwap` exists only for the owning module's\n * `import.meta.hot.accept` callback.\n */\nexport interface DataHandle<T> {\n /** Current parsed values. Read every frame \u2014 HMR swaps them under you. */\n get(): T;\n /**\n * Called with the new value after every successful hot swap (NOT with the\n * initial value). Returns an unsubscribe function.\n */\n subscribe(fn: (value: T) => void): () => void;\n /**\n * HMR entry point \u2014 call from the owning module's\n * `import.meta.hot.accept('<file>.data.json', (m) => handle.hotSwap(m?.default))`.\n * Parses like load, but a FAILING edit keeps the last good value and\n * `console.error`s instead of throwing: a typo mid-live-tune must not\n * crash the running game. Fix the file and save again.\n */\n hotSwap(next: unknown): void;\n /**\n * The project-relative path this handle was defined with (`defineData`'s\n * own `sourcePath` argument, populated automatically below) \u2014 OPTIONAL so\n * any hand-rolled structural implementer stays valid. `getRef`\n * (`./data-ref.ts`) reads this to catch a `getRef(wrongHandle, \"stem#key\")`\n * mismatch with a teaching error instead of silently resolving against the\n * wrong asset; without it, `getRef` just skips that one check.\n */\n readonly sourcePath?: string;\n}\n\n/**\n * Define a data asset from its Zod schema, the statically imported JSON, and\n * its project-relative path (used verbatim in error messages, so pass the\n * real one). Parse-on-load: an invalid file fails loud at import time, naming\n * the file, every bad path, and the fix (\u00A72.3, \u00A76.5 \"errors teach\").\n *\n * Schema fields should carry `.min/.max/.default/.describe` \u2014 the same\n * annotations every authored schema uses \u2014 so the emitted JSON Schema\n * documents the file and (W2+) the editor renders real widgets.\n */\nexport function defineData<S extends z.ZodType>(\n schema: S,\n initial: unknown,\n sourcePath: string,\n): DataHandle<z.output<S>> {\n let current = parseDataJson(schema, initial, sourcePath);\n const subscribers = new Set<(value: z.output<S>) => void>();\n return {\n get: () => current,\n sourcePath,\n subscribe(fn) {\n subscribers.add(fn);\n return () => subscribers.delete(fn);\n },\n hotSwap(next) {\n let parsed: z.output<S>;\n try {\n parsed = parseDataJson(schema, next, sourcePath);\n } catch (err) {\n // Keep the last good value alive \u2014 see the JSDoc on DataHandle.hotSwap.\n // biome-ignore lint/suspicious/noConsole: deliberate, greppable \u2014 a rejected live-tune edit must be loud without crashing the running game (mirrors achieved-tier.ts's direct console use)\n console.error(`[data] hot edit rejected, keeping previous values.\\n${String(err)}`);\n return;\n }\n current = parsed;\n for (const fn of subscribers) fn(current);\n },\n };\n}\n\n/**\n * Emit the JSON Schema interchange artifact for a data asset's Zod schema\n * (\u00A72.1: `*.schema.json` is EMITTED, never hand-written; D4: generated and\n * committed by default so VS Code works with no dev server running).\n *\n * Wraps `z.toJSONSchema` with `io: 'input'` \u2014 the one non-obvious knob:\n * defaulted fields must be OPTIONAL in the file schema (a data file may omit\n * them), whereas the default output mode would mark every field `required`\n * and make VS Code reject valid files. The template's root `emit-schemas.ts`\n * is the blessed caller (`npm run emit-schemas`).\n *\n * The emitted root schema also explicitly ALLOWS the `\"$schema\"` string\n * property, mirroring the parse-side strip below: without it, a TABLE\n * asset's emitted schema (`z.record` \u2192 `additionalProperties: <rowSchema>`)\n * makes VS Code flag the data file's own `\"$schema\"` line as an invalid row.\n */\nexport function toDataJsonSchema(schema: z.ZodType): Record<string, unknown> {\n const emitted = z.toJSONSchema(schema, { io: 'input' }) as Record<string, unknown>;\n if (emitted['type'] === 'object') {\n const properties = (emitted['properties'] ?? {}) as Record<string, unknown>;\n properties['$schema'] ??= { type: 'string' };\n emitted['properties'] = properties;\n }\n return emitted;\n}\n\n/**\n * Parse a raw `.data.json` payload: strips the `\"$schema\"` interchange line,\n * validates through the Zod schema, throws the teaching error on failure.\n * `defineData` uses this internally on load and hot swap; it is exported for\n * emit/doctor-style scripts (the template's `emit-schemas.ts` validates every\n * data file through it, so file validation and runtime validation can never\n * disagree).\n */\nexport function parseDataJson<S extends z.ZodType>(\n schema: S,\n data: unknown,\n sourcePath: string,\n): z.output<S> {\n // Strip the data file's `\"$schema\"` interchange line before validating \u2014\n // singleton (z.object, strip-mode) schemas ignored it by luck, but a TABLE\n // asset (z.record) would try to validate it as a row and fail.\n const payload =\n data !== null && typeof data === 'object' && !Array.isArray(data) && '$schema' in data\n ? Object.fromEntries(Object.entries(data).filter(([key]) => key !== '$schema'))\n : data;\n const result = schema.safeParse(payload);\n if (!result.success) {\n const issues = result.error.issues\n .map((issue) => ` - ${issue.path.join('.') || '(root)'}: ${issue.message}`)\n .join('\\n');\n throw new Error(\n `Data asset \"${sourcePath}\" failed its schema:\\n${issues}\\n` +\n `Fix the JSON to match the Zod schema (its emitted twin, ` +\n `\"${sourcePath.replace(/\\.data\\.json$/, '.schema.json')}\", documents every field \u2014 ` +\n `re-emit with \\`npm run emit-schemas\\` if the schema changed). ` +\n `Worked example: the template's src/data/.`,\n );\n }\n return result.data;\n}\n"],
5
- "mappings": "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",
6
- "names": ["z", "z"]
3
+ "sources": ["../src/data/curve.ts", "../src/data/data-ref.ts", "../src/data/data-check-core.ts", "../src/data/data-asset.ts", "../src/data/vite-plugin-data.ts"],
4
+ "sourcesContent": ["/**\n * Curve FIELDS in data assets \u2014 a response curve a designer shapes, living\n * inside an ordinary `.data.json` like any other tunable value.\n *\n * A curve is a FIELD TYPE, never a file type: there is no `.curve.json`, no\n * curve asset class, no baked LUT, and no document. `curve(axes)` is a real\n * Zod schema you drop into a `*.schema.ts` beside `z.number()`, so the value\n * lives in the same data file, hot-swaps through the same `defineData` handle,\n * and is edited by the same Data panel that edits everything else.\n *\n * The JSON a designer (or an agent) reads and writes:\n *\n * ```json\n * { \"interpolation\": \"smooth\",\n * \"points\": [{ \"x\": 0, \"y\": 1 }, { \"x\": 12, \"y\": 0.85 }, { \"x\": 30, \"y\": 0.2 }] }\n * ```\n *\n * TWO MODES, NO TANGENTS. `linear` and `smooth`, and nothing else. Per-key\n * tangent handles would double the JSON, make it unreadable to an agent, and\n * hand every author a way to make a curve overshoot its own points. Smoothness\n * is DERIVED instead \u2014 Fritsch\u2013Carlson monotone cubic tangents, computed from\n * the points themselves \u2014 so the interpolant is guaranteed to stay inside each\n * segment's `[yLeft, yRight]` band. \"Shape it by moving points\" is the whole\n * authoring model.\n *\n * RUNG AUDIT (recorded decision, 2026-08-14): no npm dependency. The candidate\n * packages (`monotone-cubic-spline` and its siblings) are unmaintained\n * micro-packages, and d3's `curveMonotoneX` is an SVG PATH GENERATOR \u2014 it\n * draws, it does not sample `y` at an `x`. What we need is ~40 lines of\n * well-known numerics with no API surface, so it is written here.\n *\n * `sampleCurve` is the entire runtime: a pure function, no class, no cache\n * object, no \"curve system\". Game code calls it every frame from its own\n * update, the same way it reads any other tuning value.\n */\n\nimport { z } from 'zod';\n\n/** One authored key: a `y` value at an `x` position. No tangents \u2014 see above. */\nexport interface CurvePoint {\n x: number;\n y: number;\n}\n\n/** A curve field's value, exactly as it sits in the `.data.json`. */\nexport interface CurveValue {\n interpolation: 'linear' | 'smooth';\n points: CurvePoint[];\n}\n\n/**\n * The curve's DOMAIN, declared by the engineer in the schema (never by the\n * data): what the axes mean and how far they run. The editor draws its rails\n * from this and clamps every drag to it; the schema rejects a point outside\n * it.\n */\nexport interface CurveAxes {\n x: { min: number; max: number; label?: string };\n y: { min: number; max: number; label?: string };\n}\n\n/** How the marker rides out to the emitted JSON Schema \u2014 the editor's and the dev menu's detection key, the exact shape `dataRef` uses with `x-vgai-ref`. */\nconst CURVE_META_KEY = 'x-vgai-curve';\n\nfunction axisLabel(axes: CurveAxes, axis: 'x' | 'y'): string {\n return axes[axis].label ?? axis;\n}\n\n/** Every out-of-domain / ordering problem in one pass, as teaching issues (`parseDataJson`'s \"errors teach\" doctrine, \u00A76.5). */\nfunction checkPoints(value: CurveValue, axes: CurveAxes, ctx: z.RefinementCtx): void {\n const points = value.points;\n for (let i = 0; i < points.length; i++) {\n const point = points[i] as CurvePoint;\n for (const axis of ['x', 'y'] as const) {\n const { min, max } = axes[axis];\n if (point[axis] < min || point[axis] > max) {\n ctx.addIssue({\n code: 'custom',\n path: ['points', i, axis],\n message:\n `${point[axis]} is outside this curve's ${axisLabel(axes, axis)} domain ` +\n `[${min}, ${max}] \u2014 move the point inside the domain, or widen the axis in the ` +\n `schema (curve({ ${axis}: { min, max } })).`,\n });\n }\n }\n if (i === 0) continue;\n const previous = points[i - 1] as CurvePoint;\n if (point.x <= previous.x) {\n ctx.addIssue({\n code: 'custom',\n path: ['points', i, 'x'],\n message:\n `${point.x} must be strictly greater than the previous point's x (${previous.x}) \u2014 ` +\n 'curve points run left to right along x, with no duplicate x. Sort the points, or ' +\n 'nudge this one.',\n });\n }\n }\n}\n\n/**\n * A Zod schema for a data-asset FIELD holding a response curve. Use it in a\n * `.schema.ts` exactly like any other field:\n *\n * ```ts\n * export const TuningSchema = z.object({\n * spawnRate: z.number().min(0).max(10).default(1),\n * difficultyRamp: curve({\n * x: { min: 0, max: 300, label: 'Sim time (s)' },\n * y: { min: 0, max: 2, label: 'Spawn multiplier' },\n * }).describe('How hard the game leans on the player as the round runs.'),\n * });\n * ```\n *\n * The schema is STRICT (an unrecognized key is an error, never a silent\n * strip), demands at least two points, and enforces strictly-increasing `x`\n * with every point inside the declared axes \u2014 each with a message that names\n * the fix rather than the rule.\n *\n * The emitted JSON Schema (`toDataJsonSchema`) carries\n * `\"x-vgai-curve\": axes` \u2014 a Zod `.meta()`, which survives `z.toJSONSchema`\n * including inside a table's `additionalProperties` row schema, exactly the\n * way `dataRef`'s `\"x-vgai-ref\"` does. That marker is what makes the editor's\n * Data panel render a curve EDITOR instead of a raw-JSON cell, with no emitter\n * changes anywhere.\n */\nexport function curve(axes: CurveAxes): z.ZodType<CurveValue> {\n const point = z.strictObject({\n x: z.number().describe(`Position along ${axisLabel(axes, 'x')}.`),\n y: z.number().describe(`Value at that position, along ${axisLabel(axes, 'y')}.`),\n });\n return z\n .strictObject({\n interpolation: z\n .enum(['linear', 'smooth'])\n .describe(\n 'How the value moves between points: \"linear\" for straight segments, ' +\n '\"smooth\" for a monotone cubic that never overshoots them.',\n ),\n points: z\n .array(point)\n .min(2, {\n message:\n 'a curve needs at least two points \u2014 add another { \"x\": \u2026, \"y\": \u2026 } so the ' +\n 'value has somewhere to travel between.',\n })\n .describe('Keys in increasing x order; the value is clamped outside the first/last x.'),\n })\n .superRefine((value, ctx) => checkPoints(value, axes, ctx))\n .meta({ [CURVE_META_KEY]: axes }) as unknown as z.ZodType<CurveValue>;\n}\n\n/**\n * Read a curve field's declared axes back off a LIVE Zod schema (as opposed to\n * the emitted JSON Schema, which the editor's Data panel reads instead).\n *\n * This is the dev-menu capability's detection door: it derives its rows by\n * walking the game's own `TuningSchema` object, so it needs the axes from the\n * schema in hand. Wrappers (`.optional()`, `.default(\u2026)`, `.describe(\u2026)` chains\n * that re-wrap) hide the metadata behind an inner type, so a bounded unwrap\n * walks down to it.\n */\nexport function curveAxesOf(schema: z.ZodType): CurveAxes | undefined {\n let current: unknown = schema;\n for (let depth = 0; depth < 5 && current; depth++) {\n const meta = (current as z.ZodType).meta?.() as Record<string, unknown> | undefined;\n const axes = meta?.[CURVE_META_KEY];\n if (axes && typeof axes === 'object' && 'x' in axes && 'y' in axes) return axes as CurveAxes;\n current = (current as { def?: { innerType?: unknown } }).def?.innerType;\n }\n return undefined;\n}\n\n/**\n * Fritsch\u2013Carlson tangents: the ordinary cubic-Hermite slopes, then limited so\n * the interpolant cannot overshoot the data. This is what makes \"smooth\" safe\n * to hand a designer \u2014 a curve that dips below its own points would silently\n * feed the game a value nobody authored.\n */\nfunction monotoneTangents(points: readonly CurvePoint[]): number[] {\n const n = points.length;\n const secants: number[] = [];\n for (let i = 0; i < n - 1; i++) {\n const left = points[i] as CurvePoint;\n const right = points[i + 1] as CurvePoint;\n secants.push((right.y - left.y) / (right.x - left.x));\n }\n const tangents: number[] = [secants[0] as number];\n for (let i = 1; i < n - 1; i++) {\n tangents.push(((secants[i - 1] as number) + (secants[i] as number)) / 2);\n }\n tangents.push(secants[n - 2] as number);\n\n for (let i = 0; i < n - 1; i++) {\n const secant = secants[i] as number;\n if (secant === 0) {\n // A flat segment must stay flat: any slope at either end would bulge.\n tangents[i] = 0;\n tangents[i + 1] = 0;\n continue;\n }\n let alpha = (tangents[i] as number) / secant;\n let beta = (tangents[i + 1] as number) / secant;\n // A tangent fighting the segment's own direction is a local extremum the\n // data does not have.\n if (alpha < 0) {\n tangents[i] = 0;\n alpha = 0;\n }\n if (beta < 0) {\n tangents[i + 1] = 0;\n beta = 0;\n }\n const radius = alpha * alpha + beta * beta;\n if (radius > 9) {\n const scale = 3 / Math.sqrt(radius);\n tangents[i] = scale * alpha * secant;\n tangents[i + 1] = scale * beta * secant;\n }\n }\n return tangents;\n}\n\n/**\n * The curve's value at `x` \u2014 the whole runtime API.\n *\n * Read it every frame from the game's own update, the way every other data\n * value is read (`tuning.get()` then sample); never precompute a table, and\n * never hold the result across frames \u2014 an edit to the `.data.json` hot-swaps\n * underneath you and that is the point.\n *\n * Outside `[first.x, last.x]` the value CLAMPS to the nearest endpoint: a\n * curve declares what it knows about, and extrapolating past it would invent\n * numbers nobody authored.\n */\nexport function sampleCurve(value: CurveValue, x: number): number {\n const points = value.points;\n if (points.length === 0) return 0;\n const first = points[0] as CurvePoint;\n const last = points[points.length - 1] as CurvePoint;\n if (points.length === 1 || x <= first.x) return first.y;\n if (x >= last.x) return last.y;\n\n let i = 0;\n while (i < points.length - 2 && x >= (points[i + 1] as CurvePoint).x) i++;\n const left = points[i] as CurvePoint;\n const right = points[i + 1] as CurvePoint;\n const span = right.x - left.x;\n if (span <= 0) return left.y; // duplicate x \u2014 the schema rejects it; a hand-built value might not\n const t = (x - left.x) / span;\n if (value.interpolation === 'linear') return left.y + (right.y - left.y) * t;\n\n const tangents = monotoneTangents(points);\n const t2 = t * t;\n const t3 = t2 * t;\n return (\n (2 * t3 - 3 * t2 + 1) * left.y +\n (t3 - 2 * t2 + t) * span * (tangents[i] as number) +\n (-2 * t3 + 3 * t2) * right.y +\n (t3 - t2) * span * (tangents[i + 1] as number)\n );\n}\n", "/**\n * `file#key` refs \u2014 the resolvable half.\n *\n * Cross-asset references (`\"enemies#goblin\"`, `\"tuning#economy.slotGrowth\"`)\n * have been DETECTED and VALIDATED since W5 (`data-check-core.ts`'s\n * `DATA_REF_PATTERN` + `findDanglingDataRefs`, enforced by both `vgai doctor`\n * and the build plugin) \u2014 but nothing could actually RESOLVE one at runtime,\n * and nothing declared a field AS a ref in its schema (detection was by\n * string-shape convention alone). This module closes both halves:\n *\n * - {@link dataRef} \u2014 a typed Zod schema for a ref FIELD. Stamps the emitted\n * JSON Schema with a precise `pattern` (the target stem is baked into the\n * regex, so VS Code validates the prefix for free) and a machine-readable\n * `\"x-vgai-ref\": target` marker (a Zod `.meta()` that survives\n * `toDataJsonSchema`, including inside a table's `additionalProperties` row\n * schema \u2014 no emitter changes needed). The editor's Data panel\n * (`json-schema-fields.ts`'s `'ref'` kind) reads that marker to render a\n * `stem#key` PICKER instead of free text.\n * - {@link getRef} \u2014 lazy, read-time resolution. The caller names the target\n * HANDLE explicitly (`getRef(items, goblin.drop)`), so the resolved type is\n * the handle's own row type \u2014 no stem\u2192handle registry, no load-order\n * problem, no cycle hazard (nothing walks a graph; two refs resolved when\n * you choose to resolve them isn't a cycle), and it's HMR-correct for free\n * (it reads `target.get()` at CALL time, same doctrine as every other data\n * read \u2014 \"read every frame\", never cache across frames).\n *\n * Deliberately NOT built (and the design note this module's history carries):\n * eager resolution inside `defineData` (would change `get()`'s shipped output\n * type and entangle HMR \u2014 an edit to `enemies.data.json` would have to\n * re-notify every OTHER asset that references it), and a global\n * `resolveRef(addressString)` (needs a runtime stem\u2192handle registry that\n * `src/data/assets.ts` \u2014 side-effect-free, config-load-time, schemas-only by\n * its own module doc \u2014 must not become).\n */\n\nimport { z } from 'zod';\nimport type { DataHandle } from './data-asset';\nimport { DATA_REF_PATTERN } from './data-check-core';\n\n/** An asset-stem-tagged `file#key` string \u2014 `` `${Stem}#${string}` ``. */\nexport type DataRef<Stem extends string = string> = `${Stem}#${string}`;\n\n/** Prefix/key-path grammar a ref must match, parameterized by target stem (mirrors `DATA_REF_PATTERN` in `data-check-core.ts`, minus the alternation \u2014 this one is anchored to ONE target). */\nfunction refPattern(target: string): RegExp {\n return new RegExp(`^${target}#[A-Za-z0-9_-]+(?:\\\\.[A-Za-z0-9_-]+)*$`);\n}\n\n/**\n * A Zod schema for a data-asset FIELD that references another asset by\n * `\"<target>#<key>(.<field>)*\"`. Use it in a `.schema.ts`:\n *\n * ```ts\n * export const EnemyRowSchema = z.object({\n * name: z.string(),\n * drop: dataRef('items'), // -> \"items#potion\"\n * });\n * ```\n *\n * The emitted JSON Schema (`toDataJsonSchema`) carries both a precise\n * `pattern` (VS Code validates the target-stem prefix for free) and\n * `\"x-vgai-ref\": target` (the editor's Data panel picker keys off this).\n * Detection by `data-check-core.ts`'s conventional-string calibration still\n * applies at the VALUE level (a `dataRef` field's actual string still has to\n * look like `target#key` to resolve) \u2014 this only makes the field's INTENT\n * declared in the schema, closing the \u00A79.4 \"renamed target file silently\n * un-checks its refs\" hole for anything that adopts it (see\n * `collectDeclaredRefFields`/`findMissingRefTargets` in `data-check-core.ts`).\n */\nexport function dataRef<Stem extends string>(target: Stem): z.ZodType<DataRef<Stem>> {\n return z\n .string()\n .regex(refPattern(target), {\n message:\n `must be a \"${target}#<key>\" reference \u2014 ` +\n `expected the target asset stem \"${target}\", e.g. \"${target}#some-key\".`,\n })\n .meta({ 'x-vgai-ref': target }) as z.ZodType<DataRef<Stem>>;\n}\n\n/** First failing key-path segment, or `null` if the whole path resolves \u2014 same walk `data-check-core.ts`'s `firstUnresolvedSegment` does over parsed JSON, here over a live handle's value. */\nfunction firstMissingSegment(root: unknown, keyPath: readonly string[]): string | null {\n let node: unknown = root;\n for (const segment of keyPath) {\n if (node === null || typeof node !== 'object' || Array.isArray(node) || !(segment in node)) {\n return segment;\n }\n node = (node as Record<string, unknown>)[segment];\n }\n return null;\n}\n\n/**\n * Resolve a `\"target#key(.field)*\"` ref against the named target HANDLE,\n * read live (`target.get()` at call time \u2014 HMR-correct, no caching across\n * frames, same doctrine as any other data read).\n *\n * `ref`'s stem prefix should name `target`'s own asset \u2014 when `target` was\n * built with a `sourcePath` (every `defineData` call populates it), a\n * mismatch throws a teaching error instead of silently walking the wrong\n * handle's data. Without a `sourcePath` (a hand-rolled `DataHandle`, or an\n * older one built before this field existed) the mismatch check is skipped \u2014\n * resolution still proceeds against `target`, since the caller named it\n * explicitly and that's the actual contract.\n *\n * Throws a teaching error naming the ref and the first key-path segment that\n * doesn't resolve (a deleted row, a typo'd key) \u2014 never returns `undefined`\n * silently, matching `parseDataJson`'s \"errors teach\" doctrine (\u00A76.5).\n */\nexport function getRef<T>(target: DataHandle<Record<string, T>>, ref: string): T {\n const m = DATA_REF_PATTERN.exec(ref);\n if (!m) {\n throw new Error(\n `getRef: \"${ref}\" is not a \"file#key\" reference \u2014 ` +\n 'expected a string shaped like \"some-file#some-key\".',\n );\n }\n const refStem = m[1] as string;\n const keyPathStr = m[2] as string;\n const targetStem = target.sourcePath\n ?.split('\\\\')\n .join('/')\n .split('/')\n .pop()\n ?.replace(/\\.data\\.json$/, '');\n if (targetStem !== undefined && targetStem !== refStem) {\n throw new Error(\n `getRef: ref \"${ref}\" names target asset \"${refStem}\", but the handle passed in is ` +\n `\"${targetStem}\" (${target.sourcePath}). Pass the handle ` +\n `\"${refStem}#...\" actually names \u2014 getRef(${refStem}, ...), not getRef(${targetStem}, ...).`,\n );\n }\n const keyPath = keyPathStr.split('.');\n const root = target.get();\n const missing = firstMissingSegment(root, keyPath);\n if (missing !== null) {\n throw new Error(\n `getRef: dangling ref \"${ref}\" \u2014 \"${missing}\" does not exist` +\n (targetStem ? ` in ${targetStem}.data.json` : '') +\n '. Fix the key or remove the ref (refs are \"file#key\" strings).',\n );\n }\n let node: unknown = root;\n for (const segment of keyPath) node = (node as Record<string, unknown>)[segment];\n return node as T;\n}\n", "/**\n * Data-asset integrity core \u2014 the PURE half of W5 enforcement (\"dangling\n * `file#key` ref\" / \u00A76.7 build-path validation). No filesystem, no vite, no\n * zod: plain parsed-JSON in, findings out, so BOTH consumers \u2014 `vgai\n * doctor`'s data checks (`packages/editor/src/doctor/data-checks.ts`) and the\n * build-time plugin (`./vite-plugin-data.ts`) \u2014 share one definition of \"what\n * is a ref\" and \"what counts as dangling\" and can never drift apart.\n *\n * What is a ref (\u00A72.2): a cross-asset reference is a plain string\n * `\"file#key\"` \u2014 optionally with a field path, `\"tuning#economy.slotGrowth\"`\n * (file `#` key `.` field-path). There is no branded-ref Zod helper shipped\n * yet (W1 deferred it), so ref DETECTION is by convention, tuned against\n * false positives: a string is treated as a ref IFF it matches\n * {@link DATA_REF_PATTERN} AND its `file` prefix names a data asset that\n * actually exists in the project (`src/data/<file>.data.json`). Strings that\n * merely contain `#` \u2014 hex colors (`\"#ff0000\"`, no prefix), URLs with\n * fragments (`/` and `:` never match), musical pitches (`\"C#4\"` only matches\n * if a `C.data.json` exists) \u2014 never fire. The flip side is deliberate and\n * honest: a ref whose file prefix names a NONEXISTENT asset is\n * indistinguishable from an arbitrary string and is NOT flagged (\"error if\n * refs exist yet\" \u2014 the check only bites once the target file exists).\n *\n * `dataRef` (`./data-ref.ts`) now exists, closing that flip side for anyone\n * who adopts it: a `dataRef(target)` field's EMITTED schema carries\n * `\"x-vgai-ref\": target` \u2014 a declared intent, independent of whether the\n * target file currently exists. {@link collectDeclaredRefFields} /\n * {@link findMissingRefTargets} below walk that (zod-free \u2014 plain emitted\n * JSON Schema in, findings out, same purity contract as the rest of this\n * module) to catch the \u00A79.4 hole for DECLARED refs: renaming/deleting a\n * target file used to silently un-check every ref into it; now a declared\n * ref's target is checked by NAME, not by \"does a string happen to look\n * like one right now.\" Undeclared conventional-string refs keep the\n * calibrated behavior above, unchanged.\n */\n\n/**\n * `file#key(.field)*` \u2014 prefix must look like an asset stem (letters,\n * digits, `_`, `-`), key path is dot-separated segments of the same alphabet.\n */\nexport const DATA_REF_PATTERN = /^([A-Za-z0-9_-]+)#([A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)*)$/;\n\n/** One ref-shaped string found while walking a data file's parsed JSON. */\nexport interface DataRefOccurrence {\n /** Asset stem of the data file the ref string was found IN (e.g. `\"tuning\"`). */\n readonly inAsset: string;\n /** JSON path of the string inside that file (e.g. `\"rows.goblin.drops[2]\"`). */\n readonly atPath: string;\n /** The full ref string, verbatim (e.g. `\"enemies#goblin\"`). */\n readonly ref: string;\n /** Parsed target asset stem (`\"enemies\"`). */\n readonly targetAsset: string;\n /** Parsed dot-separated key path into the target (`[\"goblin\"]`). */\n readonly targetKeyPath: readonly string[];\n}\n\n/** One dangling ref: the target FILE exists but the key path does not resolve in it. */\nexport interface DanglingRefFinding extends DataRefOccurrence {\n /** The first key-path segment that failed to resolve, for the teaching message. */\n readonly missingSegment: string;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * Walk one data file's parsed JSON and collect every ref-shaped string whose\n * file prefix names an asset in `assetNames`. The top-level `\"$schema\"`\n * interchange line is skipped (it is wiring, not data \u2014 same special-casing\n * as `parseDataJson`'s strip).\n */\nexport function collectDataRefs(\n inAsset: string,\n json: unknown,\n assetNames: ReadonlySet<string>,\n): DataRefOccurrence[] {\n const out: DataRefOccurrence[] = [];\n visitForRefs(json, '', true, (node, path) => {\n const occurrence = asRefOccurrence(inAsset, node, path, assetNames);\n if (occurrence) out.push(occurrence);\n });\n return out;\n}\n\n/** Depth-first walk over parsed JSON, calling `onString` for every string leaf with its JSON path. */\nfunction visitForRefs(\n node: unknown,\n path: string,\n topLevel: boolean,\n onString: (value: string, path: string) => void,\n): void {\n if (typeof node === 'string') {\n onString(node, path);\n return;\n }\n if (Array.isArray(node)) {\n node.forEach((item, i) => {\n visitForRefs(item, `${path}[${i}]`, false, onString);\n });\n return;\n }\n if (isPlainObject(node)) {\n for (const [key, value] of Object.entries(node)) {\n if (topLevel && key === '$schema') continue;\n visitForRefs(value, path ? `${path}.${key}` : key, false, onString);\n }\n }\n}\n\n/** The string is a ref iff it matches the pattern AND its file prefix names a real asset. */\nfunction asRefOccurrence(\n inAsset: string,\n value: string,\n path: string,\n assetNames: ReadonlySet<string>,\n): DataRefOccurrence | null {\n const m = DATA_REF_PATTERN.exec(value);\n if (!m || !assetNames.has(m[1] as string)) return null;\n return {\n inAsset,\n atPath: path || '(root)',\n ref: value,\n targetAsset: m[1] as string,\n targetKeyPath: (m[2] as string).split('.'),\n };\n}\n\n/**\n * Find every dangling `file#key(.field)*` ref across a project's data assets.\n *\n * @param files parsed `.data.json` payloads keyed by asset stem (the\n * filename minus `.data.json` \u2014 the same name the ref's prefix uses, \u00A72.2).\n * Pass only files that parsed as JSON; unparseable files are already an\n * error in their own right and can't be resolved against anyway.\n * @returns one finding per ref whose target FILE is in `files` but whose key\n * path does not fully resolve inside it (a table row that doesn't exist, a\n * singleton field path that walks off the object).\n */\nexport function findDanglingDataRefs(files: ReadonlyMap<string, unknown>): DanglingRefFinding[] {\n const assetNames = new Set(files.keys());\n const findings: DanglingRefFinding[] = [];\n for (const [name, json] of files) {\n for (const occurrence of collectDataRefs(name, json, assetNames)) {\n const missing = firstUnresolvedSegment(files.get(occurrence.targetAsset), occurrence);\n if (missing !== null) findings.push({ ...occurrence, missingSegment: missing });\n }\n }\n return findings;\n}\n\n/** The first key-path segment that fails to resolve in the target payload, or `null` if the whole path resolves. */\nfunction firstUnresolvedSegment(target: unknown, ref: DataRefOccurrence): string | null {\n let node: unknown = target;\n for (const segment of ref.targetKeyPath) {\n if (!isPlainObject(node) || !(segment in node)) return segment;\n node = node[segment];\n }\n return null;\n}\n\n/** One `dataRef` field declared in an emitted JSON Schema. */\nexport interface DeclaredRefField {\n /** Dot-separated field path within the schema (e.g. `\"drop\"`, `\"loot.dropTable\"`). A table's row-schema fields are reported WITHOUT a row-key segment \u2014 the declaration applies to every row alike. */\n readonly fieldPath: string;\n /** The asset stem the field's `dataRef(...)` call named. */\n readonly targetStem: string;\n}\n\n/** One declared ref whose target stem names no asset that currently exists. */\nexport interface MissingRefTargetFinding {\n readonly fieldPath: string;\n readonly targetStem: string;\n}\n\n/** Loosely-typed view of an emitted JSON Schema node \u2014 walked structurally, never through Zod (this module stays zod-free). */\ninterface JsonSchemaNode {\n properties?: Record<string, unknown>;\n additionalProperties?: unknown;\n 'x-vgai-ref'?: unknown;\n}\n\nfunction asSchemaNode(value: unknown): JsonSchemaNode | null {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as JsonSchemaNode)\n : null;\n}\n\n/**\n * Walk an emitted data-asset JSON Schema (`toDataJsonSchema`'s output, or the\n * committed `.schema.json` twin \u2014 same shape either way) and collect every\n * field stamped `\"x-vgai-ref\": <target>` by `dataRef` (`./data-ref.ts`).\n * Handles both blessed shapes (spec \u00A72.1): a singleton's `properties`, and a\n * table's `additionalProperties` row schema \u2014 walked with the SAME field\n * path (a table's declared ref applies uniformly to every row, there is no\n * per-row schema variation). Nested `properties` inside a ref-declaring\n * field are not walked further (a ref leaf has no children); nested groups\n * elsewhere recurse via their own `properties`.\n */\nexport function collectDeclaredRefFields(schema: unknown): DeclaredRefField[] {\n const out: DeclaredRefField[] = [];\n walkForDeclaredRefs(schema, '', out);\n return out;\n}\n\nfunction walkForDeclaredRefs(raw: unknown, path: string, out: DeclaredRefField[]): void {\n const node = asSchemaNode(raw);\n if (!node) return;\n\n if (typeof node['x-vgai-ref'] === 'string') {\n out.push({ fieldPath: path || '(root)', targetStem: node['x-vgai-ref'] });\n return; // a ref leaf \u2014 nothing further to walk under it\n }\n\n if (isPlainObject(node.properties)) {\n for (const [key, child] of Object.entries(node.properties)) {\n if (key === '$schema') continue;\n walkForDeclaredRefs(child, path ? `${path}.${key}` : key, out);\n }\n }\n // A table's row schema (`z.record(rowSchema)` emits an object-valued\n // `additionalProperties`) \u2014 same field-path grammar, no row-key segment.\n if (isPlainObject(node.additionalProperties)) {\n walkForDeclaredRefs(node.additionalProperties, path, out);\n }\n}\n\n/**\n * Every declared ref field whose target stem names no asset in\n * `assetNames` \u2014 the \u00A79.4 fix: a `dataRef('items')` field stays flagged if\n * `items.data.json` is renamed or deleted, independent of whether any\n * CURRENT value happens to look like an `items#...` string (the gap the\n * conventional-string calibration above deliberately leaves open).\n */\nexport function findMissingRefTargets(\n declared: readonly DeclaredRefField[],\n assetNames: ReadonlySet<string>,\n): MissingRefTargetFinding[] {\n return declared\n .filter((d) => !assetNames.has(d.targetStem))\n .map((d) => ({ fieldPath: d.fieldPath, targetStem: d.targetStem }));\n}\n", "/**\n * Data assets \u2014 W1 (\u00A72.3 consumption API).\n *\n * A data asset is designer-tunable game data: a JSON file in the project's\n * `src/data/`, governed by a Zod schema (`*.schema.ts`, engineer-owned) whose\n * emitted JSON Schema (`*.schema.json`, see {@link toDataJsonSchema}) gives\n * VS Code validation via the data file's `\"$schema\"` first line. Data assets\n * are immutable at runtime \u2014 definitions, not state (\u00A72.1).\n *\n * The blessed consumption shape, scaffolded working-at-birth by the project\n * template (`src/data/` there is the worked example \u2014 \u00A76.2):\n *\n * ```ts\n * import raw from './tuning.data.json';\n * import { TuningSchema } from './tuning.schema';\n * import { defineData } from '@engine/data/data-asset';\n *\n * export const tuning = defineData(TuningSchema, raw, 'src/data/tuning.data.json');\n * // Vite HMR needs the literal dep path in THIS module (static analysis) \u2014\n * // this one line is the entire live-tuning wiring (['default'] not .default:\n * // strict tsconfigs index-signature ModuleNamespace):\n * import.meta.hot?.accept('./tuning.data.json', (m) => tuning.hotSwap(m?.['default']));\n * ```\n *\n * Game code reads `tuning.get()` inside its update loop (never cache fields\n * across frames) or `subscribe()`s; an edit to the `.data.json` \u2014 from\n * VS Code, an agent, or the future editor Data tab (W2) \u2014 flows through Vite\n * HMR into the running game with no remount. React roots/tools get a\n * `useData` hook with W3's tool-hooks package (deferred with it: the engine\n * core stays React-free).\n */\n\nimport { z } from 'zod';\n\n/**\n * A live handle to one data asset (\u00A72.3). `get`/`subscribe` are the whole\n * runtime surface; `hotSwap` exists only for the owning module's\n * `import.meta.hot.accept` callback.\n */\nexport interface DataHandle<T> {\n /** Current parsed values. Read every frame \u2014 HMR swaps them under you. */\n get(): T;\n /**\n * Called with the new value after every successful hot swap (NOT with the\n * initial value). Returns an unsubscribe function.\n */\n subscribe(fn: (value: T) => void): () => void;\n /**\n * HMR entry point \u2014 call from the owning module's\n * `import.meta.hot.accept('<file>.data.json', (m) => handle.hotSwap(m?.default))`.\n * Parses like load, but a FAILING edit keeps the last good value and\n * `console.error`s instead of throwing: a typo mid-live-tune must not\n * crash the running game. Fix the file and save again.\n */\n hotSwap(next: unknown): void;\n /**\n * The project-relative path this handle was defined with (`defineData`'s\n * own `sourcePath` argument, populated automatically below) \u2014 OPTIONAL so\n * any hand-rolled structural implementer stays valid. `getRef`\n * (`./data-ref.ts`) reads this to catch a `getRef(wrongHandle, \"stem#key\")`\n * mismatch with a teaching error instead of silently resolving against the\n * wrong asset; without it, `getRef` just skips that one check.\n */\n readonly sourcePath?: string;\n}\n\n/**\n * Define a data asset from its Zod schema, the statically imported JSON, and\n * its project-relative path (used verbatim in error messages, so pass the\n * real one). Parse-on-load: an invalid file fails loud at import time, naming\n * the file, every bad path, and the fix (\u00A72.3, \u00A76.5 \"errors teach\").\n *\n * Schema fields should carry `.min/.max/.default/.describe` \u2014 the same\n * annotations every authored schema uses \u2014 so the emitted JSON Schema\n * documents the file and (W2+) the editor renders real widgets.\n */\nexport function defineData<S extends z.ZodType>(\n schema: S,\n initial: unknown,\n sourcePath: string,\n): DataHandle<z.output<S>> {\n let current = parseDataJson(schema, initial, sourcePath);\n const subscribers = new Set<(value: z.output<S>) => void>();\n return {\n get: () => current,\n sourcePath,\n subscribe(fn) {\n subscribers.add(fn);\n return () => subscribers.delete(fn);\n },\n hotSwap(next) {\n let parsed: z.output<S>;\n try {\n parsed = parseDataJson(schema, next, sourcePath);\n } catch (err) {\n // Keep the last good value alive \u2014 see the JSDoc on DataHandle.hotSwap.\n // biome-ignore lint/suspicious/noConsole: deliberate, greppable \u2014 a rejected live-tune edit must be loud without crashing the running game (mirrors achieved-tier.ts's direct console use)\n console.error(`[data] hot edit rejected, keeping previous values.\\n${String(err)}`);\n return;\n }\n current = parsed;\n for (const fn of subscribers) fn(current);\n },\n };\n}\n\n/**\n * Emit the JSON Schema interchange artifact for a data asset's Zod schema\n * (\u00A72.1: `*.schema.json` is EMITTED, never hand-written; D4: generated and\n * committed by default so VS Code works with no dev server running).\n *\n * Wraps `z.toJSONSchema` with `io: 'input'` \u2014 the one non-obvious knob:\n * defaulted fields must be OPTIONAL in the file schema (a data file may omit\n * them), whereas the default output mode would mark every field `required`\n * and make VS Code reject valid files. The template's root `emit-schemas.ts`\n * is the blessed caller (`npm run emit-schemas`).\n *\n * The emitted root schema also explicitly ALLOWS the `\"$schema\"` string\n * property, mirroring the parse-side strip below: without it, a TABLE\n * asset's emitted schema (`z.record` \u2192 `additionalProperties: <rowSchema>`)\n * makes VS Code flag the data file's own `\"$schema\"` line as an invalid row.\n */\nexport function toDataJsonSchema(schema: z.ZodType): Record<string, unknown> {\n const emitted = z.toJSONSchema(schema, { io: 'input' }) as Record<string, unknown>;\n if (emitted['type'] === 'object') {\n const properties = (emitted['properties'] ?? {}) as Record<string, unknown>;\n properties['$schema'] ??= { type: 'string' };\n emitted['properties'] = properties;\n }\n return emitted;\n}\n\n/**\n * Parse a raw `.data.json` payload: strips the `\"$schema\"` interchange line,\n * validates through the Zod schema, throws the teaching error on failure.\n * `defineData` uses this internally on load and hot swap; it is exported for\n * emit/doctor-style scripts (the template's `emit-schemas.ts` validates every\n * data file through it, so file validation and runtime validation can never\n * disagree).\n */\nexport function parseDataJson<S extends z.ZodType>(\n schema: S,\n data: unknown,\n sourcePath: string,\n): z.output<S> {\n // Strip the data file's `\"$schema\"` interchange line before validating \u2014\n // singleton (z.object, strip-mode) schemas ignored it by luck, but a TABLE\n // asset (z.record) would try to validate it as a row and fail.\n const payload =\n data !== null && typeof data === 'object' && !Array.isArray(data) && '$schema' in data\n ? Object.fromEntries(Object.entries(data).filter(([key]) => key !== '$schema'))\n : data;\n const result = schema.safeParse(payload);\n if (!result.success) {\n const issues = result.error.issues\n .map((issue) => ` - ${issue.path.join('.') || '(root)'}: ${issue.message}`)\n .join('\\n');\n throw new Error(\n `Data asset \"${sourcePath}\" failed its schema:\\n${issues}\\n` +\n `Fix the JSON to match the Zod schema (its emitted twin, ` +\n `\"${sourcePath.replace(/\\.data\\.json$/, '.schema.json')}\", documents every field \u2014 ` +\n `re-emit with \\`npm run emit-schemas\\` if the schema changed). ` +\n `Worked example: the template's src/data/.`,\n );\n }\n return result.data;\n}\n", "/**\n * `vgaiDataCheck` \u2014 build-path validation for data assets and project tools (the\n * enforcement half of W5). A Vite plugin for the PROJECT's own `vite.config.ts`\n * (the template wires it; the editor dev server boots Vite from the ENGINE's\n * config and never runs this \u2014 its surface is covered by `vgai doctor` instead).\n * Build-only (`apply: 'build'`): in dev, `defineData`'s parse-on-load already\n * fails loud in the console.\n *\n * Three checks, all failing the build loud:\n *\n * 1. **Schema integrity** (`buildStart`): every registered asset's\n * `.data.json` is read fresh from disk and validated through\n * {@link parseDataJson} \u2014 the EXACT parse `defineData` runs at load (\u00A79.1:\n * an emitted-JSON-Schema validator here could disagree with runtime; the\n * Zod schema itself never can). The schemas arrive EXECUTABLE because\n * `vite.config.ts` statically imports the project's data-asset registry\n * (`src/data/assets.ts` in the template) \u2014 Vite's config loader bundles\n * config-relative TS imports, which is what makes this the one build-side\n * place project Zod schemas can run.\n * 2. **Ref integrity** (`buildStart`): every `file#key(.field)*` string in\n * every `src/data/*.data.json` (registered or not) must resolve \u2014 shared\n * definition with `vgai doctor` via {@link findDanglingDataRefs}, so the\n * two surfaces can never disagree about what \"dangling\" means. A second,\n * narrower ref check runs alongside it for REGISTERED assets only: any\n * field declared with `dataRef(target)` (`./data-ref.ts`) whose `target`\n * names no asset currently on disk fails loud too (\u00A79.4's \"renamed the\n * target file\" hole, closed for declared refs \u2014 see\n * `collectDeclaredRefFields`/`findMissingRefTargets` in `data-check-core.ts`).\n * 3. **Tools never ship** (`generateBundle`): no emitted chunk may contain a\n * module from `src/tools/` or a `*.tool.*` file (\u00A74: tester serves and\n * standalone builds strip tools entirely; W4 made this true by\n * construction \u2014 nothing imports tools \u2014 and this check PINS it against\n * the day some game module imports a tool \"just for a helper\").\n *\n * Unregistered data files (a `.data.json` with no entry in the passed\n * `assets` list) are a WARNING, not an error: they still get ref-integrity\n * and runtime parse-on-load, but no build-time schema check \u2014 the warning\n * names the registry file to fix.\n */\n\n// Node built-ins are imported LAZILY (inside `collectDataCheckProblems`),\n// never statically: this module is re-exported by the `@vgai/engine/config`\n// barrel, which every project `*.schema.ts` reaches through its `data-ref`\n// shim \u2014 so this module EVALUATES in the browser graph of any world whose\n// schema the game imports. A static `import { \u2026 } from 'node:fs'` dies there\n// (\"Module node:fs has been externalized for browser compatibility\") before\n// the world can mount. The fs work itself only ever runs in Node (vite build\n// / emit-schemas), where the dynamic import resolves normally.\nimport type { Plugin } from 'vite';\nimport type { z } from 'zod';\nimport { parseDataJson, toDataJsonSchema } from './data-asset';\nimport {\n collectDeclaredRefFields,\n findDanglingDataRefs,\n findMissingRefTargets,\n} from './data-check-core';\n\n/** One registered data asset: its executable Zod schema + the project-relative path of its `.data.json`. */\nexport interface DataCheckAsset {\n readonly schema: z.ZodType;\n /** e.g. `'src/data/tuning.data.json'` \u2014 read fresh from disk at buildStart. */\n readonly sourcePath: string;\n}\n\nexport interface VgaiDataCheckOptions {\n readonly assets: readonly DataCheckAsset[];\n /** Project root the `sourcePath`s resolve against. Default: `process.cwd()` (vite build's cwd). */\n readonly root?: string;\n}\n\n/** The Node APIs the checks need, resolved lazily \u2014 see the import note above. */\ninterface NodeIo {\n readdirSync: typeof import('node:fs')['readdirSync'];\n readFileSync: typeof import('node:fs')['readFileSync'];\n statSync: typeof import('node:fs')['statSync'];\n join: typeof import('node:path')['join'];\n relative: typeof import('node:path')['relative'];\n}\n\nasync function loadNodeIo(): Promise<NodeIo> {\n const [fs, path] = await Promise.all([import('node:fs'), import('node:path')]);\n return {\n readdirSync: fs.readdirSync,\n readFileSync: fs.readFileSync,\n statSync: fs.statSync,\n join: path.join,\n relative: path.relative,\n };\n}\n\n/** Recursively collect `*.data.json` under `dir` (absent dir \u2192 empty \u2014 a project with no data assets builds fine). */\nfunction findDataFiles(io: NodeIo, dir: string): string[] {\n let entries: string[];\n try {\n entries = io.readdirSync(dir) as string[];\n } catch {\n return [];\n }\n const out: string[] = [];\n for (const entry of entries) {\n const full = io.join(dir, entry);\n if (io.statSync(full).isDirectory()) out.push(...findDataFiles(io, full));\n else if (entry.endsWith('.data.json')) out.push(full);\n }\n return out;\n}\n\n/** `src/data/tuning.data.json` \u2192 `tuning` (normalizing either slash direction). */\nfunction assetStem(sourcePath: string): string {\n const rel = sourcePath.split('\\\\').join('/');\n return (\n rel\n .split('/')\n .pop()\n ?.replace(/\\.data\\.json$/, '') ?? rel\n );\n}\n\n/**\n * Parse every data file in the project once \u2014 the registered subset gets\n * schema validation, the whole set feeds ref integrity. Unparseable files\n * become errors; unregistered files become warnings naming the registry.\n *\n * DUPLICATE-STEM GUARD: `findDataFiles` walks `src/data/` recursively, so two\n * files in different subdirectories can share a stem (e.g.\n * `src/data/enemies.data.json` and `src/data/legacy/enemies.data.json`) \u2014 the\n * exact identity `file#key` refs AND the registry's `sourcePath` lookup both\n * address by stem alone (\u00A72.2). Without this guard `parsedByStem.set` would\n * silently last-wins the collision, and check 1 below could validate a\n * REGISTERED asset's schema against a totally unrelated file's JSON. Detected\n * here (one error per colliding path, naming every path) and excluded from\n * `parsedByStem` entirely \u2014 any resolution against a duplicate stem would be\n * an arbitrary, possibly-wrong pick, so neither check 1 nor check 2 (ref\n * integrity) resolves anything through it once it's flagged.\n */\ninterface ParsedDataFiles {\n parsedByStem: Map<string, unknown>;\n /** Stems the duplicate-stem guard already flagged \u2014 check 1 skips its own\n * redundant \"not found on disk\" for these (the duplicate-stem error\n * already explains why the stem has no single resolvable payload). */\n ambiguousStems: Set<string>;\n}\n\nfunction parseAllDataFiles(\n io: NodeIo,\n root: string,\n registered: ReadonlySet<string>,\n errors: string[],\n warnings: string[],\n): ParsedDataFiles {\n const parsedByStem = new Map<string, unknown>();\n const pathsByStem = new Map<string, string[]>();\n for (const abs of findDataFiles(io, io.join(root, 'src', 'data'))) {\n const rel = io.relative(root, abs).split('\\\\').join('/');\n const stem = assetStem(rel);\n const paths = pathsByStem.get(stem);\n if (paths) paths.push(rel);\n else pathsByStem.set(stem, [rel]);\n\n try {\n parsedByStem.set(stem, JSON.parse(io.readFileSync(abs, 'utf-8') as string));\n } catch (err) {\n errors.push(\n `\"${rel}\" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,\n );\n continue;\n }\n if (!registered.has(rel)) {\n warnings.push(\n `\"${rel}\" is not registered in the data-asset registry (src/data/assets.ts), so it gets ` +\n 'no build-time schema check (runtime parse-on-load still applies). Register it there \u2014 ' +\n 'the template ships the worked example.',\n );\n }\n }\n\n const ambiguousStems = new Set<string>();\n for (const [stem, paths] of pathsByStem) {\n if (paths.length < 2) continue;\n ambiguousStems.add(stem);\n errors.push(\n `asset stem \"${stem}\" is claimed by ${paths.length} files (${paths.join(', ')}) \u2014 ` +\n '\"file#key\" refs and the data-asset registry address a file by its stem alone (\u00A72.2), so ' +\n 'this is an unresolvable identity collision. Rename one \u2014 asset stems must be unique ' +\n 'across the whole project, not just per directory.',\n );\n parsedByStem.delete(stem); // arbitrary otherwise \u2014 neither check below may resolve through it\n }\n\n return { parsedByStem, ambiguousStems };\n}\n\n/**\n * The fs half of checks 1 + 2, extracted from the plugin hook so it is\n * directly unit-testable against a fixture folder (no rollup context needed).\n */\nexport async function collectDataCheckProblems(options: VgaiDataCheckOptions): Promise<{\n errors: string[];\n warnings: string[];\n}> {\n const io = await loadNodeIo();\n const root = options.root ?? process.cwd();\n const errors: string[] = [];\n const warnings: string[] = [];\n const registered = new Set(options.assets.map((a) => a.sourcePath.split('\\\\').join('/')));\n const { parsedByStem, ambiguousStems } = parseAllDataFiles(\n io,\n root,\n registered,\n errors,\n warnings,\n );\n\n // Check 1 \u2014 registered assets validate through the exact runtime parse.\n for (const asset of options.assets) {\n const rel = asset.sourcePath.split('\\\\').join('/');\n if (!parsedByStem.has(assetStem(rel))) {\n // An AMBIGUOUS stem already has its own duplicate-stem error above,\n // which fully explains why there's no single payload to validate\n // against \u2014 a second \"not found on disk\" here would just be\n // confusing noise about a file that plainly IS on disk.\n if (!ambiguousStems.has(assetStem(rel))) {\n errors.push(\n `registered data asset \"${rel}\" was not found on disk \u2014 fix the path in the registry ` +\n '(src/data/assets.ts) or restore the file.',\n );\n }\n continue;\n }\n try {\n parseDataJson(asset.schema, parsedByStem.get(assetStem(rel)), rel);\n } catch (err) {\n errors.push(err instanceof Error ? err.message : String(err));\n }\n }\n\n // Check 2 \u2014 no dangling refs anywhere.\n for (const dangling of findDanglingDataRefs(parsedByStem)) {\n errors.push(\n `dangling data ref in \"src/data/${dangling.inAsset}.data.json\" at ${dangling.atPath}: ` +\n `\"${dangling.ref}\" \u2014 \"${dangling.missingSegment}\" does not exist in ` +\n `src/data/${dangling.targetAsset}.data.json. Fix the key or remove the ref ` +\n '(refs are \"file#key\" strings).',\n );\n }\n\n // Check 2b \u2014 every declared `dataRef(target)` field (registered assets\n // only, same set check 1 already validates) must name a target that's\n // actually on disk \u2014 the \u00A79.4 fix: this fires even if NO current value\n // happens to look like a ref, unlike check 2 above (see\n // `data-check-core.ts`'s module doc).\n const assetNames = new Set(parsedByStem.keys());\n for (const asset of options.assets) {\n const rel = asset.sourcePath.split('\\\\').join('/');\n const declared = collectDeclaredRefFields(toDataJsonSchema(asset.schema));\n for (const missing of findMissingRefTargets(declared, assetNames)) {\n errors.push(\n `\"${rel}\" declares field \"${missing.fieldPath}\" as dataRef('${missing.targetStem}'), but ` +\n `no \"src/data/${missing.targetStem}.data.json\" exists. Restore/rename the target file, ` +\n `or update the dataRef('${missing.targetStem}') call in the schema.`,\n );\n }\n }\n\n return { errors, warnings };\n}\n\n/** True iff a bundled module id is tool code that must never ship (\u00A74). Pure, unit-tested. */\nexport function isToolModuleId(id: string): boolean {\n const normalized = id.split('\\\\').join('/');\n return /\\/src\\/tools\\//.test(normalized) || /\\.tool\\.[tj]sx?(\\?|$)/.test(normalized);\n}\n\n/**\n * Build-path validation plugin \u2014 see the module doc. Wire it in the project's\n * `vite.config.ts`, passing the data-asset registry:\n *\n * ```ts\n * import { vgaiDataCheck } from '@vgai/engine/data/vite-plugin-data';\n * import { dataAssets } from './src/data/assets';\n * export default defineConfig({\n * plugins: [vgaiDataCheck({\n * root: __dirname,\n * assets: dataAssets.map((a) => ({ schema: a.schema, sourcePath: `src/data/${a.name}.data.json` })),\n * })],\n * });\n * ```\n */\nexport function vgaiDataCheck(options: VgaiDataCheckOptions): Plugin {\n return {\n name: 'vgai:data-check',\n apply: 'build',\n async buildStart() {\n const { errors, warnings } = await collectDataCheckProblems(options);\n for (const warning of warnings) this.warn(warning);\n if (errors.length > 0) {\n // ONE error carrying every problem \u2014 fail loud with the full picture,\n // not a fix-one-rebuild-see-the-next loop.\n this.error(\n `data-asset validation failed (${errors.length} problem${errors.length === 1 ? '' : 's'}` +\n `):\\n${errors.map((e) => ` - ${e}`).join('\\n')}`,\n );\n }\n },\n generateBundle(_outputOptions, bundle) {\n const leaked = new Set<string>();\n for (const output of Object.values(bundle)) {\n if (output.type !== 'chunk') continue;\n for (const id of Object.keys(output.modules)) {\n if (isToolModuleId(id)) leaked.add(id.split('\\\\').join('/'));\n }\n }\n if (leaked.size > 0) {\n this.error(\n 'project tool code reached the game build \u2014 tools are editor-only and must never ship ' +\n 'to players. Remove every game-code import of these ' +\n `modules (tools may import game code, never the reverse):\\n${[...leaked]\n .map((id) => ` - ${id}`)\n .join('\\n')}`,\n );\n }\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["z", "z", "z", "z"]
7
7
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.11",
5
+ "version": "0.5.13",
6
6
  "description": "Readable TypeScript game engine and universal host for Three.js, PixiJS, and React games.",
7
7
  "keywords": [
8
8
  "game-engine",
@@ -2,7 +2,7 @@
2
2
  "$comment": "Curated by human/agent review, NOT auto-generated (unlike engine-api.json beside it). Every claim was verified against code — see each entry's `evidence`. Staleness is machine-checked at two granularities: `schemaDirHash` (sha256 over packages/engine/src/asset-formats/) and per-entry `evidenceHashes` (sha256 of each cited evidence FILE — catches drift in files outside the schema dir, which is where most entries' evidence lives; T5.5/§7.2).",
3
3
  "_readme": "Read this before authoring scene JSON or engine code, to avoid the negatives below. Refresh flow after a schema-dir OR evidence-file change: `npx tsx scripts/capabilities-hash.ts --check` (fails naming the stale entry) -> re-review affected entries against current code -> `--write` to record fresh hashes. The pre-commit hook runs --check automatically. Each entry's `evidenceHashes` maps the repo-relative file path parsed off the front of each `evidence[]` citation (before any `:line-range` suffix) to a sha256 of that file's current content.",
4
4
  "schemaDirPath": "packages/engine/src/asset-formats/",
5
- "schemaDirHash": "3f8728d85cc14c17acbdababa414b51f40d73cee30434e83cf23b01518db06a6",
5
+ "schemaDirHash": "692bc97e09d46cd5a3e7d30e9e5574f0cb6ef48b1c96bd779fd5af2268c3b3e8",
6
6
  "reviewedAt": "2026-07-01",
7
7
  "entries": [
8
8
  {
@@ -62,33 +62,33 @@
62
62
  }
63
63
  },
64
64
  {
65
- "id": "ingestion-pixi-tiers",
65
+ "id": "ingestion-pixi",
66
66
  "area": "ingestion",
67
- "status": "supported-with-tiers",
68
- "claim": "Unmodified OSS PixiJS games run via the canvas ingest seam: manifest 'iframe-reachable' + bundleUrl — in-realm capture, editor edits, overlay persistence. extraDeps must be registry-known (unknown throws); capture failure degrades loudly.",
67
+ "status": "supported-with-caveats",
68
+ "claim": "Unmodified OSS PixiJS games run via the canvas ingest seam: the game's own entry module is served in the editor's realm with pixi.js deduped to the host instance — in-realm capture, editor edits, overlay persistence. A game whose pixi the trap cannot reach fails to mount by name.",
69
69
  "evidence": [
70
70
  "vendor/games/verify-unaltered.mjs (vendored-bytes gate: zero UNRECORDED diff — byte-identical files, plus any recorded per-file patch that must reverse-apply to the pinned upstream)",
71
71
  "packages/engine/src/manifest/schema.ts"
72
72
  ],
73
- "notes": "Track P shipped 2026-07-03; proven: bubbo-bubbo/puzzling-potions/flappy-pixi. Tiers/limits: ARCHITECTURE-CORE.md.",
73
+ "notes": "Limits: ARCHITECTURE-CORE.md. Any editor capability a mount does not reach stands warned per capability until it works.",
74
74
  "evidenceHashes": {
75
- "vendor/games/verify-unaltered.mjs": "df875b09d698b3cfc03443ff3dba4929f5f08ea8913009d4fae253d478282312",
76
- "packages/engine/src/manifest/schema.ts": "d689135c4702e09be1de6a66c8625e64be10ca80deb988f30430678fa77b88db"
75
+ "vendor/games/verify-unaltered.mjs": "7ce44c050cd32824a7449ef03138a600d5c770395d93f04fc30037c19a6f49bd",
76
+ "packages/engine/src/manifest/schema.ts": "6dacf48d618ef35070ba1cb6bdf6e58d5742b7c228461e81d6542ff22df06e5c"
77
77
  }
78
78
  },
79
79
  {
80
80
  "id": "ingestion-react-dom",
81
81
  "area": "ingestion",
82
82
  "status": "supported-with-caveats",
83
- "claim": "Unmodified native-React DOM games run via the manifest `ingest-react` identity: vendored byte-for-byte (vendor/games/, zero-diff CI), mounted through the react world stack under the host React 19 (deduped). Embed-only in v1 — hosted, sized, disposed; NO authoring (the editor never writes JSX back into an unmodified game, D-N4).",
83
+ "claim": "Unmodified native-React DOM games run via the manifest `ingest-react` identity: vendored byte-for-byte (vendor/games/, zero-diff CI), mounted through the react world stack under the host React 19 (deduped). NO authoring in v1 (the editor never writes JSX back into an unmodified game, D-N4).",
84
84
  "evidence": [
85
85
  "vendor/games/verify-unaltered.mjs (vendored-bytes gate: zero UNRECORDED diff — byte-identical files, plus any recorded per-file patch that must reverse-apply to the pinned upstream)",
86
86
  "packages/editor/src/adapter-resolver.ts"
87
87
  ],
88
88
  "notes": "Scope is native-DOM React ONLY (D-N1): R3F/@pixi/react games are canvas games and ride the three.js/pixi seams instead. Generality limit: ONE proven game (react-rpg — menu-to-dungeon e2e under React 19; both game-2 candidates rejected on evidence, R-N7). Game deps must be host-installed root deps (D-N6). Open R-N6: vendored react games' raw window listeners bypass the play-mode input gate.",
89
89
  "evidenceHashes": {
90
- "vendor/games/verify-unaltered.mjs": "df875b09d698b3cfc03443ff3dba4929f5f08ea8913009d4fae253d478282312",
91
- "packages/editor/src/adapter-resolver.ts": "ce88071c9a4061df0a3b7edef24031bf81704ae2c3ede62ed6bf43d4f34ac556"
90
+ "vendor/games/verify-unaltered.mjs": "7ce44c050cd32824a7449ef03138a600d5c770395d93f04fc30037c19a6f49bd",
91
+ "packages/editor/src/adapter-resolver.ts": "243eb9fea298416a7fb02ea1c6cfa3832c47845d15d96792d6ce360cba1e69c0"
92
92
  }
93
93
  },
94
94
  {
@@ -100,22 +100,12 @@
100
100
  "ingest": {
101
101
  "type": "object",
102
102
  "properties": {
103
- "strategy": {
104
- "type": "string",
105
- "enum": [
106
- "shared",
107
- "deduped",
108
- "iframe-reachable",
109
- "opaque-embed"
110
- ],
111
- "description": "Capture mechanics rung this ingested root uses (§4) — also the root`s natural local capability tier"
112
- },
113
- "entryHtml": {
114
- "description": "Entry HTML path, project-relative. WIRED for opaque-embed (D-W2): the file`s bytes are read verbatim through the project-static route and hosted in a sandboxed iframe (embed-only, no scene introspection), for both canvas and three roots. Legal but NOT wired to a mount for iframe-reachable (a named error at resolve names this — use `bundleUrl` instead, the wired reachable route).",
103
+ "contractShim": {
104
+ "description": "Project-relative path to a HOST-ADDED ES module that declares this game's `window.vgaiGame` contract (adapter/ingest/game-contract.ts) WITHOUT editing a vendored byte — the pristine-copy door. The host imports it immediately BEFORE the game's own entry module, in the editor's realm. TIMING CONTRACT, which a shim author must obey: because it runs before the game`s entry, it MUST assign `window.vgaiGame` SYNCHRONOUSLY at top level. Anything that needs the game`s own modules must be LAZY: a dynamic `import()` INSIDE each verb/provider closure, resolved at call time. Importing a game module eagerly from the shim would evaluate it ahead of the game`s own entry and reorder its module side effects. Every such `import()` MUST take a STRING LITERAL, never a variable/table lookup. A variable specifier is not statically analyzable, so the dev server leaves it alone and it resolves at runtime against the shim`s own `/@fs/` url — while the game`s entry has had ITS relative imports rewritten to whatever url the server considers canonical for those files. Module identity is per-url, so the shim silently binds a SECOND, freshly-evaluated copy of the whole game and every provider reports a game that never started. Measured on three-descent: 285 segments through the game`s graph, 0 through the shim`s, and no error anywhere.",
115
105
  "type": "string"
116
106
  },
117
- "contractShim": {
118
- "description": "Project-relative path to a HOST-ADDED ES module that declares this game's `window.vgaiGame` contract (adapter/ingest/game-contract.ts) WITHOUT editing a vendored byte the pristine-copy door. The host injects it as a `<script type=\"module\">` into the assembled iframe document AFTER the game`s own importmap (moving a module load ahead of an import map invalidates the map) and BEFORE the game`s entry module, so deferred module scripts run it first. TIMING CONTRACT, which a shim author must obey: because it runs before the game`s entry, it MUST assign `window.vgaiGame` SYNCHRONOUSLY at top level the host waits for that assignment and fails the mount by name if it never arrives. Anything that needs the game`s own modules must be LAZY: a dynamic `import()` INSIDE each verb/provider closure, resolved at call time. Importing a game module eagerly from the shim would evaluate it ahead of the game`s own entry and reorder its module side effects. Read by the vendored/self-hosted three route only (strategy `iframe-reachable` with `entryHtml`); declaring it elsewhere is a schema error rather than a silently dead field.",
107
+ "dataWriter": {
108
+ "description": "Project-relative path to a HOST-ADDED ES module that writes this game's own DATA file back the sibling of a source write for a game whose authorable truth is not source. Descent places its robots, powerups and hostages in binary records inside `descent.hog`, reaching no source literal, so an authored move of one is an edit to that file and nothing else can write it. The module declares two exports and the host knows nothing else about it: `dataFile` (a project-relative string the ONE file this writer edits) and `planDataEdit(bytes, {record, property, baseline, next})`, which returns `{changed: true, bytes}` or `{changed: false, reason}` and may be async. It is loaded in the editor realm beside the game, so it may `import()` the game's own modules to resolve what only a running game knows (Descent's writer asks the game`s own `find_point_seg` which segment a moved object landed in, and refuses \"outside the mine\" rather than writing data the game would read back wrong). It NEVER fabricates: a record it cannot address, or a property it does not model, is a named refusal. An object is anchored to a record by carrying `userData.vgaiRecordIndex` the game declares that identity itself (a recorded patch or a shim); the host never infers one.",
119
109
  "type": "string"
120
110
  },
121
111
  "assets": {
@@ -136,40 +126,11 @@
136
126
  }
137
127
  },
138
128
  "captureTimeoutMs": {
139
- "description": "How long to wait for capture before REPORTING the degrade loudly not how long to keep listening. On the self-hosted-three route the devtools subscription is permanent, so a renderer appearing later still upgrades a reported-embed-only mount in place; the host clamps this value so a long one cannot block the editor on a question the upgrade path answers anyway.",
129
+ "description": "How long the host waits for this game's first captured frame before the mount FAILS by name. A game that needs a long boot (streamed world assets, a menu the player must clear) raises it; the default is 10s.",
140
130
  "type": "number"
141
- },
142
- "bundleUrl": {
143
- "description": "Absolute URL of the externalized built game bundle — the WIRED iframe-reachable mount route (iframe-reachable only), for BOTH canvas (Track P, ingest-iframe-2d.ts's mountIngestGame2DIframeReachableMulti) and three (D-W1, ingest-iframe-reachable-adapter.ts's mountIngestGameIframeReachableBundle). PRECONDITION: the bundle must externalize its root`s own runtime as a bare, un-rewritten import (`pixi.js` for canvas, `three` for three — `vgai bundle` does this for you) so it resolves through the iframe importmap to the HOST instance the capture trap is installed on; a bundle with an inlined runtime is unreachable and degrades to the embed-only floor. Exactly one of bundleUrl/entryHtml is required when strategy is iframe-reachable.",
144
- "type": "string"
145
- },
146
- "baseHref": {
147
- "description": "Iframe <base href> so the game's relative asset URLs resolve (iframe-reachable only).",
148
- "type": "string"
149
- },
150
- "assetBaseUrl": {
151
- "description": "Host-side basePath forced onto the trapped runtime's own asset resolution — pixi's `Assets.init` for a canvas root, three's `DefaultLoadingManager.setURLModifier` for a three root (D-W1) — absolute, or root-relative (resolved against the editor page's own origin at mount time, ingest-mode.ts, so a static manifest never has to know the dev-server port) (iframe-reachable only — a document.write iframe keeps the parent window.location).",
152
- "type": "string"
153
- },
154
- "extraDeps": {
155
- "description": "Bare specifiers of this root's externalized deps beyond its own runtime (pixi.js/three — e.g. '@pixi/sound', 'gsap'). Resolved by the editor's host-namespace registry into module namespaces when known (D-P1 — the manifest carries data, not code); an unregistered specifier falls back to the open project's own node_modules (D-Z3), served as a verbatim iframe importmap URL. Throws a named error if neither resolves it (iframe-reachable only).",
156
- "type": "array",
157
- "items": {
158
- "type": "string"
159
- }
160
- },
161
- "pixiModuleUrl": {
162
- "description": "PIXI-ONLY: standalone matching-major pixi ESM URL to trap instead of the host pixi, for version-skewed games (e.g. a v6 game on a v8 host); this field's PRESENCE is how the skew is expressed (D-P2 — no separate strategy/tier for version skew) (iframe-reachable only). Declaring this on a `kind: \"three\"` root is schema-legal (this field is kind-agnostic here) but throws a NAMED not-supported error at resolve — three has no version-skew mount this wave (D-W7: a hypothetical `threeModuleUrl` is explicitly parked, no consumer exists).",
163
- "type": "string"
164
- },
165
- "bodyHtml": {
166
- "description": "HTML injected into the iframe body before the game script boots (e.g. a `<div id=\"game-root\">` the game expects to find) (iframe-reachable only).",
167
- "type": "string"
168
131
  }
169
132
  },
170
- "required": [
171
- "strategy"
172
- ],
133
+ "additionalProperties": false,
173
134
  "description": "Ingest adapter configuration for an unmodified game (a repo-vendored game or your own external folder)"
174
135
  }
175
136
  },
@@ -204,42 +165,12 @@
204
165
  },
205
166
  "loop": {
206
167
  "default": "gated",
207
- "description": "gated: host-driven loop (default). self-driven: this root drives its own loop (the \"composited, unsynchronized\" tier) — an axis independent of capability tier",
168
+ "description": "gated: host-driven loop (default). self-driven: this root drives its own loop (composited, unsynchronized)",
208
169
  "type": "string",
209
170
  "enum": [
210
171
  "gated",
211
172
  "self-driven"
212
173
  ]
213
- },
214
- "capabilities": {
215
- "description": "Per-delivery-context capability tier declarations (§4). Declaring above the adapter's ceiling for that context is an error; declaring at or below it (including `unsupported`) is legal.",
216
- "type": "object",
217
- "properties": {
218
- "local": {
219
- "description": "Declared capability tier when served via the local CLI (Vite pipeline); omitted -> derived (§4)",
220
- "type": "string",
221
- "enum": [
222
- "first-party",
223
- "shared",
224
- "deduped",
225
- "iframe-reachable",
226
- "opaque-embed",
227
- "unsupported"
228
- ]
229
- },
230
- "hosted": {
231
- "description": "Declared capability tier when served hosted (no bundler: esbuild-wasm + externals only); omitted -> derived (§4)",
232
- "type": "string",
233
- "enum": [
234
- "first-party",
235
- "shared",
236
- "deduped",
237
- "iframe-reachable",
238
- "opaque-embed",
239
- "unsupported"
240
- ]
241
- }
242
- }
243
174
  }
244
175
  },
245
176
  "required": [
@@ -360,6 +291,20 @@
360
291
  "height"
361
292
  ]
362
293
  },
294
+ "rendering": {
295
+ "description": "Construction-time renderer properties, which no world can declare after the fact. Everything a world CAN declare (tone mapping, output colour space, clear colour, shadow filter) lives on the world's own renderer config instead.",
296
+ "type": "object",
297
+ "properties": {
298
+ "antialias": {
299
+ "type": "boolean",
300
+ "description": "Whether every three root is built with a multisampled drawing buffer. This is the ONE render property a world cannot declare for itself (world3d-react/renderer-config.ts): a WebGL context fixes its sample count at CREATION from this boolean, long before a world mounts, so it belongs to the PROJECT. The runtime reader is mount-manifest.ts, which threads it into createHostRenderer for each three root. WebGL exposes no sample COUNT — the implementation picks one (4x on every desktop browser measured), so a source engine that authored 8x/16x gets multisampling but not its exact count."
301
+ }
302
+ },
303
+ "required": [
304
+ "antialias"
305
+ ],
306
+ "additionalProperties": false
307
+ },
363
308
  "debug": {
364
309
  "description": "Debug-bridge production gating (D18) — governs whether ?vgai-debug=1 installs window.__vgai outside dev builds.",
365
310
  "type": "object",