@vgai/engine 0.5.15 → 0.5.17

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 (220) hide show
  1. package/dist/adapter/adapter-module.d.ts +229 -33
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +222 -50
  4. package/dist/adapter/authoring-seam-contract.d.ts +530 -0
  5. package/dist/adapter/authoring-seam-contract.d.ts.map +1 -0
  6. package/dist/adapter/authoring-seam-contract.js +162 -0
  7. package/dist/adapter/authoring.d.ts +237 -50
  8. package/dist/adapter/authoring.d.ts.map +1 -1
  9. package/dist/adapter/authoring.js +2 -1
  10. package/dist/adapter/entrypoint-selection-source.d.ts +35 -0
  11. package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -0
  12. package/dist/adapter/entrypoint-selection-source.js +151 -0
  13. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +7 -3
  14. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
  15. package/dist/adapter/finders/scenes-from-entrypoint-selection.js +145 -27
  16. package/dist/adapter/first-party-systems.d.ts +13 -3
  17. package/dist/adapter/first-party-systems.d.ts.map +1 -1
  18. package/dist/adapter/first-party-systems.js +10 -3
  19. package/dist/adapter/index.d.ts +7 -2
  20. package/dist/adapter/index.d.ts.map +1 -1
  21. package/dist/adapter/index.js +4 -0
  22. package/dist/adapter/ingest/contract-system-adapters.d.ts +1 -1
  23. package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
  24. package/dist/adapter/ingest/contract-system-adapters.js +13 -25
  25. package/dist/adapter/ingest/game-contract-seams.d.ts +214 -0
  26. package/dist/adapter/ingest/game-contract-seams.d.ts.map +1 -0
  27. package/dist/adapter/ingest/game-contract-seams.js +62 -0
  28. package/dist/adapter/ingest/game-contract.d.ts +5 -3
  29. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  30. package/dist/adapter/ingest/game-contract.js +4 -1
  31. package/dist/adapter/ingest/merge-debug-adapters.d.ts +52 -0
  32. package/dist/adapter/ingest/merge-debug-adapters.d.ts.map +1 -0
  33. package/dist/adapter/ingest/merge-debug-adapters.js +141 -0
  34. package/dist/adapter/ingest/observation-debug-adapter.d.ts +56 -0
  35. package/dist/adapter/ingest/observation-debug-adapter.d.ts.map +1 -0
  36. package/dist/adapter/ingest/observation-debug-adapter.js +131 -0
  37. package/dist/adapter/ingest/scene-capture.d.ts +38 -13
  38. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  39. package/dist/adapter/ingest/scene-capture.js +101 -24
  40. package/dist/adapter/ingest/visible-capture-window.d.ts +22 -5
  41. package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
  42. package/dist/adapter/ingest/visible-capture-window.js +46 -7
  43. package/dist/adapter/native-debug-module.d.ts +43 -0
  44. package/dist/adapter/native-debug-module.d.ts.map +1 -0
  45. package/dist/adapter/native-debug-module.js +230 -0
  46. package/dist/adapter/object3d-authoring-subject.d.ts +32 -0
  47. package/dist/adapter/object3d-authoring-subject.d.ts.map +1 -0
  48. package/dist/adapter/object3d-authoring-subject.js +25 -0
  49. package/dist/adapter/root-adapter.d.ts +25 -56
  50. package/dist/adapter/root-adapter.d.ts.map +1 -1
  51. package/dist/adapter/root-adapter.js +4 -20
  52. package/dist/adapter/root-seam-contract.d.ts +129 -0
  53. package/dist/adapter/root-seam-contract.d.ts.map +1 -0
  54. package/dist/adapter/root-seam-contract.js +37 -0
  55. package/dist/adapter/seam-evidence.d.ts +118 -0
  56. package/dist/adapter/seam-evidence.d.ts.map +1 -0
  57. package/dist/adapter/seam-evidence.js +231 -0
  58. package/dist/adapter/setup-three-root-adapter.d.ts +2 -3
  59. package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
  60. package/dist/adapter/setup-three-root-adapter.js +11 -12
  61. package/dist/adapter/system-adapter.d.ts +6 -1
  62. package/dist/adapter/system-adapter.d.ts.map +1 -1
  63. package/dist/adapter/system-seam-contract.d.ts +354 -0
  64. package/dist/adapter/system-seam-contract.d.ts.map +1 -0
  65. package/dist/adapter/system-seam-contract.js +95 -0
  66. package/dist/animation/xstate-animation-binding.d.ts +6 -0
  67. package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
  68. package/dist/animation/xstate-animation-binding.js +142 -4
  69. package/dist/asset-formats/collider.d.ts +1 -1
  70. package/dist/asset-formats/mesh.d.ts +2 -2
  71. package/dist/asset-formats/particles.d.ts +1 -1
  72. package/dist/canvas-react/engine-bridge.d.ts +6 -6
  73. package/dist/canvas-react/engine-bridge.d.ts.map +1 -1
  74. package/dist/canvas-react/engine-bridge.js +2 -2
  75. package/dist/canvas-react/index.d.ts +2 -2
  76. package/dist/canvas-react/index.d.ts.map +1 -1
  77. package/dist/canvas-react/index.js +1 -1
  78. package/dist/canvas-react/pixi-react-adapter.d.ts +15 -9
  79. package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -1
  80. package/dist/canvas-react/pixi-react-adapter.js +129 -64
  81. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  82. package/dist/canvas-react/pixi-react-root-factory.js +4 -0
  83. package/dist/canvas-react/world-context.d.ts +22 -12
  84. package/dist/canvas-react/world-context.d.ts.map +1 -1
  85. package/dist/canvas-react/world-context.js +12 -51
  86. package/dist/data/curve.d.ts +1 -1
  87. package/dist/data/curve.js +2 -2
  88. package/dist/dev/instruments.d.ts +4 -4
  89. package/dist/dev/instruments.js +4 -4
  90. package/dist/dev/register-render-vitals.d.ts +2 -2
  91. package/dist/dev/register-render-vitals.js +2 -2
  92. package/dist/dev/static-batch-advisor.d.ts +1 -1
  93. package/dist/dev/static-batch-advisor.js +2 -2
  94. package/dist/ecs/user-data.d.ts +5 -0
  95. package/dist/ecs/user-data.d.ts.map +1 -1
  96. package/dist/ecs/user-data.js +3 -0
  97. package/dist/input/input-manager.d.ts +64 -0
  98. package/dist/input/input-manager.d.ts.map +1 -1
  99. package/dist/input/input-manager.js +149 -0
  100. package/dist/manifest/load.d.ts +0 -3
  101. package/dist/manifest/load.d.ts.map +1 -1
  102. package/dist/manifest/load.js +0 -4
  103. package/dist/manifest/schema.d.ts +9 -11
  104. package/dist/manifest/schema.d.ts.map +1 -1
  105. package/dist/manifest/schema.js +2 -23
  106. package/dist/pixi/authoring.d.ts +33 -0
  107. package/dist/pixi/authoring.d.ts.map +1 -1
  108. package/dist/pixi/authoring.js +301 -2
  109. package/dist/pixi/ingest.d.ts +2 -1
  110. package/dist/pixi/ingest.d.ts.map +1 -1
  111. package/dist/pixi/ingest.js +4 -2
  112. package/dist/pixi/scene-capture.d.ts +9 -1
  113. package/dist/pixi/scene-capture.d.ts.map +1 -1
  114. package/dist/pixi/scene-capture.js +9 -6
  115. package/dist/react/world-state.d.ts +12 -0
  116. package/dist/react/world-state.d.ts.map +1 -1
  117. package/dist/react/world-state.js +14 -0
  118. package/dist/render/particles-factory.d.ts +13 -6
  119. package/dist/render/particles-factory.d.ts.map +1 -1
  120. package/dist/render/particles-factory.js +6 -3
  121. package/dist/render/soft-particle-depth.d.ts +3 -3
  122. package/dist/render/soft-particle-depth.d.ts.map +1 -1
  123. package/dist/render/soft-particle-depth.js +8 -13
  124. package/dist/runtime/create-runtime.d.ts +25 -62
  125. package/dist/runtime/create-runtime.d.ts.map +1 -1
  126. package/dist/runtime/create-runtime.js +58 -105
  127. package/dist/runtime/dev-build.d.ts +37 -0
  128. package/dist/runtime/dev-build.d.ts.map +1 -0
  129. package/dist/runtime/dev-build.js +49 -0
  130. package/dist/runtime/game.d.ts +38 -72
  131. package/dist/runtime/game.d.ts.map +1 -1
  132. package/dist/runtime/game.js +40 -55
  133. package/dist/runtime/mount-game.d.ts.map +1 -1
  134. package/dist/runtime/mount-game.js +12 -1
  135. package/dist/runtime/mount-manifest.d.ts +0 -13
  136. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  137. package/dist/runtime/mount-manifest.js +5 -25
  138. package/dist/runtime/types.d.ts +18 -0
  139. package/dist/runtime/types.d.ts.map +1 -1
  140. package/dist/setup/setup-renderer.d.ts +3 -4
  141. package/dist/setup/setup-renderer.d.ts.map +1 -1
  142. package/dist/setup/setup-renderer.js +3 -4
  143. package/dist/world3d-react/engine-bridge.d.ts +1 -1
  144. package/dist/world3d-react/engine-bridge.d.ts.map +1 -1
  145. package/dist/world3d-react/engine-bridge.js +29 -4
  146. package/dist/world3d-react/index.d.ts +1 -1
  147. package/dist/world3d-react/index.js +1 -1
  148. package/dist/world3d-react/r3f-adapter.d.ts +7 -0
  149. package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
  150. package/dist/world3d-react/r3f-adapter.js +132 -28
  151. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  152. package/dist/world3d-react/r3f-root-factory.js +14 -0
  153. package/dist/world3d-react/rapier-physics-bridge.d.ts +4 -4
  154. package/dist/world3d-react/rapier-physics-bridge.js +3 -3
  155. package/dist/world3d-react/world-context.d.ts +52 -3
  156. package/dist/world3d-react/world-context.d.ts.map +1 -1
  157. package/dist/world3d-react/world-context.js +94 -64
  158. package/dist-config/config.js.map +1 -1
  159. package/package.json +1 -1
  160. package/schemas/engine-capabilities.json +8 -8
  161. package/schemas/vgai-project.schema.json +1 -6
  162. package/src/adapter/adapter-module.ts +405 -72
  163. package/src/adapter/authoring-seam-contract.ts +211 -0
  164. package/src/adapter/authoring.ts +242 -50
  165. package/src/adapter/entrypoint-selection-source.ts +159 -0
  166. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +162 -36
  167. package/src/adapter/first-party-systems.ts +17 -3
  168. package/src/adapter/index.ts +51 -1
  169. package/src/adapter/ingest/contract-system-adapters.ts +26 -30
  170. package/src/adapter/ingest/game-contract-seams.ts +85 -0
  171. package/src/adapter/ingest/game-contract.ts +9 -4
  172. package/src/adapter/ingest/merge-debug-adapters.ts +193 -0
  173. package/src/adapter/ingest/observation-debug-adapter.ts +158 -0
  174. package/src/adapter/ingest/scene-capture.ts +126 -23
  175. package/src/adapter/ingest/visible-capture-window.ts +63 -9
  176. package/src/adapter/native-debug-module.ts +307 -0
  177. package/src/adapter/object3d-authoring-subject.ts +50 -0
  178. package/src/adapter/root-adapter.ts +25 -56
  179. package/src/adapter/root-seam-contract.ts +57 -0
  180. package/src/adapter/seam-evidence.ts +336 -0
  181. package/src/adapter/setup-three-root-adapter.ts +13 -15
  182. package/src/adapter/system-adapter.ts +6 -1
  183. package/src/adapter/system-seam-contract.ts +121 -0
  184. package/src/animation/xstate-animation-binding.ts +151 -4
  185. package/src/canvas-react/engine-bridge.ts +6 -6
  186. package/src/canvas-react/index.ts +2 -1
  187. package/src/canvas-react/pixi-react-adapter.tsx +143 -77
  188. package/src/canvas-react/pixi-react-root-factory.tsx +4 -0
  189. package/src/canvas-react/world-context.ts +26 -101
  190. package/src/data/curve.ts +2 -2
  191. package/src/dev/instruments.ts +4 -4
  192. package/src/dev/register-render-vitals.ts +2 -2
  193. package/src/dev/static-batch-advisor.ts +2 -2
  194. package/src/ecs/user-data.ts +5 -0
  195. package/src/input/input-manager.ts +145 -0
  196. package/src/manifest/load.ts +0 -7
  197. package/src/manifest/schema.ts +2 -25
  198. package/src/pixi/authoring.ts +329 -2
  199. package/src/pixi/ingest.ts +9 -2
  200. package/src/pixi/scene-capture.ts +20 -5
  201. package/src/react/world-state.tsx +15 -0
  202. package/src/render/particles-factory.ts +16 -6
  203. package/src/render/soft-particle-depth.ts +8 -13
  204. package/src/runtime/create-runtime.ts +79 -158
  205. package/src/runtime/dev-build.ts +49 -0
  206. package/src/runtime/game.ts +72 -113
  207. package/src/runtime/mount-game.ts +19 -1
  208. package/src/runtime/mount-manifest.ts +5 -37
  209. package/src/runtime/types.ts +22 -0
  210. package/src/setup/setup-renderer.ts +3 -4
  211. package/src/world3d-react/engine-bridge.ts +29 -4
  212. package/src/world3d-react/index.ts +1 -1
  213. package/src/world3d-react/r3f-adapter.tsx +140 -31
  214. package/src/world3d-react/r3f-root-factory.tsx +14 -0
  215. package/src/world3d-react/rapier-physics-bridge.tsx +4 -4
  216. package/src/world3d-react/world-context.ts +111 -87
  217. package/dist/runtime/dev-layers.d.ts +0 -34
  218. package/dist/runtime/dev-layers.d.ts.map +0 -1
  219. package/dist/runtime/dev-layers.js +0 -46
  220. package/src/runtime/dev-layers.ts +0 -46
@@ -100,18 +100,6 @@ export interface AdapterRegion {
100
100
  readonly id: string;
101
101
  /** What the host hands this root. */
102
102
  readonly surface: AdapterSurface;
103
- /**
104
- * `true` = this region grades the game's own DEV LAYER (the manifest root's
105
- * `dev: true`), not shipped game content.
106
- *
107
- * Carried here because a dev root occupies the same MEDIUM as the content
108
- * root beside it — a dev GUI is a `dom` root next to the HUD's `dom` root —
109
- * and anything that maps a subject to "the region on this surface" would
110
- * otherwise be choosing between them by array order. A region consumer that
111
- * means shipped content says so by skipping `dev` regions (the loader's
112
- * `regionForSurface` does), rather than by guessing which one matched.
113
- */
114
- readonly dev: boolean;
115
103
  /** Projection library, by name. */
116
104
  readonly projector: ProjectorName;
117
105
  /**
@@ -162,8 +150,68 @@ export interface AdapterRegion {
162
150
  * how a declaration slot turns back into a language.
163
151
  */
164
152
  readonly include?: readonly string[];
153
+ /**
154
+ * Surfaces this root mounts BESIDE {@link AdapterRegion.surface}, each with
155
+ * the source files that render on it. Together with `surface` — the manifest
156
+ * root's own adapter, which is always the PRIMARY — this states the root's
157
+ * FULL mounted surface set (decision, 2026-08-16; ARCHITECTURE-CORE §The
158
+ * editor protocol).
159
+ *
160
+ * A root is not always one surface. The shipped case is an R3F game whose
161
+ * `three` root mounts a canvas AND adopts a DOM HUD beside it
162
+ * (`adoptGameDomRoot` already adopts the pair together): its HUD modules
163
+ * render react-dom host elements, and `userData-oid` on a `<div>` is a prop
164
+ * react-dom does not recognize, so the editor must know they are the `dom`
165
+ * half. Before this slot there was nowhere to say so — `id` names the
166
+ * manifest root this region grades, so a second `dom` region for such a root
167
+ * would either name a root the host would try to MOUNT (fabrication, which
168
+ * the anti-shim rule forbids) or name no root at all.
169
+ *
170
+ * Each mount's `include` is REQUIRED and carries the same glob vocabulary as
171
+ * {@link AdapterRegion.include} (`**`, `*`, `?`). A mount with no files would
172
+ * declare a surface without saying which source renders on it, which answers
173
+ * no question any reader asks.
174
+ *
175
+ * READER: the same one — `resolveFileRegion`'s rung 1. A file claimed by a
176
+ * region's own `include` AND by a mount is a contradiction in the game's own
177
+ * declaration and settles nothing, exactly as two regions claiming one file
178
+ * do; it falls through to reach, and failing that to the caller's loud
179
+ * ambiguous path.
180
+ *
181
+ * {@link regionsFromManifestRoots} emits none: the manifest states one
182
+ * adapter per root and NOTHING infers a second surface from source text.
183
+ * Where the declaration and what a file renders disagree, the disagreement is
184
+ * reported as drift (the editor's `OID003`), never resolved by reading bytes.
185
+ */
186
+ readonly mounts?: readonly AdapterRegionMount[];
165
187
  }
166
188
 
189
+ /**
190
+ * One ADDITIONAL surface a root mounts, and the source files that render on it.
191
+ * See {@link AdapterRegion.mounts}.
192
+ */
193
+ export interface AdapterRegionMount {
194
+ /** The surface this part of the root mounts on. */
195
+ readonly surface: AdapterSurface;
196
+ /** Project-relative globs (`**`, `*`, `?`) naming the files that render here. */
197
+ readonly include: readonly string[];
198
+ }
199
+
200
+ const AdapterRegionMountSchema = z
201
+ .object({
202
+ surface: z
203
+ .enum(['three', 'canvas', 'dom'])
204
+ .describe('An additional surface this root mounts, beside the manifest root’s own adapter'),
205
+ include: z
206
+ .array(z.string().min(1))
207
+ .min(1)
208
+ .describe(
209
+ 'Project-relative globs (`**`, `*`, `?`) naming the source files that render on this ' +
210
+ 'additional surface',
211
+ ),
212
+ })
213
+ .strict();
214
+
167
215
  const AdapterRegionBasisSchema = z
168
216
  .object({
169
217
  up: z.enum(['y', 'z']).describe('World axis pointing away from the ground'),
@@ -175,12 +223,6 @@ const AdapterRegionSchema = z
175
223
  .object({
176
224
  id: z.string().min(1).describe('Manifest root id this region grades'),
177
225
  surface: z.enum(['three', 'canvas', 'dom']).describe('Render surface the host hands this root'),
178
- dev: z
179
- .boolean()
180
- .describe(
181
- "true = this region grades the game's own dev layer, not shipped content (the " +
182
- "manifest root's `dev: true`)",
183
- ),
184
226
  projector: z.enum(PROJECTOR_NAMES).describe('Projection library serving this surface, by name'),
185
227
  dialect: z
186
228
  .string()
@@ -204,6 +246,14 @@ const AdapterRegionSchema = z
204
246
  'Project-relative globs (`**`, `*`, `?`) naming source files this region owns, for files ' +
205
247
  'no root entry’s import closure reaches; a declared include wins over reach',
206
248
  ),
249
+ mounts: z
250
+ .array(AdapterRegionMountSchema)
251
+ .min(1)
252
+ .optional()
253
+ .describe(
254
+ 'Surfaces this root mounts BESIDE its own adapter, each with the files that render on ' +
255
+ 'them; with `surface`, the root’s full mounted surface set',
256
+ ),
207
257
  })
208
258
  .strict();
209
259
 
@@ -220,6 +270,73 @@ const RegionBindingSchema = z.union([
220
270
  z.array(AdapterRegionSchema).min(1),
221
271
  ]);
222
272
 
273
+ /**
274
+ * The per-region PARAMETERS the `'manifest-roots'` rule takes — see
275
+ * {@link AdapterDefinition.regionIncludes}. Exactly the two fields of an
276
+ * {@link AdapterRegion} the mechanical derivation cannot produce, because
277
+ * neither is a fact the manifest states: which files a region owns beyond
278
+ * its entry's reach, and which surfaces its root mounts beside its own.
279
+ */
280
+ export interface AdapterRegionOverlay {
281
+ /** See {@link AdapterRegion.include}. */
282
+ readonly include?: readonly string[];
283
+ /** See {@link AdapterRegion.mounts}. */
284
+ readonly mounts?: readonly AdapterRegionMount[];
285
+ }
286
+
287
+ const AdapterRegionOverlaySchema = z
288
+ .object({
289
+ include: z
290
+ .array(z.string().min(1))
291
+ .min(1)
292
+ .optional()
293
+ .describe(
294
+ 'Project-relative globs (`**`, `*`, `?`) naming source files this derived region owns, ' +
295
+ 'for files no root entry’s import closure reaches; a declared include wins over reach',
296
+ ),
297
+ mounts: z
298
+ .array(AdapterRegionMountSchema)
299
+ .min(1)
300
+ .optional()
301
+ .describe(
302
+ 'Surfaces this root mounts BESIDE its own adapter, each with the files that render on ' +
303
+ 'them; with the derived region’s own surface, the root’s full mounted surface set',
304
+ ),
305
+ })
306
+ .strict()
307
+ .refine((value) => value.include !== undefined || value.mounts !== undefined, {
308
+ message:
309
+ 'regionIncludes entry declares neither `include` nor `mounts` — an empty overlay states ' +
310
+ 'nothing the derivation did not already say. Remove it, or name the files it owns.',
311
+ });
312
+
313
+ /**
314
+ * PARAMETERS FOR THE RULE, keyed by manifest root id (decision, 2026-08-17;
315
+ * ARCHITECTURE-CORE §The editor protocol, "Declared includes LAYER onto the
316
+ * manifest-roots rule").
317
+ *
318
+ * The finder-selection shape, one level up: `regions: 'manifest-roots'` SELECTS
319
+ * the mechanical derivation, and this table DECLARES the per-region parameters
320
+ * it cannot derive. The two compose — {@link regionsFromManifestRoots} merges
321
+ * them — so a first-party project states one include without hand-writing its
322
+ * whole region table, and adding a root still grows the table for free.
323
+ *
324
+ * Illegal beside an explicit `regions` LIST, and rejected by name: that form is
325
+ * the full REPLACEMENT, and its entries carry `include`/`mounts` themselves.
326
+ * Two homes for one fact is how a declaration goes stale in one of them.
327
+ *
328
+ * MEASURED BASIS (PR #2281, verified twice): the region binding was
329
+ * replace-only, so no `nativeAdapter()` project could declare a single include
330
+ * without restating every region — which is what blocked the source-text R3F
331
+ * floor's retirement across the five shipped 3D projects.
332
+ */
333
+ const RegionIncludesSchema = z
334
+ .record(z.string().min(1), AdapterRegionOverlaySchema)
335
+ .describe(
336
+ 'Per-region parameters for the `manifest-roots` rule, keyed by manifest root id: the ' +
337
+ '`include` globs and `mounts` surfaces the mechanical derivation cannot produce',
338
+ );
339
+
223
340
  /**
224
341
  * The MECHANICAL derivation behind the `'manifest-roots'` rule: one region per
225
342
  * declared root, its surface and adapter identity read straight off the
@@ -228,13 +345,20 @@ const RegionBindingSchema = z.union([
228
345
  *
229
346
  * The per-surface library table below is an ENGINE fact, stated once:
230
347
  *
231
- * | root `adapter` | projector | dialect | anchors |
232
- * |-----------------------|-----------|---------------|------------------------------------|
233
- * | `three` | `three` | `r3f` | source-prop, construction-literal |
234
- * | `canvas` | `pixi` | `pixi-react` | source-prop, construction-literal |
235
- * | `dom` | `dom` | `jsx` | source-prop |
236
- * | `{module,surface}` | (surface) | `null` | live-only |
237
- * | `{ingest,surface}` | (surface) | `null` | live-only |
348
+ * | root `adapter` | projector | dialect | anchors |
349
+ * |-----------------------|-----------|---------------|------------------------------------------------------|
350
+ * | `three` | `three` | `r3f` | source-prop, source-structure, construction-literal |
351
+ * | `canvas` | `pixi` | `pixi-react` | source-prop, source-structure, construction-literal |
352
+ * | `dom` | `dom` | `jsx` | source-prop, source-structure |
353
+ * | `{module,surface}` | (surface) | `null` | live-only |
354
+ * | `{ingest,surface}` | (surface) | `null` | live-only |
355
+ *
356
+ * `source-structure` rides with every first-party dialect because all three
357
+ * write through the SAME structural door (`/__ui-source/struct`) — a region
358
+ * that can rewrite a JSX attribute in a file can rewrite that file's element
359
+ * tree. It is listed separately rather than folded into `source-prop` because
360
+ * they are different doors: a lane could lose one and keep the other, and the
361
+ * matrix has to be able to say so.
238
362
  *
239
363
  * A module/ingest root's truth is the game's own source, which no first-party
240
364
  * dialect writer authors — so it declares `live-only` rather than implying a
@@ -244,46 +368,82 @@ const RegionBindingSchema = z.union([
244
368
  * Every derived region carries {@link NATIVE_REGION_BASIS} outright, so the
245
369
  * editor's ground plane READS a declaration on the native path too — the
246
370
  * default is published, never assumed at the read site.
371
+ *
372
+ * It DERIVES no {@link AdapterRegion.include} and no {@link AdapterRegion.mounts},
373
+ * and there is no derivation that could: the manifest states ONE adapter per
374
+ * root and names only its entry, so a root's extra owned files and its second
375
+ * mounted surface are facts only the game can state. It states them as
376
+ * `regionIncludes` PARAMETERS, which this function merges onto the region it
377
+ * derives for that root — the rule stays selected, the parameters are declared
378
+ * (ARCHITECTURE-CORE §The editor protocol, "Declared includes LAYER onto the
379
+ * manifest-roots rule"). A key naming no declared root merges onto nothing;
380
+ * the LOADER reports that, because only it can see both sides.
247
381
  */
248
- export function regionsFromManifestRoots(roots: readonly ResolvedAdapterRoot[]): AdapterRegion[] {
249
- return roots.map((root) => {
250
- const surface = root.surface;
251
- const dev = root.dev;
252
- const firstParty = root.adapter.type === 'builtin';
253
- const projector: ProjectorName = surface === 'canvas' ? 'pixi' : surface;
254
- const basis = NATIVE_REGION_BASIS;
255
- if (!firstParty) {
256
- return {
257
- id: root.id,
258
- surface,
259
- dev,
260
- projector,
261
- dialect: null,
262
- anchors: ['live-only'] as const,
263
- basis,
264
- };
265
- }
266
- if (surface === 'dom') {
267
- return {
268
- id: root.id,
269
- surface,
270
- dev,
271
- projector,
272
- dialect: 'jsx',
273
- anchors: ['source-prop'] as const,
274
- basis,
275
- };
276
- }
382
+ export function regionsFromManifestRoots(
383
+ roots: readonly ResolvedAdapterRoot[],
384
+ regionIncludes: Readonly<Record<string, AdapterRegionOverlay>> = {},
385
+ ): AdapterRegion[] {
386
+ return roots.map((root) => ({
387
+ ...derivedRegion(root),
388
+ ...overlayOf(regionIncludes[root.id]),
389
+ }));
390
+ }
391
+
392
+ /** The declared half of a merged region — omitted keys leave the derived
393
+ * region untouched, so an overlay can only ADD what the rule cannot derive. */
394
+ function overlayOf(overlay: AdapterRegionOverlay | undefined): Partial<AdapterRegion> {
395
+ if (!overlay) return {};
396
+ return {
397
+ ...(overlay.include ? { include: overlay.include } : {}),
398
+ ...(overlay.mounts ? { mounts: overlay.mounts } : {}),
399
+ };
400
+ }
401
+
402
+ /** Region ids an overlay names that no declared root answers to. A typo here
403
+ * would otherwise be a declaration that silently does nothing, which is the
404
+ * failure class the whole declaration lane exists to end. */
405
+ export function unmatchedRegionIncludeIds(
406
+ roots: readonly ResolvedAdapterRoot[],
407
+ regionIncludes: Readonly<Record<string, AdapterRegionOverlay>>,
408
+ ): string[] {
409
+ const declared = new Set(roots.map((root) => root.id));
410
+ return Object.keys(regionIncludes).filter((id) => !declared.has(id));
411
+ }
412
+
413
+ /** One root's region, before any declared parameters are merged onto it. */
414
+ function derivedRegion(root: ResolvedAdapterRoot): AdapterRegion {
415
+ const surface = root.surface;
416
+ const firstParty = root.adapter.type === 'builtin';
417
+ const projector: ProjectorName = surface === 'canvas' ? 'pixi' : surface;
418
+ const basis = NATIVE_REGION_BASIS;
419
+ if (!firstParty) {
277
420
  return {
278
421
  id: root.id,
279
422
  surface,
280
- dev,
281
423
  projector,
282
- dialect: surface === 'three' ? 'r3f' : 'pixi-react',
283
- anchors: ['source-prop', 'construction-literal'] as const,
424
+ dialect: null,
425
+ anchors: ['live-only'] as const,
284
426
  basis,
285
427
  };
286
- });
428
+ }
429
+ if (surface === 'dom') {
430
+ return {
431
+ id: root.id,
432
+ surface,
433
+ projector,
434
+ dialect: 'jsx',
435
+ anchors: ['source-prop', 'source-structure'] as const,
436
+ basis,
437
+ };
438
+ }
439
+ return {
440
+ id: root.id,
441
+ surface,
442
+ projector,
443
+ dialect: surface === 'three' ? 'r3f' : 'pixi-react',
444
+ anchors: ['source-prop', 'source-structure', 'construction-literal'] as const,
445
+ basis,
446
+ };
287
447
  }
288
448
 
289
449
  // ---------------------------------------------------------------------------
@@ -292,9 +452,7 @@ export function regionsFromManifestRoots(roots: readonly ResolvedAdapterRoot[]):
292
452
 
293
453
  /**
294
454
  * The rule form of a finder's region targeting: every region the host mounts
295
- * from an exported composition (a root with an `entry`) that is not a declared
296
- * DEV layer. A dev root is the game's own dev GUI — the manifest already says
297
- * so (`dev: true`), so skipping it reads a declaration rather than guessing.
455
+ * from an exported composition a root with an `entry`.
298
456
  */
299
457
  export const EXPORTED_COMPOSITION_REGIONS = 'exported-composition-regions';
300
458
 
@@ -385,10 +543,44 @@ const FinderSelectionSchema = z.union([
385
543
  export type SceneReach =
386
544
  /** Mounting the region IS opening it — the single-composition degenerate case. */
387
545
  | { readonly kind: 'root-mount' }
388
- /** The entrypoint's own selection table reaches it under `key`. */
389
- | { readonly kind: 'entrypoint-selection'; readonly selection: string; readonly key: string }
546
+ /**
547
+ * The entrypoint's own selection table reaches it under `key`.
548
+ *
549
+ * `active` is the SECOND load-bearing fact about the same slot: the
550
+ * entrypoint indexes its table with one key right now (`scenes[activeScene]`),
551
+ * and that key's composition is literally what mounting the region renders.
552
+ * So an active entry is the region's own standing document — it NAMES that
553
+ * document rather than earning a second one beside it, exactly as
554
+ * `root-mount` does. Absent means "not known to be the one at the slot",
555
+ * which is the honest answer whenever the key cannot be read statically; it
556
+ * is never a guess, and it is per-entry because two regions each have their
557
+ * own slot with its own occupant.
558
+ */
559
+ | {
560
+ readonly kind: 'entrypoint-selection';
561
+ readonly selection: string;
562
+ readonly key: string;
563
+ readonly active?: boolean;
564
+ }
390
565
  /** A portable story mounts it in isolation. */
391
566
  | { readonly kind: 'story'; readonly storyId: string }
567
+ /**
568
+ * The RUNNING game's own published scenes contract reaches it, under
569
+ * `sceneId` (`adapter/ingest/game-contract.ts`'s `scenes.list()` ids).
570
+ *
571
+ * This is the LIVE reach: opening it while playing means asking the game to
572
+ * navigate, the way its own buttons do. An authorable entry that also names
573
+ * `source` can independently own an Edit isolation document; reach never
574
+ * decides whether a source composition can be mounted as a piece.
575
+ *
576
+ * A DECLARATION, never an inference: the host does not sniff a mapping
577
+ * between a table entry and a contract id, and it does not assume the two
578
+ * vocabularies coincide. The adapter states which contract id this entry is,
579
+ * and the host validates that id against the game's OWN `list()` at the
580
+ * moment it navigates — so a game that stops publishing the scene refuses by
581
+ * name instead of navigating somewhere else.
582
+ */
583
+ | { readonly kind: 'game-contract'; readonly sceneId: string }
392
584
  /** Declared, with no code path that opens it. */
393
585
  | { readonly kind: 'none'; readonly reason: string };
394
586
 
@@ -399,9 +591,11 @@ const SceneReachSchema = z.union([
399
591
  kind: z.literal('entrypoint-selection'),
400
592
  selection: z.string().min(1),
401
593
  key: z.string().min(1),
594
+ active: z.boolean().optional(),
402
595
  })
403
596
  .strict(),
404
597
  z.object({ kind: z.literal('story'), storyId: z.string().min(1) }).strict(),
598
+ z.object({ kind: z.literal('game-contract'), sceneId: z.string().min(1) }).strict(),
405
599
  z.object({ kind: z.literal('none'), reason: z.string().min(1) }).strict(),
406
600
  ]);
407
601
 
@@ -437,6 +631,10 @@ export interface SceneEntry {
437
631
  readonly authorable: boolean;
438
632
  readonly reach: SceneReach;
439
633
  readonly source?: SceneSource;
634
+ /** Optional adapter-declared prerequisite run before `source` is constructed
635
+ * in Edit isolation. The export is a zero-argument function; the host never
636
+ * infers setup from neighboring filenames or game globals. */
637
+ readonly isolationSetup?: SceneSource;
440
638
  /** Which finder produced it; absent = the adapter stated it outright. */
441
639
  readonly finder?: FinderName;
442
640
  }
@@ -450,6 +648,7 @@ const SceneEntrySchema = z
450
648
  authorable: z.boolean(),
451
649
  reach: SceneReachSchema,
452
650
  source: SceneSourceSchema.optional(),
651
+ isolationSetup: SceneSourceSchema.optional(),
453
652
  finder: z.enum(FINDER_NAMES).optional(),
454
653
  })
455
654
  .strict();
@@ -481,32 +680,128 @@ const AdapterSceneTableSchema = z
481
680
  // Observation
482
681
  // ---------------------------------------------------------------------------
483
682
 
683
+ /**
684
+ * The two faces a declaration projects onto. They are exactly the two
685
+ * `DebugAdapter` halves (`adapter/system-adapter.ts`): `state` becomes a
686
+ * provider (`providers()` / `state(name)`), `command` becomes a verb
687
+ * (`commands()` / `invoke(name, args)`). There is deliberately no third kind —
688
+ * see {@link OBSERVATION_KIND_REFUSAL}.
689
+ */
690
+ export const OBSERVATION_KINDS = ['command', 'state'] as const;
691
+ export type ObservationKind = (typeof OBSERVATION_KINDS)[number];
692
+
693
+ /**
694
+ * Why a declaration may not name a `SystemAdapters` slot here.
695
+ *
696
+ * A game's system slots are already declared through the game contract's own
697
+ * `systemAdapters` carrier (`adapter/ingest/game-contract.ts`), which validates
698
+ * each slot per-surface and reports a malformed one BY NAME
699
+ * (`contract-system-adapters.ts`). A second door onto the same slots is exactly
700
+ * the two-declarations-one-slot problem `game-contract.ts` records for `debug`,
701
+ * where it says outright that a second door "would let a game declare two
702
+ * different debug planes with no rule for which wins".
703
+ */
704
+ const OBSERVATION_KIND_REFUSAL =
705
+ "observation.kind must be 'command' or 'state' — the two DebugAdapter faces a declaration " +
706
+ 'projects onto. A SystemAdapters slot is declared through the game contract’s `systemAdapters` ' +
707
+ 'carrier, which validates it per-surface; declaring one here would be a second door onto the ' +
708
+ 'same slot with no rule for which wins.';
709
+
484
710
  /**
485
711
  * One statically declared observation slot. Declaring a slot and ANSWERING it
486
712
  * live are separate facts (the two-state rule): a slot declared here whose
487
- * `answer` throws or whose game never booted reads as broken, never as
488
- * "the game has none".
713
+ * `answer` throws, whose `answer` is absent, or whose game never booted reads as
714
+ * broken, never as "the game has none". The projector
715
+ * (`adapter/ingest/observation-debug-adapter.ts`) keeps that distinction: an
716
+ * unanswerable slot is still LISTED, and refuses by name when read.
489
717
  */
490
718
  export interface ObservationDeclaration {
491
719
  readonly id: string;
492
- readonly kind: 'system' | 'command' | 'state';
720
+ readonly kind: ObservationKind;
721
+ readonly description?: string | undefined;
722
+ /** Command execution side; omitted means a local app-owned command. */
723
+ readonly locus?: 'client' | 'server' | undefined;
493
724
  /**
494
725
  * Lazily evaluated against the mounted game. The field may be a closure; the
495
726
  * TABLE around it stays statically readable, which is the whole discipline.
727
+ *
728
+ * What `game` IS is the host's to hand over and is stated at the projector:
729
+ * for an ingest mount it is the REALM the game's own modules ran in, because
730
+ * a foreign game's public handles are the only thing the host can honestly
731
+ * pass (`observation-debug-adapter.ts`).
496
732
  */
497
- readonly answer?: (game: unknown) => unknown;
733
+ readonly answer?: (game: unknown, ...args: unknown[]) => unknown;
498
734
  }
499
735
 
500
736
  const ObservationDeclarationSchema = z
501
737
  .object({
502
738
  id: z.string().min(1),
503
- kind: z.enum(['system', 'command', 'state']),
739
+ kind: z.enum(OBSERVATION_KINDS, { error: () => OBSERVATION_KIND_REFUSAL }),
740
+ description: z.string().min(1).optional(),
741
+ locus: z.enum(['client', 'server']).optional(),
504
742
  answer: z
505
743
  .custom<(game: unknown) => unknown>((value) => typeof value === 'function', {
506
744
  message: 'observation.answer must be a function evaluated against the mounted game',
507
745
  })
508
746
  .optional(),
509
747
  })
748
+ .strict()
749
+ .refine((value) => value.kind === 'command' || value.locus === undefined, {
750
+ path: ['locus'],
751
+ message: 'observation.locus is valid only for a command declaration',
752
+ });
753
+
754
+ // ---------------------------------------------------------------------------
755
+ // Native input
756
+ // ---------------------------------------------------------------------------
757
+
758
+ /** The action value shapes the session input door already speaks. */
759
+ export const ADAPTER_INPUT_VALUE_TYPES = [
760
+ 'digital',
761
+ 'scalar',
762
+ 'vector2',
763
+ 'pointerDelta',
764
+ 'pointerPosition',
765
+ ] as const;
766
+ export type AdapterInputValueType = (typeof ADAPTER_INPUT_VALUE_TYPES)[number];
767
+ export type AdapterInputValue = boolean | number | { readonly x: number; readonly y: number };
768
+
769
+ /** One action in an app-owned input store. */
770
+ export interface AdapterInputAction {
771
+ readonly name: string;
772
+ readonly valueType: AdapterInputValueType;
773
+ }
774
+
775
+ /**
776
+ * Adapter-side binding from the universal session input door to a game's
777
+ * ordinary input store. The game owns both its physical-device listeners and
778
+ * its state; this table only tells the host how to drive that same state for a
779
+ * bot or live REPL session. No component imports this contract.
780
+ */
781
+ export interface AdapterInputBinding {
782
+ /** Manifest root whose native scheduler consumes these actions. */
783
+ readonly root: string;
784
+ /** Current action vocabulary. A closure permits an app-owned dynamic map. */
785
+ readonly actions: (game: unknown) => readonly AdapterInputAction[];
786
+ /** Apply one action through the app's own input-store write path. */
787
+ readonly set: (game: unknown, action: string, value: AdapterInputValue) => void;
788
+ /** Release every app-owned virtual action. */
789
+ readonly clear: (game: unknown) => void;
790
+ }
791
+
792
+ const AdapterInputBindingSchema = z
793
+ .object({
794
+ root: z.string().min(1),
795
+ actions: z.custom<AdapterInputBinding['actions']>((value) => typeof value === 'function', {
796
+ message: 'input.actions must be a function returning the app-owned action vocabulary',
797
+ }),
798
+ set: z.custom<AdapterInputBinding['set']>((value) => typeof value === 'function', {
799
+ message: 'input.set must be a function writing the app-owned input store',
800
+ }),
801
+ clear: z.custom<AdapterInputBinding['clear']>((value) => typeof value === 'function', {
802
+ message: 'input.clear must be a function releasing app-owned virtual actions',
803
+ }),
804
+ })
510
805
  .strict();
511
806
 
512
807
  // ---------------------------------------------------------------------------
@@ -516,8 +811,16 @@ const ObservationDeclarationSchema = z
516
811
  /** The adapter module's default export, after {@link defineAdapter}. */
517
812
  export interface AdapterDefinition {
518
813
  readonly regions: RegionBinding;
814
+ /**
815
+ * Per-region parameters for the `'manifest-roots'` RULE, keyed by manifest
816
+ * root id — the `include` globs and `mounts` surfaces the mechanical
817
+ * derivation cannot produce. {@link regionsFromManifestRoots} merges them;
818
+ * an explicit `regions` LIST carries its own and rejects these by name.
819
+ */
820
+ readonly regionIncludes: Readonly<Record<string, AdapterRegionOverlay>>;
519
821
  readonly scenes: AdapterSceneTable;
520
822
  readonly observation: readonly ObservationDeclaration[];
823
+ readonly input?: AdapterInputBinding | undefined;
521
824
  }
522
825
 
523
826
  /** What a game writes. Every field optional — the near-empty adapter is the
@@ -525,17 +828,44 @@ export interface AdapterDefinition {
525
828
  * native. */
526
829
  export interface AdapterDefinitionInput {
527
830
  readonly regions?: RegionBinding;
831
+ readonly regionIncludes?: Readonly<Record<string, AdapterRegionOverlay>>;
528
832
  readonly scenes?: AdapterSceneTable;
529
833
  readonly observation?: readonly ObservationDeclaration[];
834
+ readonly input?: AdapterInputBinding | undefined;
530
835
  }
531
836
 
837
+ /**
838
+ * Why the two forms may not be combined, said in the error a game's author
839
+ * reads. An explicit region list REPLACES the derivation and its entries carry
840
+ * `include`/`mounts` themselves; layering a second table over it would put one
841
+ * fact in two homes, with no rule for which wins.
842
+ */
843
+ const REGION_INCLUDES_WITH_LIST_REFUSAL =
844
+ '`regionIncludes` states parameters for the `manifest-roots` RULE, so it cannot be paired ' +
845
+ 'with an explicit `regions` list — that form is the full replacement, and each entry carries ' +
846
+ 'its own `include`/`mounts`. Move these globs onto the matching `regions[]` entry, or drop the ' +
847
+ 'list and let the derivation run.';
848
+
532
849
  export const AdapterDefinitionSchema = z
533
850
  .object({
534
851
  regions: RegionBindingSchema.optional(),
852
+ regionIncludes: RegionIncludesSchema.optional(),
535
853
  scenes: AdapterSceneTableSchema.optional(),
536
854
  observation: z.array(ObservationDeclarationSchema).optional(),
855
+ input: AdapterInputBindingSchema.optional(),
537
856
  })
538
- .strict();
857
+ .strict()
858
+ // An EMPTY table beside a list is fine and must stay fine: `defineAdapter`
859
+ // publishes `regionIncludes: {}` on every definition, and its output is
860
+ // parsed a second time by the host's loader — a refusal on the key's mere
861
+ // presence would reject the module's own valid output on the round trip.
862
+ .refine(
863
+ (value) =>
864
+ value.regionIncludes === undefined ||
865
+ Object.keys(value.regionIncludes).length === 0 ||
866
+ !Array.isArray(value.regions),
867
+ { message: REGION_INCLUDES_WITH_LIST_REFUSAL },
868
+ );
539
869
 
540
870
  /**
541
871
  * Validate and freeze a game's binding table.
@@ -557,8 +887,10 @@ export function defineAdapter(input: AdapterDefinitionInput = {}): AdapterDefini
557
887
  }
558
888
  return Object.freeze({
559
889
  regions: input.regions ?? 'manifest-roots',
890
+ regionIncludes: Object.freeze({ ...(input.regionIncludes ?? {}) }),
560
891
  scenes: Object.freeze({ ...(input.scenes ?? {}) }),
561
892
  observation: Object.freeze([...(input.observation ?? [])]),
893
+ ...(input.input ? { input: input.input } : {}),
562
894
  });
563
895
  }
564
896
 
@@ -566,8 +898,9 @@ export function defineAdapter(input: AdapterDefinitionInput = {}): AdapterDefini
566
898
  export interface NativeAdapterOptions {
567
899
  /** Scene-table entries this game states outright, beside what the finders find. */
568
900
  readonly scenes?: readonly SceneEntry[];
569
- /** Observation slots this game declares. */
570
- readonly observation?: readonly ObservationDeclaration[];
901
+ /** Per-region `include`/`mounts` parameters for the derivation — see
902
+ * {@link AdapterDefinition.regionIncludes}. */
903
+ readonly regionIncludes?: Readonly<Record<string, AdapterRegionOverlay>>;
571
904
  }
572
905
 
573
906
  /**
@@ -580,6 +913,7 @@ export interface NativeAdapterOptions {
580
913
  export function nativeAdapter(options: NativeAdapterOptions = {}): AdapterDefinition {
581
914
  return defineAdapter({
582
915
  regions: 'manifest-roots',
916
+ ...(options.regionIncludes ? { regionIncludes: options.regionIncludes } : {}),
583
917
  scenes: {
584
918
  ...(options.scenes ? { entries: options.scenes } : {}),
585
919
  find: [
@@ -587,7 +921,6 @@ export function nativeAdapter(options: NativeAdapterOptions = {}): AdapterDefini
587
921
  { finder: 'prefabsFromStories' },
588
922
  ],
589
923
  },
590
- ...(options.observation ? { observation: options.observation } : {}),
591
924
  });
592
925
  }
593
926