@volter/project 0.0.0-stage → 0.5.203

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 (53) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +18 -3
  4. package/package.json +64 -4
  5. package/schemas/engine-api.json +57 -0
  6. package/schemas/engine-api.md +22 -0
  7. package/schemas/volter-project.schema.json +468 -0
  8. package/schemas/volter-settings.schema.json +149 -0
  9. package/src/adapter/adapter-module.ts +997 -0
  10. package/src/adapter/adapter-surface.ts +34 -0
  11. package/src/adapter/authoring-seam-contract.ts +224 -0
  12. package/src/adapter/authoring.ts +1336 -0
  13. package/src/adapter/binding.ts +445 -0
  14. package/src/adapter/debug-registry-ref.ts +19 -0
  15. package/src/adapter/editor-looks.ts +519 -0
  16. package/src/adapter/finders/finder-result.ts +29 -0
  17. package/src/adapter/finders/registry.ts +116 -0
  18. package/src/adapter/frame-capture.ts +80 -0
  19. package/src/adapter/host-context.ts +114 -0
  20. package/src/adapter/index.ts +209 -0
  21. package/src/adapter/ingest/contract-system-slots.ts +28 -0
  22. package/src/adapter/ingest/game-contract-seams.ts +86 -0
  23. package/src/adapter/ingest/game-contract.ts +506 -0
  24. package/src/adapter/ingest/mount-readiness.ts +140 -0
  25. package/src/adapter/loop-gate-report.ts +53 -0
  26. package/src/adapter/manifest-interpreter.ts +64 -0
  27. package/src/adapter/native-entry-surface.ts +100 -0
  28. package/src/adapter/physics-adapter-2d.ts +30 -0
  29. package/src/adapter/render-audio.ts +20 -0
  30. package/src/adapter/render-memory.ts +34 -0
  31. package/src/adapter/renderer-config.ts +84 -0
  32. package/src/adapter/root-adapter.ts +243 -0
  33. package/src/adapter/root-seam-contract.ts +61 -0
  34. package/src/adapter/root-stacking.ts +67 -0
  35. package/src/adapter/seam-evidence.ts +336 -0
  36. package/src/adapter/system-adapter.ts +865 -0
  37. package/src/adapter/system-seam-contract.ts +138 -0
  38. package/src/adapter/system-slot.ts +301 -0
  39. package/src/adapter/transform.ts +35 -0
  40. package/src/adapter/workspace-arrangement.ts +92 -0
  41. package/src/core/system-phase.ts +52 -0
  42. package/src/inspection.ts +281 -0
  43. package/src/manifest/configuration-kinds.ts +241 -0
  44. package/src/manifest/editor-port.ts +69 -0
  45. package/src/manifest/filename.ts +45 -0
  46. package/src/manifest/kind-modules.ts +58 -0
  47. package/src/manifest/load-file.ts +33 -0
  48. package/src/manifest/load.ts +380 -0
  49. package/src/manifest/locate.ts +55 -0
  50. package/src/manifest/runtime-environment.ts +48 -0
  51. package/src/manifest/schema.ts +690 -0
  52. package/src/settings/keys.ts +152 -0
  53. package/src/settings/schema.ts +151 -0
@@ -0,0 +1,1336 @@
1
+ /**
2
+ * AuthoringAdapter — the editor's authoring contract. The editor talks to THIS,
3
+ * keyed by opaque string node ids, instead of to a concrete document format.
4
+ *
5
+ * An implementer's document model is its own private business; an ingested
6
+ * game's adapter implements the same
7
+ * providers directly over its live `Object3D` tree. The editor sees only the
8
+ * interface and the advertised `capabilities` — it never branches on which
9
+ * implementer it is talking to.
10
+ *
11
+ * The interface lives in the engine (not the editor) so `MountedThreeRoot.authoring`
12
+ * can reference it without the engine depending on the editor; implementers live
13
+ * in `packages/editor/src/authoring/`.
14
+ *
15
+ * **Reserved inspector paths convention:** the property paths `name`, `visible`,
16
+ * `locked`, `renderVisible` and `exclude` are reserved — an adapter that
17
+ * supports them exposes them through the ordinary `InspectorProvider.get`/`set`
18
+ * (no separate provider). The shell renders the hierarchy panel's rename field,
19
+ * eye (visibility) toggle, lock toggle, render-camera toggle and
20
+ * exclude checkbox against these paths, generically, for any adapter that
21
+ * reports them via `InspectorProvider.properties`.
22
+ *
23
+ * A COLUMN FOLLOWS WHAT THE ADAPTER ANSWERS, never a chrome knob (owner ruling,
24
+ * 2026-09-19). `renderVisible` and `exclude` are answered by almost nothing —
25
+ * three.js has ONE `Object3D.visible` governing viewport and render alike, and
26
+ * nothing but a view layer has an exclusion — so their columns are absent or a
27
+ * reserved blank cell, and appear the moment an adapter whose truth really does
28
+ * separate them says so. `exclude` and `renderVisible` are reported in the
29
+ * sense their name carries: `exclude: true` means EXCLUDED, `renderVisible:
30
+ * false` means hidden from renders.
31
+ */
32
+
33
+ import type { Transform } from './transform';
34
+
35
+ /**
36
+ * Booleans the editor UI gates affordances on (hide what an adapter can't do).
37
+ *
38
+ * Deliberately SMALL: a capability flag earns its place only when the shell
39
+ * actually branches on it. Structural affordances (create/delete/reparent) are
40
+ * gated by `structure` PROVIDER PRESENCE instead — the flag and the provider
41
+ * cannot disagree that way.
42
+ */
43
+ export interface AuthoringCapabilities {
44
+ transform: boolean;
45
+ inspectorFields: boolean;
46
+ persist: boolean;
47
+ }
48
+
49
+ /**
50
+ * The adapter's own answer to "what is truth behind these rows, and how
51
+ * writable is it" (spec 29 §5). Provenance never varies WITHIN one adapter's
52
+ * rows — it is a property of the world boundary — so it is declared ONCE per
53
+ * adapter and rendered at the SEAM (the composite's `world:<id>` group row,
54
+ * or the panel header for a bare adapter), never per row. Only the adapter
55
+ * knows its truth: the shell must read this field, never guess from adapter
56
+ * identity (rule zero). Machine-readable operation gates stay in {@link
57
+ * AuthoringCapabilities}/provider presence — this type carries the HUMAN
58
+ * explanation those gates can point at when an affordance is present but
59
+ * unavailable.
60
+ */
61
+ export interface AuthoringProvenance {
62
+ /**
63
+ * What the rows project:
64
+ * - `document` — an authored data file the editor owns (e.g. a scene file);
65
+ * - `source-code` — the game's own source is the document (e.g. JSX);
66
+ * - `foreign` — a live tree the editor does not own (an unmodified external
67
+ * game, or any world edited only through an overlay); edits go to the
68
+ * overlay, never the source;
69
+ * - `live` — a running game adopted at play time; ordinary edits are not persisted;
70
+ * - `boundary` — a declared world with no live editing surface here.
71
+ */
72
+ source: 'document' | 'source-code' | 'foreign' | 'live' | 'boundary';
73
+ /** Short badge text the shell shows verbatim at the seam (e.g. "scene",
74
+ * "jsx", "overlay", "read-only", "live"). */
75
+ label: string;
76
+ /** One sentence explaining the truth/writability — shown as the seam
77
+ * badge's tooltip and as the reason on disabled affordances. */
78
+ detail: string;
79
+ }
80
+
81
+ /**
82
+ * Semantic place a node occupies in the universal authoring hierarchy.
83
+ *
84
+ * `kind` below remains the adapter's native kind (`mesh`, `button`,
85
+ * `pixi-container`, ...). `role` says what the row MEANS to the editor shell,
86
+ * so presentation and interaction never have to infer semantics from an id,
87
+ * label, or substrate-specific kind string.
88
+ */
89
+ export type EditorNodeRole =
90
+ | 'folder'
91
+ | 'root'
92
+ | 'document'
93
+ | 'story'
94
+ | 'component'
95
+ | 'instance'
96
+ | 'element'
97
+ | 'entity'
98
+ | 'boundary';
99
+
100
+ /** A node in the authoring hierarchy — format-neutral. */
101
+ export interface EditorNode {
102
+ /** STABLE id — survives reload (see ingest structural-path ids). */
103
+ id: string;
104
+ label: string;
105
+ /** Adapter-declared semantic role. Optional for third-party/older adapters;
106
+ * the shell falls back to the native `kind` only for icon selection. */
107
+ role?: EditorNodeRole;
108
+ /** Optional source-owned context kept visually subordinate to `label`, such
109
+ * as `<header>` beneath a React component name or a document's file path. */
110
+ secondaryLabel?: string;
111
+ kind: 'mesh' | 'light' | 'camera' | 'audio' | 'group' | 'object' | (string & {});
112
+ /**
113
+ * What to CALL this node's type in the inspector header — the component or
114
+ * class an author thinks of it as, when that differs from the native `kind`
115
+ * the shell uses to pick an icon. An `<Enemy>` renders a `group`, but "group"
116
+ * is a fact about its implementation, not its identity; Godot names the class
117
+ * here and Unity names the prefab. Adapters that omit it keep showing `kind`.
118
+ */
119
+ typeLabel?: string;
120
+ parentId: string | null;
121
+ childIds: string[];
122
+ /**
123
+ * THIS ADAPTER'S OWN DEFAULT OPEN STATE for this row, overriding the shell's
124
+ * shape heuristic (`GameHierarchy.tsx`'s `defaultExpansionByNode`, which
125
+ * infers openness from authored depth and branch shape).
126
+ *
127
+ * It exists because one adapter genuinely knows better than any shape rule
128
+ * can: Blender's Outliner has an explicit per-element flag
129
+ * (`outliner_tree.cc:139` makes every fresh element CLOSED; only the Scene
130
+ * Collection and the layer collections clear it,
131
+ * `tree_display_view_layer.cc:130,167`), so "collections open, objects
132
+ * closed" is a fact about the tree rather than a guess about it. Omitted —
133
+ * which is every other adapter — leaves the heuristic exactly as it was.
134
+ */
135
+ defaultExpanded?: boolean;
136
+ /** Opaque project-owned identity shared by authored rows across adapter roots. */
137
+ crossSurfaceId?: string;
138
+ /** Optional authored parent identity resolved against another row's cross-surface id. */
139
+ crossSurfaceParentId?: string;
140
+ /** Source-owned sibling ordinal used only when native rows are joined across surfaces. */
141
+ crossSurfaceOrder?: number;
142
+ /**
143
+ * Live source-owned label for the projected group containing this row's adapter root.
144
+ * Any row may carry it: a native surface can have no semantic root of its own when all of
145
+ * its authored rows are children of entities owned by another surface.
146
+ */
147
+ crossSurfaceGroupLabel?: string;
148
+ }
149
+
150
+ export interface PropertyDescriptor {
151
+ path: string;
152
+ label: string;
153
+ type: 'string' | 'number' | 'boolean' | 'vec3' | 'color' | 'enum' | 'asset' | 'json';
154
+ readonly?: boolean;
155
+ /**
156
+ * Why this field cannot be written, in the adapter's own vocabulary. The
157
+ * Inspector, its control surface, and the write refusal all project this
158
+ * same sentence; omitting it uses the generic read-only explanation.
159
+ */
160
+ readonlyReason?: string;
161
+ options?: unknown[];
162
+ /**
163
+ * Optional domain-shaped grouping (T3.4 slice 2): properties sharing the
164
+ * same `group` label render together under a titled sub-section in the
165
+ * generic inspector, instead of the flat property grid. Backward-compatible
166
+ * — omitted (or two descriptors with different/no `group`) renders exactly
167
+ * as before this field existed. This is the ONLY authoring-richness
168
+ * addition v1 makes; there is no schema and no new PropertyDescriptor
169
+ * `type` — a custom adapter reports its own domain concepts
170
+ * (health/score/an enum, …) through the SAME provider, merely labeled into
171
+ * a named group.
172
+ */
173
+ group?: string;
174
+ /**
175
+ * The value shown is the DECLARED DEFAULT — the document does not carry this
176
+ * property, the thing that owns it does. Every mainstream inspector
177
+ * distinguishes these (Unity greys an unmodified field and bolds an
178
+ * overridden one) because otherwise you cannot tell what a document actually
179
+ * says from what it merely inherits. Editing a defaulted property writes it
180
+ * into the document for the first time.
181
+ */
182
+ defaulted?: boolean;
183
+ /**
184
+ * This property can be REMOVED from the document, reverting it to the
185
+ * declared default — Unity's revert arrow, Godot's reset arrow. Only set when
186
+ * removal is legal: the owner declares the property optional (or gives it a
187
+ * default), so dropping it leaves a still-valid document. The inverse of
188
+ * editing a `defaulted` property.
189
+ */
190
+ resettable?: boolean;
191
+ /**
192
+ * The SOURCE TEXT of the default a revert returns this property to (`1.55`,
193
+ * `'raider'`), so the revert affordance can name its destination instead of
194
+ * saying "default" and hoping.
195
+ *
196
+ * Set ONLY when the owner actually declares a default the adapter can read.
197
+ * Absence is the honest, common case: an optional property with no declared
198
+ * default is still removable — what takes over is then the owner's own
199
+ * internal fallback or nothing at all, which this side does not know. Never
200
+ * fill it with the property's CURRENT value: that is what the author is
201
+ * discarding, not what they get back.
202
+ */
203
+ revertsTo?: string;
204
+ }
205
+
206
+ export interface HierarchyProvider {
207
+ roots(): EditorNode[];
208
+ node(id: string): EditorNode | null;
209
+ /**
210
+ * OPTIONAL semantic-join revision. A composite hierarchy uses explicit
211
+ * `crossSurface*` fields to join native roots; this signature changes if and
212
+ * only if those fields, their semantic ancestry, or their order changed.
213
+ * Ordinary native-only child churn may leave it stable. `null` explicitly
214
+ * means this provider has no semantic participation. Omission means the
215
+ * composite must conservatively re-read the provider's whole hierarchy.
216
+ */
217
+ crossSurfaceStructureSignature?(): string | null;
218
+ /**
219
+ * OPTIONAL, THREE-SURFACE — the live scene object for raycast/gizmo binding
220
+ * (null for a node this adapter has no object for), opaque here: its Three
221
+ * typing is `@volter/editor-threejs`'s `threeHierarchy`. A Pixi, DOM or React
222
+ * adapter has no scene object at all: it OMITS this method rather than
223
+ * implementing a `() => null` stub that pretends the concept applies. Callers
224
+ * already treat `null` as "no object here", so absence and `null` mean the
225
+ * same thing to them — optional-chain it (`hierarchy.object3D?.(id) ?? null`).
226
+ */
227
+ object3D?(id: string): unknown;
228
+ /** OPTIONAL, THREE-SURFACE — inverse of {@link object3D}. Same rule: an
229
+ * adapter with no scene-object tree omits it. */
230
+ idForObject3D?(o: unknown): string | null;
231
+ }
232
+
233
+ export interface SelectionProvider {
234
+ get(): string[];
235
+ /** Set the selected authoring subjects. `exact` is for projections that
236
+ * already name a real node (for example a folded-internal light inventory);
237
+ * the default keeps the adapter's normal semantic component resolution. */
238
+ set(ids: string[], options?: { intent?: 'semantic' | 'exact' }): void;
239
+ /** Resolve a raw native/render hit to the semantic authoring subject. The
240
+ * shell must not infer ownership from ancestry or substrate-specific kinds.
241
+ * Older adapters may omit this; raw ids then remain the selection ids. */
242
+ resolve?(
243
+ rawId: string,
244
+ options?: { intent?: 'normal' | 'deep'; scopeId?: string | null },
245
+ ): SelectionResolution | null;
246
+ }
247
+
248
+ /** Adapter-owned result of semantic selection resolution: the raw native/render
249
+ * hit mapped to the node the author actually means. ONE id, deliberately:
250
+ * separate inspector/transform/boundary aliases would have to agree, and
251
+ * nothing in the shell can enforce that. */
252
+ export interface SelectionResolution {
253
+ /** Hierarchy selection and Inspector subject. */
254
+ id: string;
255
+ }
256
+
257
+ export type TransformChannel = 'position' | 'rotation' | 'scale';
258
+
259
+ export interface TransformEditability {
260
+ writable: boolean;
261
+ /** Human-facing explanation for a disabled gizmo/field. */
262
+ reason?: string;
263
+ /**
264
+ * Is there an AUTHORED override of this channel that {@link
265
+ * TransformProvider.remove} could drop? The per-channel analogue of a
266
+ * property descriptor's `resettable`, and it is answered the same way: the
267
+ * document actually carries this channel, and this session has a door that
268
+ * can take it back.
269
+ *
270
+ * It lives HERE rather than being inferred from `remove`'s presence because
271
+ * `remove` is a lane-wide fact while this is a per-subject one — a composite
272
+ * routes both, but only this one can say that THIS node's `position` is
273
+ * authored and THAT node's is not. Omitted ⇒ nothing to drop, and the
274
+ * protocol's removal door refuses by name instead of acking a revert it did
275
+ * not perform.
276
+ *
277
+ * EACH HALF IS ANSWERED BY WHOEVER CAN SEE IT, and a lane that cannot read
278
+ * its own document may not invent the first. A source lane holding the
279
+ * file's text answers both (the first-party R3F lane reads the callsite's
280
+ * attributes before saying yes). A lane whose client holds only an
281
+ * oid → file:line index and writes through a server that resolves the
282
+ * address — the vendored-ingest lane — can honestly answer only the SECOND,
283
+ * so that is what it answers, and `remove`'s own {@link WriteAck} reports
284
+ * whether an attribute was actually dropped (`persisted: false` when the
285
+ * callsite carried none). The inverse is what is forbidden on every lane:
286
+ * acking a drop that took no byte.
287
+ */
288
+ removable?: boolean;
289
+ }
290
+
291
+ /** The one lawful Play→source write: a person explicitly attributes the
292
+ * selected live node's current transform to its OID-anchored source literal.
293
+ * This is an atomic optional group so availability and the write door cannot
294
+ * drift apart. Ordinary transform gestures remain governed by begin/apply/end
295
+ * and therefore stay ephemeral while Play is running. */
296
+ export interface TransformSourceCommitProvider {
297
+ availability(id: string): { available: boolean; reason?: string };
298
+ commit(id: string): Promise<WriteAck>;
299
+ }
300
+
301
+ export interface TransformProvider {
302
+ /** Honest inspector presentation for the substrate's transform vocabulary. */
303
+ dimensions?(id: string): '2d' | '3d' | null;
304
+ get(id: string): Transform;
305
+ /** Pre-gesture persistence check. Omitted means the adapter's transform
306
+ * capability applies uniformly. A false result must suppress manipulation
307
+ * before the live object is moved, never discover refusal at drag end. */
308
+ editability?(id: string, channel: TransformChannel): TransformEditability;
309
+ /** Pause whatever controller fights the gizmo (physics/script) for editing. */
310
+ beginEdit(id: string): void;
311
+ apply(id: string, t: Transform): void;
312
+ /**
313
+ * Close the gesture — and, for an adapter that persists, ANSWER FOR IT: the
314
+ * returned {@link WriteAck} is this edit's own outcome, produced after the
315
+ * write it triggered has settled. Returning nothing is the honest report of
316
+ * an adapter that performed no write, and the shell then reports the
317
+ * live-only floor rather than borrowing an answer from somewhere else.
318
+ */
319
+ endEdit(id: string): void | WriteAck | Promise<void | WriteAck>;
320
+ /**
321
+ * Optional — DROP one channel's authored transform entirely, rather than
322
+ * writing a value into it.
323
+ *
324
+ * THE SAME DOOR {@link InspectorProvider.remove} IS, for the channel the
325
+ * Transform section owns instead of a property row. `apply`/`endEdit` write a
326
+ * VALUE, so reverting a channel an authoring gesture APPENDED by writing the
327
+ * old numbers back leaves `position={[0, 0, 0]}` standing where the source
328
+ * carried nothing — the file ends one attribute heavier than it started, and
329
+ * no byte-level edit/revert round trip can close. Only removal expresses that
330
+ * absence, which is why a lane without this door grades UNVERIFIABLE rather
331
+ * than passing.
332
+ *
333
+ * SAME ACK CONTRACT AS {@link endEdit} — the pipe's per-edit {@link WriteAck}
334
+ * for THIS removal, awaited. Removal is a write; it moves bytes, and the
335
+ * caller has the same right to be told where they went. Returning nothing
336
+ * means this provider performed no persisted write and the shell reports the
337
+ * live-only floor.
338
+ *
339
+ * What "absent" MEANS is the dialect writer's to decide (deleting the JSX
340
+ * attribute, for the source lanes). Nothing above this seam infers it: a lane
341
+ * whose transform truth IS the running object — there is no document for the
342
+ * channel to be absent from — simply omits the method, and the protocol door
343
+ * refuses by name rather than falling back to a value write.
344
+ */
345
+ remove?(id: string, channel: TransformChannel): void | WriteAck | Promise<void | WriteAck>;
346
+ /** Explicit Play→source attribution. Absent outside an eligible live OID
347
+ * surface; never used implicitly by a transform gesture. */
348
+ readonly sourceCommit?: TransformSourceCommitProvider;
349
+ }
350
+
351
+ export interface InspectorProvider {
352
+ /** Schema-driven — fixed to no document format. */
353
+ properties(id: string): PropertyDescriptor[];
354
+ get(id: string, path: string): unknown;
355
+ /**
356
+ * Write one property — and, for an adapter that persists, ANSWER FOR IT: the
357
+ * returned {@link WriteAck} is this edit's own outcome (same contract as
358
+ * {@link TransformProvider.endEdit}). Returning nothing means no write was
359
+ * performed by this provider, and the shell reports the live-only floor.
360
+ */
361
+ set(id: string, path: string, value: unknown): void | WriteAck | Promise<void | WriteAck>;
362
+ /**
363
+ * Optional — SHOW a value without writing it, for a control the user drags.
364
+ *
365
+ * The gizmo already has this pair ({@link TransformProvider.writeTransform}
366
+ * live, {@link TransformProvider.endEdit} persisting), and a colour picker
367
+ * needs the same shape: it streams values continuously while the pointer
368
+ * moves, and persisting each one is a write burst the absorb cycle cannot
369
+ * follow. So the control previews here on every change and calls
370
+ * {@link set} ONCE when the gesture ends.
371
+ *
372
+ * A preview NEVER persists and is never the truth: whatever the adapter
373
+ * mutates is discarded by the next remount unless a `set` follows. An
374
+ * adapter that cannot show a value without writing it simply omits this,
375
+ * and the control's preview is a no-op — the value still lands on commit.
376
+ */
377
+ preview?(id: string, path: string, value: unknown): void;
378
+ /** Optional per-subject write preflight for shell affordances outside the
379
+ * Inspector field grid (notably hierarchy eye/lock controls). The property
380
+ * descriptor remains the Inspector's source of `readonly`; this gives other
381
+ * generic surfaces the same answer and its adapter-authored reason. */
382
+ editability?(id: string, path: string): { writable: boolean; reason?: string };
383
+ /**
384
+ * Optional — REMOVE a property's authored override entirely (not "set to a
385
+ * value"), letting whatever governs it in its absence take over.
386
+ *
387
+ * THIS IS THE ONLY WAY TO EXPRESS BYTE-ABSENCE. {@link set} writes a VALUE:
388
+ * reverting a prop an authoring gesture ADDED by setting it back to what the
389
+ * default would have been leaves `position={[0, 0, 0]}` in the source where
390
+ * the file previously carried nothing at all. Only removal restores the
391
+ * bytes, which is why a lane with no `remove` cannot close its own
392
+ * edit/revert round trip and grades UNVERIFIABLE rather than passing.
393
+ *
394
+ * SAME ACK CONTRACT AS {@link set} — the pipe's per-edit {@link WriteAck}
395
+ * (`editor/src/authoring/write-pipe.ts`), awaited. Removal is a write; it
396
+ * moves bytes, and the caller has the same right to be told where they went
397
+ * and whether they moved. Returning nothing means this provider performed no
398
+ * persisted write, and the shell reports the live-only floor.
399
+ *
400
+ * What "absent" MEANS is the dialect writer's to decide — deleting a JSX
401
+ * attribute, dropping a CSS longhand so its shorthand cascades. Nothing above
402
+ * this seam infers it. Adapters with no removable-override concept simply
403
+ * omit the method; callers optional-chain, and the protocol door refuses by
404
+ * name rather than falling back to a value write.
405
+ */
406
+ remove?(id: string, path: string): void | WriteAck | Promise<void | WriteAck>;
407
+ }
408
+
409
+ /**
410
+ * An authored node that is itself an asset, even when its bytes are embedded
411
+ * in another source document instead of living at a standalone project path.
412
+ *
413
+ * The first concrete case is inline SVG in a DOM/React root: its `<path>` and
414
+ * `<g>` implementation is no more part of the authoring hierarchy than the
415
+ * meshes inside an imported model. The adapter owns the native-to-asset
416
+ * conversion; the shell only opens the returned image in its ordinary Asset
417
+ * Editor document.
418
+ */
419
+ export interface AuthoringAssetSubject {
420
+ readonly kind: 'image';
421
+ readonly name: string;
422
+ readonly mediaType: 'image/svg+xml';
423
+ readonly text: string;
424
+ /** Project-root-relative source definition, when the adapter can prove it. */
425
+ readonly sourcePath?: string;
426
+ }
427
+
428
+ export interface AssetSubjectProvider {
429
+ get(id: string): AuthoringAssetSubject | null;
430
+ /**
431
+ * Assets known to this authoring document even when they are not in its
432
+ * currently focused hierarchy projection. React story boards use this to
433
+ * expose SVGs mounted in inactive story frames without flattening those
434
+ * frames into the active hierarchy.
435
+ */
436
+ entries?(): ReadonlyArray<{ id: string; subject: AuthoringAssetSubject }>;
437
+ }
438
+
439
+ /** A semantic jump from one authored subject to another document/subject.
440
+ * The adapter owns resolution and opening; the inspector only presents the
441
+ * title and exposes the same verb to human and serialized projections. */
442
+ export interface RelatedSubjectLink {
443
+ readonly id: string;
444
+ readonly title: string;
445
+ open(): void;
446
+ }
447
+
448
+ export interface RelatedSubjectsProvider {
449
+ links(id: string): readonly RelatedSubjectLink[];
450
+ }
451
+
452
+ /** One source-owned prop that differs from its component's declared default. */
453
+ export interface ComponentInstanceOverride {
454
+ /** The owning Inspector field path; stable and agent-addressable. */
455
+ readonly path: string;
456
+ readonly label: string;
457
+ readonly value: unknown;
458
+ /** Source text of the component default, when the declaration exposes one. */
459
+ readonly defaultText?: string;
460
+ /** Applying requires a literal callsite value and a literal declared default. */
461
+ readonly canApplyToComponent: boolean;
462
+ readonly applyUnavailableReason?: string;
463
+ /** Placed instances that will inherit this default after the apply. */
464
+ readonly affectedInstanceCount: number;
465
+ }
466
+
467
+ /** Derived prefab-instance facts. Nothing in this shape is persisted separately. */
468
+ export interface ComponentInstanceDescription {
469
+ readonly componentName: string;
470
+ readonly sourcePath?: string;
471
+ readonly overrides: readonly ComponentInstanceOverride[];
472
+ }
473
+
474
+ export interface ComponentInstanceApplyResult {
475
+ readonly changed: boolean;
476
+ readonly message: string;
477
+ /** This operation's own writer acknowledgement. Absent when the request was
478
+ * refused before any write; live-only is an explicit WriteAck. */
479
+ readonly write?: WriteAck;
480
+ }
481
+
482
+ /**
483
+ * Source-derived component-instance operations. A provider exists only when an
484
+ * adapter can prove component identity and write the component's native source.
485
+ * No override store is implied: describe is a diff, revert removes callsite
486
+ * props, and apply edits the component declaration itself.
487
+ */
488
+ export interface ComponentInstancesProvider {
489
+ describe(id: string): ComponentInstanceDescription | null;
490
+ /** Open the native component/prefab board for this instance's surface. */
491
+ openComponent?(id: string): void;
492
+ revert(id: string, paths: readonly string[]): Promise<void | WriteAck>;
493
+ applyToComponent(id: string, path: string): Promise<ComponentInstanceApplyResult>;
494
+ }
495
+
496
+ /**
497
+ * WHAT A STRUCTURAL VERB ANSWERS WITH — the same widening
498
+ * {@link TransformProvider.endEdit} and {@link InspectorProvider.set} carry,
499
+ * for the same reason: the ack is produced by the component that performed
500
+ * THIS edit, and there is nowhere else for a caller to get one.
501
+ *
502
+ * `void` is the honest answer for a lane whose verb performs no write of its
503
+ * own (an in-memory adapter mutating a graph it already owns); a lane that
504
+ * writes returns the persistence pipe's own {@link WriteAck}
505
+ * (`editor/src/authoring/write-pipe.ts`), awaited, so a caller that awaits the
506
+ * verb has awaited the byte.
507
+ *
508
+ * A structural verb resolves on ITS OWN verb: `remove` asks whether the struct
509
+ * writer is bound, never whether the PROP writer is — resolving on a
510
+ * value-write door is how a lane comes to ack `source-prop` for a door it does
511
+ * not have.
512
+ */
513
+ export type StructuralWriteOutcome = void | WriteAck | Promise<void | WriteAck>;
514
+
515
+ /**
516
+ * WHAT AN ID-RETURNING STRUCTURAL VERB ANSWERS WITH — the id and the ack, as
517
+ * ONE return.
518
+ *
519
+ * The writing verbs ({@link StructureProvider.remove} and friends) answer with
520
+ * a bare {@link StructuralWriteOutcome}, because the write IS their whole
521
+ * answer. `create`/`duplicate`/`group`/`ungroup` owe their caller a second
522
+ * thing — the id(s) the op produced — and those two halves settle at DIFFERENT
523
+ * TIMES:
524
+ *
525
+ * - the id is SYNCHRONOUS where a lane can mint one. The live canvas surface
526
+ * adds a real display object to the running tree and re-indexes before it
527
+ * returns, so `create` hands back an id its caller can select in the same
528
+ * turn ("create then immediately select it" is the ordinary shell
529
+ * sequence). Making the verb `async` to carry the ack would take that
530
+ * away from every caller to serve a value that is already known.
531
+ * - the ack is ASYNCHRONOUS wherever a byte is involved, and it is the only
532
+ * thing that says whether the edit reached the file it claims.
533
+ *
534
+ * So the return carries both. A source lane whose new id only exists after the
535
+ * remount stamps it answers `id: ''` (there is no honest synchronous id) with a
536
+ * real `ack`; a router that could not route answers with an unchanged id and
537
+ * `ack: undefined` (no write was attempted, which is exactly what `void` means
538
+ * in {@link StructuralWriteOutcome}).
539
+ *
540
+ * Firing the write and dropping the promise on the floor — `void structOp(…)`
541
+ * behind a `return ''` — is the shape this type exists to make unrepresentable:
542
+ * it left the caller unable to await the byte, which is how N same-file
543
+ * duplicates went out together, each reading the file before any wrote back.
544
+ */
545
+ export interface StructuralIdWrite<Id extends string | null = string> {
546
+ /** The id the op produced — `''` when the lane cannot know it until its own
547
+ * source write completes and the world remounts. */
548
+ readonly id: Id;
549
+ readonly ack: StructuralWriteOutcome;
550
+ }
551
+
552
+ /** The plural half of {@link StructuralIdWrite}, for a verb that promotes a set
553
+ * of ids rather than minting one. */
554
+ export interface StructuralIdsWrite {
555
+ readonly ids: readonly string[];
556
+ readonly ack: StructuralWriteOutcome;
557
+ }
558
+
559
+ /** Clipboard-backed structural writes retain the existing `false` refusal
560
+ * channel while answering a successful cut/paste with the write's real
561
+ * destination. A bare `true` cannot prove a byte landed anywhere. */
562
+ export type StructuralClipboardOutcome = false | WriteAck | Promise<false | WriteAck>;
563
+
564
+ /** One kind `create` accepts: its label, and optionally what it is (a create dialog's Description)
565
+ * and the kind it extends (the dialog's class tree, as Godot's Create New Node shows it). */
566
+ export interface CreatableKind {
567
+ kind: string;
568
+ label: string;
569
+ description?: string;
570
+ extends?: string;
571
+ }
572
+
573
+ export interface StructureProvider {
574
+ /** Create a `kind` under `parentId`, answering with the new id AND this
575
+ * creation's own write ack (see {@link StructuralIdWrite}). `at` places it: a
576
+ * point in the frame this adapter's `rects` answer in (a 2D editor's "add node
577
+ * where I clicked"); absent, the kind's own default place. */
578
+ create(kind: string, parentId?: string, at?: { readonly x: number; readonly y: number }): StructuralIdWrite;
579
+ /**
580
+ * Remove `id`. May optionally return an awaitable when the
581
+ * underlying write is asynchronous (e.g. a react-world source-file edit) —
582
+ * `deleteSelection` (`editor-hotkeys.ts`) awaits it per id so a same-file
583
+ * multi-delete's writes land strictly one at a time (see that function's own
584
+ * doc comment for why serialization + deletion order together are what make
585
+ * a per-id loop sound for an adapter whose OID index isn't reindexed between
586
+ * writes). An adapter with a synchronous/in-memory remove (e.g.
587
+ * `UIAuthoringAdapter`) returns `void` — `await`ing it is a harmless no-op.
588
+ *
589
+ * A source-writing lane returns THIS removal's own {@link WriteAck} (see
590
+ * {@link StructuralWriteOutcome}).
591
+ */
592
+ remove(id: string): StructuralWriteOutcome;
593
+ /** Copy `id`, answering with the copy's id AND this duplication's own write
594
+ * ack (see {@link StructuralIdWrite}). A source lane whose copy has no id
595
+ * until the remount stamps it answers with the SOURCE id it was given —
596
+ * `duplicate` has no refusal channel, so an empty string there would be
597
+ * indistinguishable from "refused". */
598
+ duplicate(id: string): StructuralIdWrite;
599
+ /** Duplicate a selection as one native operation and select all copies.
600
+ * Preserves relationships within the selection and creates one undo step.
601
+ * Absent means the caller falls back to individual duplicate operations. */
602
+ duplicateMany?(ids: readonly string[]): StructuralWriteOutcome;
603
+ reparent(id: string, newParentId: string | null): StructuralWriteOutcome;
604
+ /** Reorder `id` to sit immediately before `beforeSiblingId` among its siblings
605
+ * (`null` = move to the end). Absent ⇒ the shell has no sibling-reorder UI
606
+ * for this adapter. */
607
+ reorder?(id: string, beforeSiblingId: string | null): StructuralWriteOutcome;
608
+ /**
609
+ * The kinds `create` accepts for a given parent (`null` parentId = a new
610
+ * root), each with a display label — drives the shell's creation palette.
611
+ * Absent ⇒ the palette shows nothing for this adapter, even though `create`
612
+ * itself may still work when called directly (e.g. programmatically, or by
613
+ * an adapter-specific affordance outside the generic palette).
614
+ */
615
+ creatableKinds?(parentId: string | null): CreatableKind[];
616
+ /** D3 (spec 27 §6) — wrap `id` in a new container element (default tag
617
+ * adapter-chosen, e.g. a `div`), re-parenting `id` as that container's sole
618
+ * child. Absent ⇒ the shell's context menu shows no Wrap item for this
619
+ * adapter. */
620
+ wrap?(id: string, wrapperTag?: string): StructuralWriteOutcome;
621
+ /** D3 (spec 27 §6) — replace `id` with its own children (the inverse of
622
+ * `wrap`). Absent ⇒ the shell's context menu shows no Unwrap item for this
623
+ * adapter. */
624
+ unwrap?(id: string): StructuralWriteOutcome;
625
+ /** Group sibling authoring objects beneath one new spatial parent. The
626
+ * adapter owns transform preservation, persistence, and undo semantics.
627
+ * Answers with the new group id — `null` when the selection cannot be
628
+ * grouped at all — AND this grouping's own write ack (see
629
+ * {@link StructuralIdWrite}). */
630
+ group?(ids: readonly string[]): StructuralIdWrite<string | null>;
631
+ /** Dissolve a plain spatial group, answering with the ids of its promoted
632
+ * children AND this dissolution's own write ack (see
633
+ * {@link StructuralIdsWrite}). Absent means this adapter has no lossless
634
+ * ungroup operation. */
635
+ ungroup?(id: string): StructuralIdsWrite;
636
+ /** Preflight for an Ungroup affordance. Omit when ungroup is never offered. */
637
+ canUngroup?(id: string): boolean;
638
+ /**
639
+ * D4.R2 (spec27 §6 D4 reopen) — remove every id in `ids` as ONE undoable
640
+ * op (a single Ctrl+Z restores the whole batch), instead of the caller
641
+ * looping `remove(id)` per id (which — one `remove` call = one undo push
642
+ * per adapter, by design — produces N separate undo entries: Ctrl+Z then
643
+ * restores them one at a time, asymmetric with the first-party adapter's
644
+ * own single batched multi-delete undo). Absent ⇒ the shell falls back to
645
+ * the per-id `remove` loop (today's N-entry behavior, unchanged) — an
646
+ * honest degrade, not a silent behavior change, for any adapter that
647
+ * hasn't implemented batching. May optionally return an awaitable, same
648
+ * widening `remove` documents above ({@link StructuralWriteOutcome}) and for
649
+ * the same reason (delete-order-residual fix): `deleteSelection`
650
+ * (`editor-hotkeys.ts`) awaits it so the caller can rely on the whole
651
+ * batch's write having landed before it returns. A synchronous/in-memory
652
+ * implementation (e.g. `UIAuthoringAdapter`'s) returns `void` — awaiting it
653
+ * is a harmless no-op.
654
+ */
655
+ removeMany?(ids: readonly string[]): StructuralWriteOutcome;
656
+ /** Copy authored entities through the host clipboard. The adapter owns the
657
+ * native payload (JSX for a source-backed Three/Canvas world, native records
658
+ * for a data-backed adapter); the shell never serializes an engine-private
659
+ * shape. */
660
+ copy?(ids: readonly string[]): boolean | Promise<boolean>;
661
+ /** Honest affordance preflight. Omit when copy is never offered. */
662
+ canCopy?(ids: readonly string[]): boolean;
663
+ /** Copy, then remove the same authored entities only after the clipboard
664
+ * write succeeds. One adapter operation owns that ordering. */
665
+ cut?(ids: readonly string[]): StructuralClipboardOutcome;
666
+ /** Paste as children of `parentId`; `null` means the adapter document root.
667
+ * The shell derives the current selection's parent so ordinary Paste creates
668
+ * siblings, matching scene-hierarchy editors. */
669
+ paste?(parentId: string | null): StructuralClipboardOutcome;
670
+ /** Honest affordance preflight. It need only cover adapter/session state;
671
+ * the actual paste still validates the live system clipboard. */
672
+ canPaste?(parentId: string | null): boolean;
673
+ }
674
+
675
+ /**
676
+ * PersistenceProvider — whether and where an adapter's edits persist (design).
677
+ * The ACTIVE adapter's provider is the ONE place this is decided — the host
678
+ * (editor) never hard-codes a destination or guards on session flags; it asks the
679
+ * provider.
680
+ */
681
+ export interface PersistenceProvider {
682
+ isDirty(): boolean;
683
+ /** The last persistence failure that still requires attention. Successful
684
+ * subsequent persistence clears it. Auto-saving source adapters use this to
685
+ * report a rolled-back source write without inventing a dirty document. */
686
+ lastError?(): string | null;
687
+ /** Save to the adapter's OWN source of truth (its source file, its own
688
+ * data, …) — whatever that adapter defines it to be. */
689
+ save(): Promise<void>;
690
+ /**
691
+ * Human/agent-readable destination this provider persists to — e.g.
692
+ * `"src/world.tsx"`, `"live-only (not saved)"`, or `"ephemeral
693
+ * (discarded on stop)"`. Drives the save-status UI and makes routing
694
+ * inspectable (design §2). Implementers whose destination can change during
695
+ * the session (a provider that tracks the currently focused document, say)
696
+ * should expose this as a live getter rather than a value captured once.
697
+ */
698
+ readonly destination: string;
699
+ /**
700
+ * The reload contract (design §5): apply an external change to this
701
+ * provider's persisted artifact into the RUNNING session (e.g. a file-watcher
702
+ * update to the artifact). Absent ⇒ the host must remount to
703
+ * pick up external changes — the honest floor, and correctly what the
704
+ * ephemeral provider reports.
705
+ *
706
+ * `rawContent`, when the host has it (the file-watcher SSE payload carries the
707
+ * artifact's exact bytes), is an OPTIONAL second parameter enabling own-echo
708
+ * detection — an event that is exactly what this provider itself last wrote is
709
+ * its own save reflected back, and should be skipped even outside the normal
710
+ * debounce window (see `EditorStore.applyExternalUpdate`'s doc comment). This
711
+ * stays optional/provider-specific — only the first-party provider currently
712
+ * uses it — so other implementers can ignore the parameter entirely.
713
+ */
714
+ applyExternal?(content: unknown, rawContent?: string): void;
715
+ }
716
+
717
+ /** D12 — per-layer viewport picking. Coordinates are client (browser) px. */
718
+ export interface PickProvider {
719
+ pick(clientX: number, clientY: number): string | null;
720
+ /**
721
+ * Every authorable subject below the point, frontmost first. The shell uses
722
+ * this for the ordinary scene-editor "pick from overlap" menu; adapters
723
+ * that cannot enumerate an overlap may omit it and still provide the
724
+ * single-hit floor through {@link pick}. `includeLocked` lists nodes the editor's lock keeps
725
+ * clicks away from too (Godot's Alt+RMB: "all nodes … including locked").
726
+ */
727
+ candidates?(clientX: number, clientY: number, options?: { includeLocked?: boolean }): readonly string[];
728
+ }
729
+
730
+ /** D4 — storybook stories. */
731
+ export interface StoryRef {
732
+ id: string;
733
+ label: string;
734
+ }
735
+ export interface StoriesProvider {
736
+ storiesFor(nodeId: string): StoryRef[]; // [] = none
737
+ active(nodeId: string): string | null;
738
+ apply(nodeId: string, storyId: string | null): void; // null clears
739
+ /** Render ONLY this node against the story (storybook canvas); null exits. */
740
+ isolate?(nodeId: string | null, storyId?: string): void;
741
+ /**
742
+ * Display vocabulary for this provider's section — e.g. an ingested game's
743
+ * contract scenes say "Scenes"; absent means the default "Stories". The
744
+ * states are the same seam either way; only the word the human reads
745
+ * changes, so a game's own screens are never labelled with a design-time
746
+ * term the game never uses.
747
+ */
748
+ readonly title?: string;
749
+ /**
750
+ * Why this provider could not enumerate, or `null` when it did. An empty
751
+ * `storiesFor` and an UNREAD one look identical from the section that draws
752
+ * them, and only the provider knows which it is — so the PROVIDER answers,
753
+ * and the shell never reaches past it into one particular provider's own
754
+ * discovery to find out (a CSF registry, a game contract's screen list).
755
+ * Absent means the provider always ran.
756
+ */
757
+ unavailable?(): string | null;
758
+ }
759
+
760
+ /**
761
+ * Where a drop landed, when the shell knows. The VIEWPORT knows a world point
762
+ * (its ground-plane raycast under the cursor); the hierarchy panel does not,
763
+ * and passes nothing. Optional both ways: an adapter with no placement concept
764
+ * ignores it, and a caller with no point omits it — so neither side has to
765
+ * pretend. Coordinates are world-space, in the adapter's own units.
766
+ */
767
+ export interface AssetDropContext {
768
+ readonly position?: readonly [number, number, number];
769
+ /** Transient drag metadata. The project's native source remains truth; this
770
+ * only tells an adapter whether the path names a file or a source component. */
771
+ readonly item?:
772
+ | { readonly kind: 'file'; readonly name: string }
773
+ | {
774
+ readonly kind: 'component';
775
+ readonly name: string;
776
+ readonly sourcePath: string;
777
+ readonly exportKind: 'default' | 'named';
778
+ readonly surface: 'three' | 'canvas' | 'dom' | 'unknown';
779
+ };
780
+ }
781
+
782
+ /** Asset drop (hierarchy + viewport). */
783
+ export interface AssetDropProvider {
784
+ accepts(nodeId: string, assetPath: string, context?: AssetDropContext): boolean;
785
+ /** A drop that lands as an ELEMENT in the game's own source is a structural
786
+ * write, so it answers with the same {@link StructuralWriteOutcome} the rest
787
+ * of {@link StructureProvider}'s writing verbs do — a lane whose drop only
788
+ * mutates the running graph returns `void`. */
789
+ drop(nodeId: string, assetPath: string, context?: AssetDropContext): StructuralWriteOutcome;
790
+ }
791
+
792
+ /** Plain-object rect shape shared by {@link RectProvider} — the same fields a real
793
+ * `DOMRect` carries (a subset, so a real `DOMRect` satisfies this structurally too).
794
+ * See {@link RectProvider} for the coordinate space (host-relative, not viewport). */
795
+ export interface DOMRectLike {
796
+ x: number;
797
+ y: number;
798
+ width: number;
799
+ height: number;
800
+ }
801
+
802
+ /** Per-node screen geometry — the KEYSTONE omission today (nothing produces rects the
803
+ * ported inspect.ts math consumes). Coordinates are **host-relative** px: relative to
804
+ * the adapter's own mounted world surface (its `position:absolute; inset:0` layer), the
805
+ * same surface the A3 selection overlay is hosted over, so the overlay can draw against
806
+ * these rects directly. (NOT viewport-client — the JSDoc formerly said so in error.) */
807
+ export interface RectProvider {
808
+ /** selectable node id → its current bounding rect, or null if unmounted/offscreen */
809
+ rect(id: string): { x: number; y: number; width: number; height: number } | null;
810
+ /** rects of layout-relevant neighbours, for snap/measure/box-model overlays */
811
+ contextRects?(id: string): {
812
+ parent?: DOMRectLike;
813
+ siblings?: DOMRectLike[];
814
+ paddingBox?: DOMRectLike;
815
+ };
816
+ /**
817
+ * D3 (spec 27 §6) — every currently-empty container this adapter's tree
818
+ * contains right now: no visible children, no text, a collapsed dimension
819
+ * (the `ui-source/inspect.ts:findEmptyContainers` math) — each with a
820
+ * grown-to-tappable placeholder rect and a display name, driving the
821
+ * overlay's dashed empty-container hint. Host-relative, same coordinate
822
+ * space as {@link rect}. Absent ⇒ this adapter has no "empty container"
823
+ * concept (e.g. an adapter with no structural containers at all) — no
824
+ * hints are drawn for it.
825
+ */
826
+ emptyContainers?(): { id: string; rect: DOMRectLike; displayName: string }[];
827
+ /**
828
+ * The node's own box as its transform carries it — the four corners of its local bounds, in the
829
+ * frame {@link rect} answers in. A turned or skewed node's corners are not an axis-aligned
830
+ * rectangle, and the overlay draws its selection frame on them, as Godot's 2D editor does.
831
+ * Absent ⇒ the overlay frames {@link rect}.
832
+ */
833
+ frame?(id: string): FrameCorners | null;
834
+ }
835
+
836
+ /** A node box's corners: top-left, top-right, bottom-right and bottom-left of its local bounds. */
837
+ export interface FrameCorners {
838
+ readonly tl: { readonly x: number; readonly y: number };
839
+ readonly tr: { readonly x: number; readonly y: number };
840
+ readonly br: { readonly x: number; readonly y: number };
841
+ readonly bl: { readonly x: number; readonly y: number };
842
+ }
843
+
844
+ /** One adapter-owned reference point rendered by the shared world overlay.
845
+ * Coordinates are host-relative px, the same frame as {@link RectProvider}.
846
+ * A provider deliberately exposes at most one point for a node so coincident
847
+ * native concepts (for example an anchor and pivot) never become ambiguous. */
848
+ export interface BoxEditReferencePoint {
849
+ /** True when the native source constrains this point (for example a
850
+ * normalized 0..1 Sprite anchor). The overlay then uses only exact native
851
+ * snap points, never an unreachable axis-aligned bounding-box target. */
852
+ bounded?: boolean;
853
+ id: string;
854
+ kind: 'anchor' | 'pivot';
855
+ label: string;
856
+ /** Exact host-space points the native reference can reach and should snap
857
+ * to. Useful for transformed corner/edge/center grids on rotated content. */
858
+ snapPoints?: ReadonlyArray<{ x: number; y: number }>;
859
+ x: number;
860
+ y: number;
861
+ }
862
+
863
+ /** Spatial gesture → source/data write for native 2D boxes (DOM or canvas).
864
+ * Distinct from the 3D TransformProvider. begin/apply/end bracket a single
865
+ * undo step; apply is live-preview. */
866
+ export interface BoxEditProvider {
867
+ begin(id: string): void;
868
+ /** patch: any of x,y,width,height,marginTop… paddingLeft… — px deltas or absolutes.
869
+ * `referenceX`/`referenceY` are the absolute host-relative coordinates of
870
+ * the active {@link referencePoint}; only providers exposing that optional
871
+ * capability receive those keys. The provider owns conversion to native
872
+ * source units and any placement-preserving compensation. */
873
+ apply(id: string, patch: Record<string, number>): void;
874
+ end(id: string): void;
875
+ /** Native transform origin for the viewport gizmo, in the same host-space
876
+ * frame as {@link RectProvider}. A 2D scene uses this instead of guessing
877
+ * from the visual bounds: sprites can have anchors, containers can have
878
+ * pivots, and neither necessarily sits at the box center. */
879
+ gizmoOrigin?(id: string): { x: number; y: number } | null;
880
+ /** Optional single native anchor/pivot affordance for this node. */
881
+ referencePoint?(id: string): BoxEditReferencePoint | null;
882
+ }
883
+
884
+ /** Text-content editing, ON the contract: the react-world adapter implements this
885
+ * provider (`react-world-authoring-adapter.ts`'s `readonly text: TextProvider`),
886
+ * bringing its own `editText(id)` write path onto the seam, so the overlay's
887
+ * double-click-to-edit-text (D3) is contract-driven rather than reaching for a
888
+ * concrete adapter's method. */
889
+ export interface TextProvider {
890
+ /** the element's editable pure-text, or null if the body is dynamic/has child elements */
891
+ get(id: string): string | null;
892
+ set(id: string, text: string): void; // dynamic-guarded by the writer
893
+ }
894
+
895
+ /**
896
+ * D3 (spec 27 §6) — background-color sampling at a viewport point, for the
897
+ * eyedropper's FALLBACK path (browsers with no native `EyeDropper` API). The
898
+ * native API, where available, samples real rendered pixels itself and needs
899
+ * none of this. Distinct from {@link PickProvider} (which returns a node id,
900
+ * not a color) and from `InspectorProvider.get('style.backgroundColor')`
901
+ * (which NORMALIZES to `#rrggbb` and so loses the "transparent" signal a
902
+ * color CHAIN walk needs — see `ui-source/inspect.ts:effectiveColorFromChain`'s
903
+ * doc comment: it walks raw, un-normalized CSS values looking for the first
904
+ * non-transparent one).
905
+ */
906
+ export interface ColorSampleProvider {
907
+ /** Raw (un-normalized) CSS `background-color` values, hit-element FIRST,
908
+ * walking up its ancestor chain — the exact shape
909
+ * `effectiveColorFromChain` consumes. `null` when nothing is hit at the
910
+ * point. Client (viewport) px, matching {@link PickProvider}. */
911
+ backgroundChainAt(clientX: number, clientY: number): string[] | null;
912
+ }
913
+
914
+ /**
915
+ * Where a node's live object CAME FROM in the game's own truth.
916
+ *
917
+ * `display` is the string the editor shows verbatim. The unanchored shape
918
+ * carries a `reason` rather than a blank, because "we don't know" and "a
919
+ * library built it" are different facts and neither may present as silence
920
+ * (the honest-floor rule).
921
+ *
922
+ * TWO ANCHORED KINDS, because a game has two kinds of truth and only one of
923
+ * them is source.
924
+ *
925
+ * - `source` is the original: a `new` expression at a `file:line:col` in the
926
+ * game's own code, which is where an edit to a constructor literal has to be
927
+ * written.
928
+ * - `data` is the sibling a level-based game needs. Its placed cargo — the
929
+ * enemies, the pickups, the objectives — reaches no source literal at all;
930
+ * it is binary records inside a level file that the game parses at load. An
931
+ * authored move of one is therefore an edit to THE GAME'S OWN DATA, and
932
+ * the honest anchor is the data file plus the index of the record that
933
+ * addresses the object inside it.
934
+ *
935
+ * `kind` is an explicit discriminant rather than an inferred one, so a consumer
936
+ * that only understands source anchors fails to compile against a data anchor
937
+ * instead of silently reading `line` off something that has none.
938
+ *
939
+ * A data record is EXACTLY ONE object by construction — an index addresses one
940
+ * record — so the "how many objects did this site construct?" question that
941
+ * governs a source write has no data-side analogue and must not be invented
942
+ * for one.
943
+ */
944
+ export type NodeCreationSite =
945
+ | { anchored: true; kind: 'source'; file: string; line: number; col: number; display: string }
946
+ | {
947
+ anchored: true;
948
+ kind: 'data';
949
+ /** Project-relative path of the game's own data file, POSIX separators. */
950
+ file: string;
951
+ /** Index of the record inside that file which addresses this object. */
952
+ record: number;
953
+ display: string;
954
+ }
955
+ | { anchored: false; reason: string };
956
+
957
+ /**
958
+ * WHERE AN AUTHORED WRITE LANDS — the vocabulary of anchor KINDS, and the
959
+ * closed set of them.
960
+ *
961
+ * {@link NodeCreationSite} answers "where did this object come from"; a kind
962
+ * answers the write-side question beside it: WHICH LANE would carry an edit to
963
+ * this node, and therefore WHAT has to be true for that edit to be correct. The
964
+ * two are not the same fact — two nodes can both be `{ kind: 'source' }` at a
965
+ * `file:line` and still be written by different lanes with different gates.
966
+ *
967
+ * Each kind's CORRECTNESS CONTRACT, and the named test that proves it:
968
+ *
969
+ * - `source-prop` — the value is a literal PROP on an element of the game's own
970
+ * source (a JSX attribute the serve-time authoring stamp addresses). Lands at
971
+ * the anchor; survives a cold remount, because the file is truth and the next
972
+ * mount re-derives from it.
973
+ * Proof: `packages/editor/test/oid-source-persistence.test.ts`.
974
+ * - `source-structure` — the edit is not a VALUE at all: it adds, removes,
975
+ * moves or re-nests an ELEMENT of the game's own source (`delete`,
976
+ * `duplicate`, `wrap`/`unwrap`, `create`, `reorder`, `reparent`, `group`).
977
+ * It is a kind rather than a flavour of `source-prop` because it resolves on
978
+ * a DIFFERENT DOOR — the dialect's structural writer, not its attribute
979
+ * writer — and a lane can have one without the other. Claiming `source-prop`
980
+ * for a structural edit is exactly the classifier/writer split the
981
+ * persistence pipe exists to make impossible: it names a value lane that
982
+ * never carried the bytes. Its landing contract adds one gate the prop lane
983
+ * does not have: the write is WHOLE-FILE and checksum-guarded, so an op
984
+ * resolved against a stale element index must fail rather than land at the
985
+ * wrong offset.
986
+ * Proof: `packages/editor/test/structural-write-pipe.test.ts`.
987
+ * - `construction-literal` — the value is a literal in the CONSTRUCTION
988
+ * EXPRESSION that built the object (`new THREE.Mesh(…)` in the game's own
989
+ * served module). Same landing/remount contract, plus one gate the prop lane
990
+ * does not have: a site that constructed more than one object is refused,
991
+ * because rewriting its literal would move all of them.
992
+ * Proof: `packages/editor/test/ingest-source-persistence.test.ts`.
993
+ * - `physics-binding` — the node's own transform props are DEAD: a simulated
994
+ * body writes its matrix every frame from a spawn it reads elsewhere, so the
995
+ * write lands at the literal that binding reads (the component callsite whose
996
+ * props the `useBox(…)`/`<RigidBody>` hook takes). It carries the two
997
+ * contracts above AND ONE MORE: the authored value must SURVIVE RE-SETTLE —
998
+ * after the world re-derives from source and the design-time settle runs, the
999
+ * body must have re-spawned at the AUTHORED pose, not at the old one. Being
1000
+ * silently re-posed by the owner is this kind's whole failure mode, and the
1001
+ * reason it is a kind rather than a flavour of `source-prop`.
1002
+ * Proof: `packages/editor/test/r3f-physics-binding.test.ts`.
1003
+ * - `data-record` — the value is a field of a RECORD in the game's own data
1004
+ * file, addressed by index; no source literal mentions it. Lands in that
1005
+ * file through the game's own declared writer; survives a cold remount for
1006
+ * the same reason source does.
1007
+ * Proof: `packages/editor/test/ingest-source-persistence.test.ts`
1008
+ * ("a level-data record is an anchor too").
1009
+ * - `live-only` — there is no anchor. The edit lands on the running object for
1010
+ * the session and NOTHING else, and the contract is that the surface SAYS SO:
1011
+ * a write ack that claims a destination it did not reach is the lie this kind
1012
+ * exists to keep nameable.
1013
+ * Proof: `packages/editor/test/oid-source-persistence.test.ts`
1014
+ * ("an object with no source stamp is refused by name")
1015
+ * + `packages/editor/test/inspection-write-destination.test.ts`.
1016
+ */
1017
+ export type WriteAnchorKind =
1018
+ | 'source-prop'
1019
+ | 'source-structure'
1020
+ | 'construction-literal'
1021
+ | 'physics-binding'
1022
+ | 'data-record'
1023
+ | 'live-only';
1024
+
1025
+ /**
1026
+ * WHAT ONE EDIT DID — the ack the persistence pipe returns for THAT edit, from
1027
+ * the component that actually performed the write.
1028
+ *
1029
+ * It is deliberately NOT a property of an adapter, a session or a surface. The
1030
+ * fact it carries is per-edit, because that is the only granularity at which it
1031
+ * can be true: one composite may hold a source-backed three root beside a
1032
+ * live-only DOM root, and one three root holds body-placed nodes beside
1033
+ * ordinary JSX props. An answer computed from the SURFACE rather than from the
1034
+ * edit reports whichever child happened to be persist-capable — measured on the
1035
+ * vendored racing game, where a three-root edit acked the other root's
1036
+ * destination with `persisted: true`.
1037
+ *
1038
+ * `persisted: false` is an ANSWER, never a failure: `live-only` is a real
1039
+ * anchor kind and the contract of that kind is that the surface SAYS SO. What
1040
+ * is forbidden is a `destination` no byte reached being reported with
1041
+ * `persisted: true`.
1042
+ *
1043
+ * A provider that performs a write returns this; one that has no write to
1044
+ * perform returns nothing and the shell reports the live-only floor. There is
1045
+ * no third answer, and in particular there is no adapter-wide blanket
1046
+ * destination standing in for one — {@link PersistenceProvider.destination}
1047
+ * answers the different question "where do this surface's SAVES go", for the
1048
+ * save-status chrome.
1049
+ */
1050
+ export interface WriteAck {
1051
+ /** Where THIS edit's bytes landed, in the writer's own words — or the
1052
+ * live-only/ephemeral floor when none did. */
1053
+ readonly destination: string;
1054
+ /** Whether a byte actually moved for THIS edit. */
1055
+ readonly persisted: boolean;
1056
+ }
1057
+
1058
+ /**
1059
+ * The exhaustiveness pin, the same shape and for the same reason as
1060
+ * {@link AUTHORING_PROVIDER_PRESENCE}: a `Record` over the union, so a lane that
1061
+ * starts producing a new kind of anchor cannot ship without joining the
1062
+ * vocabulary (the `Record` would be missing a key) and cannot invent a key the
1063
+ * union does not have (excess-property error). The values are `true` and carry
1064
+ * no meaning — the KEYS are the payload.
1065
+ *
1066
+ * What this buys downstream: the per-kind write-reach counts and a probe's
1067
+ * edit-write walk both enumerate {@link WRITE_ANCHOR_KINDS} rather than a
1068
+ * hand-written list, so a new kind arrives already measured and already walked
1069
+ * instead of silently untested.
1070
+ */
1071
+ const WRITE_ANCHOR_KIND_PRESENCE: Readonly<Record<WriteAnchorKind, true>> = {
1072
+ 'source-prop': true,
1073
+ 'source-structure': true,
1074
+ 'construction-literal': true,
1075
+ 'physics-binding': true,
1076
+ 'data-record': true,
1077
+ 'live-only': true,
1078
+ };
1079
+
1080
+ /**
1081
+ * The vocabulary as an ordered list — most-addressable first, `live-only` last,
1082
+ * so two reports of the same world are diffable line for line.
1083
+ *
1084
+ * Derived from {@link WRITE_ANCHOR_KIND_PRESENCE} rather than spelled a second
1085
+ * time: one list the compiler pins to the union, and nothing downstream can
1086
+ * enumerate a different set.
1087
+ */
1088
+ export const WRITE_ANCHOR_KINDS: readonly WriteAnchorKind[] = Object.keys(
1089
+ WRITE_ANCHOR_KIND_PRESENCE,
1090
+ ) as WriteAnchorKind[];
1091
+
1092
+ /** Zero counts for every kind — the starting point of any per-kind tally, so a
1093
+ * tally can never omit a kind by forgetting to initialize it. */
1094
+ export function emptyWriteAnchorKindCounts(): Record<WriteAnchorKind, number> {
1095
+ const counts = {} as Record<WriteAnchorKind, number>;
1096
+ for (const kind of WRITE_ANCHOR_KINDS) counts[kind] = 0;
1097
+ return counts;
1098
+ }
1099
+
1100
+ /**
1101
+ * Absent ⇒ this adapter's substrate has no creation-site index at all (a
1102
+ * document-backed world's truth is the document, not a construction site).
1103
+ * Present ⇒ every id gets an answer, anchored or reasoned.
1104
+ */
1105
+ export interface AuthoringTruth {
1106
+ /** Where the runtime object came from in the game's own source or data. */
1107
+ readonly site: NodeCreationSite;
1108
+ /** The lane THIS property edit would actually take. `undefined` means the
1109
+ * subject is synthetic or otherwise has no write plan; `live-only` is a real
1110
+ * plan whose destination is the running object. */
1111
+ readonly writeAnchorKind: WriteAnchorKind | undefined;
1112
+ }
1113
+
1114
+ /**
1115
+ * The TRUTH-family binding. One resolution answers both where a subject came
1116
+ * from and where an edit to one of its properties lands. A caller may not read
1117
+ * those facts through separate providers: that was the classifier/writer split
1118
+ * the persistence pipe exists to remove.
1119
+ */
1120
+ export interface TruthProvider {
1121
+ resolve(id: string, property: string): AuthoringTruth;
1122
+ }
1123
+
1124
+ /** A plain world-space point. Component handles cross the authoring seam as
1125
+ * data; a substrate object or renderer handle never does. */
1126
+ export type SpatialPoint3 = readonly [x: number, y: number, z: number];
1127
+
1128
+ /** Non-interactive geometry an adapter asks the viewport to draw for one
1129
+ * spatial component. The vocabulary is intentionally geometric rather than
1130
+ * domain-shaped: audio attenuation, light range and a collider radius can all
1131
+ * use the same sphere without teaching the shell any of those concepts. */
1132
+ export type SpatialHandleGuide =
1133
+ | {
1134
+ readonly kind: 'line';
1135
+ readonly points: readonly SpatialPoint3[];
1136
+ readonly color: string;
1137
+ readonly opacity?: number;
1138
+ readonly dashed?: boolean;
1139
+ }
1140
+ | {
1141
+ readonly kind: 'sphere';
1142
+ readonly center: SpatialPoint3;
1143
+ readonly radius: number;
1144
+ readonly color: string;
1145
+ readonly opacity?: number;
1146
+ readonly dashed?: boolean;
1147
+ }
1148
+ | {
1149
+ readonly kind: 'cone';
1150
+ readonly origin: SpatialPoint3;
1151
+ readonly direction: SpatialPoint3;
1152
+ readonly length: number;
1153
+ /** Full opening angle in degrees, matching Web Audio and authoring UIs. */
1154
+ readonly angle: number;
1155
+ readonly color: string;
1156
+ readonly opacity?: number;
1157
+ readonly dashed?: boolean;
1158
+ }
1159
+ | {
1160
+ readonly kind: 'box';
1161
+ readonly center: SpatialPoint3;
1162
+ readonly rotation: readonly [x: number, y: number, z: number, w: number];
1163
+ readonly halfExtents: SpatialPoint3;
1164
+ readonly color: string;
1165
+ readonly opacity?: number;
1166
+ readonly dashed?: boolean;
1167
+ }
1168
+ | {
1169
+ readonly kind: 'capsule';
1170
+ readonly center: SpatialPoint3;
1171
+ readonly rotation: readonly [x: number, y: number, z: number, w: number];
1172
+ readonly radius: number;
1173
+ readonly halfHeight: number;
1174
+ readonly color: string;
1175
+ readonly opacity?: number;
1176
+ readonly dashed?: boolean;
1177
+ };
1178
+
1179
+ /** One draggable point. `id` is stable only within its owning node; `label`
1180
+ * feeds accessible/editor hints rather than becoming a persisted name. */
1181
+ export interface SpatialDragHandle {
1182
+ readonly id: string;
1183
+ readonly label: string;
1184
+ readonly position: SpatialPoint3;
1185
+ readonly color: string;
1186
+ readonly writable: boolean;
1187
+ readonly reason?: string;
1188
+ }
1189
+
1190
+ /** One adapter-owned component projection. `category` is a viewport overlay
1191
+ * channel (for example `audio`), not a game-object kind; unknown categories
1192
+ * obey the master Helpers toggle instead of being rejected. */
1193
+ export interface SpatialHandleLayer {
1194
+ readonly id: string;
1195
+ readonly category: string;
1196
+ readonly guides: readonly SpatialHandleGuide[];
1197
+ readonly handles: readonly SpatialDragHandle[];
1198
+ }
1199
+
1200
+ /**
1201
+ * Format-neutral direct-manipulation seam for component-owned spatial values.
1202
+ *
1203
+ * The viewport draws only the returned geometry. During a drag it returns a
1204
+ * world-space point to the adapter; the adapter alone knows whether that means
1205
+ * an AudioSource distance, a light cone, a collider extent, or something else,
1206
+ * and the adapter alone owns live preview + source persistence. `commit` runs
1207
+ * once at gesture end, so source/HMR/history are never hammered per pointer
1208
+ * move. Absent means this adapter has no spatial component handles.
1209
+ */
1210
+ export interface SpatialHandlesProvider {
1211
+ layers(id: string): readonly SpatialHandleLayer[];
1212
+ preview(id: string, handleId: string, worldPosition: SpatialPoint3): void;
1213
+ /** Commit the preview and pass through the owning writer's acknowledgement.
1214
+ * `void` remains the honest live-only/no-write floor; callers must not grade
1215
+ * it as a persisted round trip. */
1216
+ commit(id: string, handleId: string, worldPosition: SpatialPoint3): StructuralWriteOutcome;
1217
+ }
1218
+
1219
+ export interface AuthoringAdapter {
1220
+ readonly capabilities: AuthoringCapabilities;
1221
+ /** Spec 29 §5 — seam-level provenance ("what is truth behind these rows").
1222
+ * Absent ⇒ the shell shows no provenance badge (degrade silently — never
1223
+ * fabricate a claim the adapter didn't make). */
1224
+ readonly provenance?: AuthoringProvenance;
1225
+ readonly hierarchy: HierarchyProvider; // required — minimum is "read the tree"
1226
+ readonly selection?: SelectionProvider;
1227
+ readonly transforms?: TransformProvider;
1228
+ readonly inspector?: InspectorProvider;
1229
+ /** The selected node is an atomic asset that can open in the Asset Editor. */
1230
+ readonly assetSubject?: AssetSubjectProvider;
1231
+ readonly related?: RelatedSubjectsProvider;
1232
+ /** Source-derived prefab/component instance overrides and native writes. */
1233
+ readonly instances?: ComponentInstancesProvider;
1234
+ readonly structure?: StructureProvider;
1235
+ readonly persistence?: PersistenceProvider;
1236
+ /** D12 — per-layer viewport picking. Absent ⇒ this adapter is not pickable. */
1237
+ readonly pickable?: PickProvider;
1238
+ /** T0 (spec 27 §2) — per-node screen geometry for a DOM visual editor's overlay/
1239
+ * snap/measure math. Absent ⇒ this adapter produces no rects (no overlay). */
1240
+ readonly rects?: RectProvider;
1241
+ /** T0 (spec 27 §2) — spatial drag-resize/move → source/data write for non-Object3D
1242
+ * (DOM) nodes. Absent ⇒ no box-edit gesture for this adapter. */
1243
+ readonly boxEdit?: BoxEditProvider;
1244
+ /** T0 (spec 27 §2) — double-click-to-edit-text on the contract (wires the react
1245
+ * adapter's existing off-contract `editText`). Absent ⇒ no in-place text edit. */
1246
+ readonly text?: TextProvider;
1247
+ /** D3 (spec 27 §6) — eyedropper fallback color sampling. Absent ⇒ the
1248
+ * canvas eyedropper swatch has no non-native path for this adapter. */
1249
+ readonly colorSample?: ColorSampleProvider;
1250
+ /** D4 — storybook stories. Absent ⇒ no stories for any node in this adapter. */
1251
+ readonly stories?: StoriesProvider;
1252
+ /** Projection subject → source/data anchor and write lane, resolved together. */
1253
+ readonly truth?: TruthProvider;
1254
+ /** Adapter-owned component guides and direct-manipulation points. */
1255
+ readonly spatialHandles?: SpatialHandlesProvider;
1256
+ /** Asset drop (hierarchy + viewport). Absent ⇒ this adapter accepts no drops. */
1257
+ readonly assetDrop?: AssetDropProvider;
1258
+ /** Change notification → UI refresh. */
1259
+ subscribe?(listener: () => void): () => void;
1260
+ }
1261
+
1262
+ /**
1263
+ * THE PROVIDER VOCABULARY — every member of {@link AuthoringAdapter} beyond the
1264
+ * two an adapter cannot exist without.
1265
+ *
1266
+ * Why this exists: the editor's coverage warnings are supposed to make stopping
1267
+ * short of a native root IMPOSSIBLE, and they cannot do that while the row
1268
+ * vocabulary is a hand-written list frozen at what its author happened to know.
1269
+ * A list like that goes quiet at "all warnings closed" while whole capabilities
1270
+ * — stories, asset drop, related subjects, structure — were never enumerated at
1271
+ * all, so nothing ever warned about them. So the vocabulary IS the type: adding
1272
+ * a provider to `AuthoringAdapter` and forgetting it here is a COMPILE ERROR
1273
+ * (the `Record` below is missing a key), and inventing a key the type does not
1274
+ * have is an excess-property error. There is no third way to add a provider.
1275
+ *
1276
+ * `capabilities` and `hierarchy` are excluded because they are required members
1277
+ * — every adapter has them by construction, so "absent" is not a state they can
1278
+ * be in and a row about them could never say anything.
1279
+ */
1280
+ export type AuthoringProviderKey = Exclude<keyof AuthoringAdapter, 'capabilities' | 'hierarchy'>;
1281
+
1282
+ /**
1283
+ * The exhaustiveness pin. A `Record` over the key union, so the compiler — not
1284
+ * a scan, not a reviewer — is what refuses a vocabulary that has drifted from
1285
+ * the interface above. The values are `true` and carry no meaning: the KEYS are
1286
+ * the payload.
1287
+ */
1288
+ const AUTHORING_PROVIDER_PRESENCE: Readonly<Record<AuthoringProviderKey, true>> = {
1289
+ provenance: true,
1290
+ selection: true,
1291
+ transforms: true,
1292
+ inspector: true,
1293
+ assetSubject: true,
1294
+ related: true,
1295
+ instances: true,
1296
+ structure: true,
1297
+ persistence: true,
1298
+ pickable: true,
1299
+ rects: true,
1300
+ boxEdit: true,
1301
+ text: true,
1302
+ colorSample: true,
1303
+ stories: true,
1304
+ truth: true,
1305
+ spatialHandles: true,
1306
+ assetDrop: true,
1307
+ subscribe: true,
1308
+ };
1309
+
1310
+ /**
1311
+ * The vocabulary as an ordered list — declaration order of the interface, so
1312
+ * two reports of the same root are diffable line for line.
1313
+ *
1314
+ * Derived from {@link AUTHORING_PROVIDER_PRESENCE} rather than spelled a second
1315
+ * time: one list that the compiler pins to the type, and nothing downstream can
1316
+ * enumerate a different set.
1317
+ */
1318
+ export const AUTHORING_PROVIDER_KEYS: readonly AuthoringProviderKey[] = Object.keys(
1319
+ AUTHORING_PROVIDER_PRESENCE,
1320
+ ) as AuthoringProviderKey[];
1321
+
1322
+ /**
1323
+ * Which providers this adapter actually exposes — PRESENCE, measured off the
1324
+ * object itself. Nothing here consults `capabilities`, the adapter's class, the
1325
+ * route that built it, or the game's id: an adapter this function has never
1326
+ * heard of gets the same answer as one it has.
1327
+ */
1328
+ export function measureAuthoringProviders(
1329
+ adapter: AuthoringAdapter,
1330
+ ): Readonly<Record<AuthoringProviderKey, boolean>> {
1331
+ const measured = {} as Record<AuthoringProviderKey, boolean>;
1332
+ for (const key of AUTHORING_PROVIDER_KEYS) {
1333
+ measured[key] = (adapter as unknown as Record<string, unknown>)[key] !== undefined;
1334
+ }
1335
+ return measured;
1336
+ }