@vgai/engine 0.5.15 → 0.5.16

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