@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,138 @@
1
+ /** Exhaustive proof requirements for mounted SystemAdapters. A bound slot is
2
+ * availability, not success: each nested operation has its own required proof
3
+ * stage and therefore cannot hide behind `active[slot] != null`. */
4
+
5
+ import type { PhysicsAdapter2D } from './physics-adapter-2d';
6
+ import { defineSeamShape, type SeamShape } from './seam-evidence';
7
+ import type {
8
+ AudioAdapter,
9
+ CameraAdapter,
10
+ DebugAdapter,
11
+ NavigationAdapter,
12
+ NetworkingAdapter,
13
+ PhysicsAdapter,
14
+ RenderDebugAdapter,
15
+ SystemAdapters,
16
+ } from './system-adapter';
17
+
18
+ export const SYSTEM_ADAPTERS_SHAPE = defineSeamShape<SystemAdapters>()({
19
+ physics: { optional: true, kind: 'value', required: 'effect' },
20
+ networking: { optional: true, kind: 'value', required: 'operation' },
21
+ navigation: { optional: true, kind: 'value', required: 'operation' },
22
+ audio: { optional: true, kind: 'value', required: 'effect' },
23
+ camera: { optional: true, kind: 'value', required: 'effect' },
24
+ debug: { optional: true, kind: 'value', required: 'operation' },
25
+ renderDebug: { optional: true, kind: 'value', required: 'effect' },
26
+ });
27
+
28
+ export const PHYSICS_ADAPTER_SHAPE = defineSeamShape<PhysicsAdapter>()({
29
+ keyedBy: { optional: true, kind: 'value', required: 'shape' },
30
+ ownerOf: { optional: false, kind: 'function', required: 'operation' },
31
+ freeze: { optional: false, kind: 'function', required: 'effect' },
32
+ commit: { optional: false, kind: 'function', required: 'effect' },
33
+ unfreeze: { optional: false, kind: 'function', required: 'effect' },
34
+ debugDraw: { optional: true, kind: 'function', required: 'operation' },
35
+ setDebugDrawEnabled: { optional: true, kind: 'function', required: 'effect' },
36
+ contactPoints: { optional: true, kind: 'function', required: 'operation' },
37
+ colliders: { optional: true, kind: 'function', required: 'operation' },
38
+ previewCollider: { optional: true, kind: 'function', required: 'effect' },
39
+ joints: { optional: true, kind: 'function', required: 'operation' },
40
+ previewJointAnchor: { optional: true, kind: 'function', required: 'effect' },
41
+ });
42
+
43
+ export const PHYSICS_2D_ADAPTER_SHAPE = defineSeamShape<PhysicsAdapter2D>()({
44
+ keyedBy: { optional: false, kind: 'value', required: 'shape' },
45
+ ownerOf: { optional: false, kind: 'function', required: 'operation' },
46
+ freeze: { optional: false, kind: 'function', required: 'effect' },
47
+ commit: { optional: false, kind: 'function', required: 'effect' },
48
+ unfreeze: { optional: false, kind: 'function', required: 'effect' },
49
+ });
50
+
51
+ export const NETWORKING_ADAPTER_SHAPE = defineSeamShape<NetworkingAdapter>()({
52
+ peers: { optional: false, kind: 'function', required: 'operation' },
53
+ networkId: { optional: false, kind: 'function', required: 'operation' },
54
+ authority: { optional: false, kind: 'function', required: 'operation' },
55
+ editable: { optional: false, kind: 'function', required: 'operation' },
56
+ getConnectionState: { optional: true, kind: 'function', required: 'operation' },
57
+ getRoomInfo: { optional: true, kind: 'function', required: 'operation' },
58
+ getReplicationStats: { optional: true, kind: 'function', required: 'operation' },
59
+ subscribe: { optional: false, kind: 'function', required: 'effect' },
60
+ getStateSnapshot: { optional: true, kind: 'function', required: 'operation' },
61
+ messageEvents: { optional: true, kind: 'function', required: 'operation' },
62
+ getRates: { optional: true, kind: 'function', required: 'operation' },
63
+ getConditioning: { optional: true, kind: 'function', required: 'operation' },
64
+ setConditioning: { optional: true, kind: 'function', required: 'effect' },
65
+ getServerConfig: { optional: true, kind: 'function', required: 'operation' },
66
+ getPlayerIdentity: { optional: true, kind: 'function', required: 'operation' },
67
+ setPlayerIdentity: { optional: true, kind: 'function', required: 'effect' },
68
+ getTrafficByType: { optional: true, kind: 'function', required: 'operation' },
69
+ sendMessage: { optional: true, kind: 'function', required: 'effect' },
70
+ getConditioningLimits: { optional: true, kind: 'function', required: 'operation' },
71
+ ping: { optional: true, kind: 'function', required: 'effect' },
72
+ inspectServer: { optional: true, kind: 'function', required: 'operation' },
73
+ disconnectClient: { optional: true, kind: 'function', required: 'effect' },
74
+ editServerState: { optional: true, kind: 'function', required: 'effect' },
75
+ deleteServerState: { optional: true, kind: 'function', required: 'effect' },
76
+ sendToClient: { optional: true, kind: 'function', required: 'effect' },
77
+ broadcast: { optional: true, kind: 'function', required: 'effect' },
78
+ disposeRoom: { optional: true, kind: 'function', required: 'effect' },
79
+ stateFieldType: { optional: true, kind: 'function', required: 'operation' },
80
+ clearTraffic: { optional: true, kind: 'function', required: 'effect' },
81
+ getTrafficByEntity: { optional: true, kind: 'function', required: 'operation' },
82
+ isRecording: { optional: true, kind: 'function', required: 'operation' },
83
+ setRecording: { optional: true, kind: 'function', required: 'effect' },
84
+ });
85
+
86
+ export const NAVIGATION_ADAPTER_SHAPE = defineSeamShape<NavigationAdapter>()({
87
+ hasNavMesh: { optional: false, kind: 'function', required: 'operation' },
88
+ findPath: { optional: false, kind: 'function', required: 'effect' },
89
+ debugMesh: { optional: false, kind: 'function', required: 'operation' },
90
+ bake: { optional: true, kind: 'function', required: 'effect' },
91
+ exportData: { optional: true, kind: 'function', required: 'operation' },
92
+ clear: { optional: true, kind: 'function', required: 'effect' },
93
+ crowdAgents: { optional: true, kind: 'function', required: 'operation' },
94
+ });
95
+
96
+ export const AUDIO_ADAPTER_SHAPE = defineSeamShape<AudioAdapter>()({
97
+ resume: { optional: true, kind: 'function', required: 'effect' },
98
+ setMuted: { optional: false, kind: 'function', required: 'effect' },
99
+ isMuted: { optional: false, kind: 'function', required: 'operation' },
100
+ graphSnapshot: { optional: true, kind: 'function', required: 'operation' },
101
+ transportState: { optional: true, kind: 'function', required: 'operation' },
102
+ acquireMeters: { optional: true, kind: 'function', required: 'effect' },
103
+ acquireRecordingStream: { optional: true, kind: 'function', required: 'effect' },
104
+ renderOffline: { optional: true, kind: 'function', required: 'operation' },
105
+ audioEvents: { optional: true, kind: 'function', required: 'operation' },
106
+ });
107
+
108
+ export const CAMERA_ADAPTER_SHAPE = defineSeamShape<CameraAdapter>()({
109
+ snapshot: { optional: false, kind: 'function', required: 'operation' },
110
+ subscribe: { optional: false, kind: 'function', required: 'effect' },
111
+ });
112
+
113
+ export const DEBUG_ADAPTER_SHAPE = defineSeamShape<DebugAdapter>()({
114
+ providers: { optional: false, kind: 'function', required: 'operation' },
115
+ state: { optional: false, kind: 'function', required: 'operation' },
116
+ stateAll: { optional: false, kind: 'function', required: 'operation' },
117
+ commands: { optional: false, kind: 'function', required: 'operation' },
118
+ invoke: { optional: false, kind: 'function', required: 'effect' },
119
+ events: { optional: false, kind: 'function', required: 'operation' },
120
+ });
121
+
122
+ export const RENDER_DEBUG_ADAPTER_SHAPE = defineSeamShape<RenderDebugAdapter>()({
123
+ captureFrame: { optional: false, kind: 'function', required: 'effect' },
124
+ memorySnapshot: { optional: true, kind: 'function', required: 'operation' },
125
+ });
126
+
127
+ type NonPhysicsSystemSlot = Exclude<keyof SystemAdapters, 'physics'>;
128
+
129
+ export const SYSTEM_PROVIDER_SHAPES: {
130
+ readonly [K in NonPhysicsSystemSlot]: SeamShape<NonNullable<SystemAdapters[K]>>;
131
+ } = {
132
+ networking: NETWORKING_ADAPTER_SHAPE,
133
+ navigation: NAVIGATION_ADAPTER_SHAPE,
134
+ audio: AUDIO_ADAPTER_SHAPE,
135
+ camera: CAMERA_ADAPTER_SHAPE,
136
+ debug: DEBUG_ADAPTER_SHAPE,
137
+ renderDebug: RENDER_DEBUG_ADAPTER_SHAPE,
138
+ };
@@ -0,0 +1,301 @@
1
+ /**
2
+ * `createSystemSlot` — THE implementation of the declared-slot façade, the
3
+ * seam between a STATIC `systems` declaration and a LIVE adapter that only
4
+ * exists once the world tree is mounted.
5
+ *
6
+ * WHY IT EXISTS. An entry declares its systems statically
7
+ * (`export const systems = { networking: networkingSystem() }`), while the
8
+ * thing being declared — a joined Colyseus room, a baked navmesh, a mounted
9
+ * `<Physics>`, a cutscene's camera ownership — is built inside the world tree,
10
+ * later, and may be replaced (reconnect, remount, hot restart) or go away
11
+ * (stop). The slot is a stable object the host installs at mount and holds
12
+ * forever; every call forwards to whatever is attached right now.
13
+ *
14
+ * ── THE HONESTY RULE, STATED IN ONE PLACE ───────────────────────────────────
15
+ * This is why the pattern is centralized: five hand-rolled copies disagreed
16
+ * about it, and each disagreement was found by a separate review.
17
+ *
18
+ * - An **action** with no live target REFUSES BY NAME
19
+ * (`<name>: <unattachedMessage>`). Never a silent no-op — a caller that
20
+ * told the game to DO something must learn that it did not happen. An
21
+ * action against a live adapter that lacks the member refuses the same
22
+ * way, naming the member.
23
+ * - A **read** answers its declared HONEST EMPTY — `'disconnected'`, `[]`,
24
+ * `null`, `false`. That is not a fabrication: it is the truth of the
25
+ * not-attached state, in the same vocabulary the editor's own edit-mode
26
+ * adapters use. Reads are also the only kind safe to call from a per-frame
27
+ * debug draw, which is why a throw into a rAF loop is never the answer.
28
+ * - An **optional** member MIRRORS THE LIVE ADAPTER'S PRESENCE: absent while
29
+ * unattached, and absent afterwards when the live adapter itself does not
30
+ * provide it. Presence IS the consume-by-presence protocol's signal, so
31
+ * the slot may never invent it. Each presence read CAPTURES the adapter it
32
+ * saw and forwards to that same instance, so a detach between the read and
33
+ * the call answers as the adapter the caller was handed rather than a
34
+ * mid-flight swap.
35
+ * - A **subscribe** member SURVIVES REATTACHMENT: listeners live on the
36
+ * slot, never on the instance, and the slot re-binds its own single
37
+ * subscription on every attach/detach. A panel that subscribed once to the
38
+ * stable slot object is therefore never left holding a disposed system, and
39
+ * it is notified when the live half appears or disappears.
40
+ * - A member the config DOES NOT LIST does not exist on the slot at all.
41
+ * That is how the anti-shim rule spells omission — e.g. `PhysicsAdapter`'s
42
+ * `debugDraw`/`contactPoints` over `@react-three/rapier`, whose debug line
43
+ * segments live inside the fiber tree and expose no handle. Absence is a
44
+ * capability answer; a stub is a lie.
45
+ *
46
+ * ── RESOURCE OWNERSHIP ──────────────────────────────────────────────────────
47
+ * OWNER: whoever built the live adapter (the hook, the bridge component). The
48
+ * slot owns only the POINTER to it plus its own listener set and its own
49
+ * subscription to the live half. SHARERS: every reader of the declared
50
+ * system — editor inspectors, instruments. TEARDOWN: the function `attach`
51
+ * returns, and nothing else. It is IDENTITY-GUARDED — it clears only while
52
+ * the same instance is still attached, so a replace-then-teardown-the-old
53
+ * ordering cannot detach the new one — and detaching never disposes the
54
+ * adapter, because the slot did not create it.
55
+ *
56
+ * Static declarations shared across mounts must use `declareScopedSystem`.
57
+ * The adapter supplies an opaque mount scope and the host resolves the declaration
58
+ * once per root. A slot then belongs to that evaluation, never to the page.
59
+ *
60
+ * NOT A WRAPPER. The slot's members have the adapter interface's own exact
61
+ * signatures and hand back the implementation's own values; nothing stands
62
+ * between a caller and the live library object.
63
+ */
64
+
65
+ import type { VolterGameSystemEmpty } from './ingest/game-contract';
66
+
67
+ /**
68
+ * `absent(reason)` — THE WHOLE-SLOT counterpart of the four per-member policies
69
+ * above, and the fifth line of the same honesty rule.
70
+ *
71
+ * The four policies answer "this slot exists but nothing is attached yet". This
72
+ * one answers a different question entirely: **this game will never have that
73
+ * subsystem, and here is what was searched.** A slot simply left out of the
74
+ * `systems` table cannot say that — an omitted key means "unsupported", which is
75
+ * the same shape as "nobody looked", and the coverage report has to file it as a
76
+ * work order forever. Declaring the absence turns an UNANSWERED row into a
77
+ * terminal one.
78
+ *
79
+ * ```ts
80
+ * export const systems = {
81
+ * navigation: createNavigationAdapter(navigation),
82
+ * networking: absent('single-player: no Colyseus client anywhere in `src/`'),
83
+ * };
84
+ * ```
85
+ *
86
+ * ## It is a CONSTRUCTOR for the shape that already exists, not a second one
87
+ *
88
+ * The returned value IS `VolterGameSystemEmpty` — the plain
89
+ * `{ present: false, evidence }` record an INGESTED game declares on
90
+ * `window.volterGame.systems.systemAdapters`, validated by the same single
91
+ * projection (`ingest/contract-system-adapters.ts`). There is one shape law, not
92
+ * two: a foreign game cannot import this helper and must stay able to write the
93
+ * record by hand, so nothing here may brand the runtime value. What the helper
94
+ * buys a FIRST-PARTY author is the reason-quality check moved to the call site —
95
+ * our own code failing its own contract fails fast, where an ingested game's
96
+ * same mistake is filed as a `malformed` verdict for its coverage report.
97
+ *
98
+ * ## It is a MARKER, never a stub adapter
99
+ *
100
+ * Do not answer an absent slot with an adapter whose methods return zeros. A
101
+ * `getConnectionState()` of `'disconnected'` on a game with no transport implies
102
+ * a connection that could exist; that is the fabrication the anti-shim rule
103
+ * forbids. The absence is STATED, and the host installs nothing for it.
104
+ *
105
+ * @param reason What was searched and what was found, in the game's own source
106
+ * terms. "no netcode anywhere in `src/`" is a finished answer a reviewer can
107
+ * re-run; "not implemented yet" is a plan, and is rejected here.
108
+ */
109
+ export function absent(reason: string): VolterGameSystemEmpty {
110
+ if (typeof reason !== 'string' || reason.trim() === '') {
111
+ throw new Error(
112
+ 'absent(): an absence needs evidence — say what was searched and what was found ' +
113
+ '(e.g. "no Colyseus client anywhere in `src/`"), so a reviewer can re-run it. ' +
114
+ 'An unevidenced absence asserts something nobody can re-check.',
115
+ );
116
+ }
117
+ return { present: false, evidence: reason };
118
+ }
119
+
120
+ /** Any adapter member. `never[]` args make the conditional match contravariantly. */
121
+ type AnyFn = (...args: never[]) => unknown;
122
+
123
+ /** The declared member's return type, used to type a `read`'s honest empty. */
124
+ type MemberReturn<F> = NonNullable<F> extends (...args: never[]) => infer R ? R : never;
125
+
126
+ /**
127
+ * The per-member policy. There are exactly four, and every member of every
128
+ * declared slot in the repo is one of them — see the honesty rule above.
129
+ */
130
+ export type SystemSlotMemberSpec<F> =
131
+ | { readonly kind: 'action' }
132
+ | { readonly kind: 'read'; readonly empty: MemberReturn<F> }
133
+ | { readonly kind: 'subscribe' }
134
+ | { readonly kind: 'optional' };
135
+
136
+ /** The keys of `T` whose values are callable (data properties are not slottable). */
137
+ type FunctionMemberKeys<T> = {
138
+ [K in keyof T]-?: NonNullable<T[K]> extends AnyFn ? K : never;
139
+ }[keyof T];
140
+
141
+ /** Per-member policies. A member left out is left OFF the slot entirely. */
142
+ export type SystemSlotMembers<T> = {
143
+ readonly [K in FunctionMemberKeys<T>]?: SystemSlotMemberSpec<T[K]>;
144
+ };
145
+
146
+ export interface SystemSlotConfig<T extends object> {
147
+ /**
148
+ * The slot's own name, used as the prefix of every refusal so a stack-less
149
+ * error still names the seam (`'networkingSystem'`).
150
+ */
151
+ readonly name: string;
152
+ /**
153
+ * The rest of an action's refusal while nothing is attached — the DOMAIN
154
+ * half of the message, naming what has not happened yet ("the world is not
155
+ * playing, or the navmesh has not finished building").
156
+ */
157
+ readonly unattachedMessage: string;
158
+ readonly members: SystemSlotMembers<T>;
159
+ }
160
+
161
+ export interface SystemSlot<T extends object> {
162
+ /**
163
+ * Publish `live` as the slot's target. Returns the ONE detach path, which
164
+ * is identity-guarded: it clears only while `live` is still the attached
165
+ * instance, and never disposes it.
166
+ */
167
+ attach(live: T): () => void;
168
+ /** The stable declared object. Hand THIS to the entry's `systems` map. */
169
+ readonly slot: T;
170
+ }
171
+
172
+ export function createSystemSlot<T extends object>(config: SystemSlotConfig<T>): SystemSlot<T> {
173
+ const { name, unattachedMessage, members } = config;
174
+
175
+ let live: T | null = null;
176
+ /** Subscribers to the stable slot object; they outlive any one live instance. */
177
+ const listeners = new Set<() => void>();
178
+ /** The slot's own single subscription to whatever is attached. */
179
+ let liveUnsubscribe: (() => void) | null = null;
180
+ let subscribeMember: string | null = null;
181
+
182
+ const asRecord = (value: object): Record<string, unknown> =>
183
+ value as unknown as Record<string, unknown>;
184
+
185
+ const liveMember = (member: string): AnyFn | undefined => {
186
+ if (!live) return undefined;
187
+ const value = asRecord(live)[member];
188
+ return typeof value === 'function' ? (value as AnyFn) : undefined;
189
+ };
190
+
191
+ const notify = (): void => {
192
+ for (const listener of [...listeners]) listener();
193
+ };
194
+
195
+ const bindLive = (): void => {
196
+ liveUnsubscribe?.();
197
+ liveUnsubscribe = null;
198
+ if (!live || subscribeMember === null || listeners.size === 0) return;
199
+ const subscribe = liveMember(subscribeMember);
200
+ if (!subscribe) return;
201
+ const result = subscribe.call(live, notify as never);
202
+ liveUnsubscribe = typeof result === 'function' ? (result as () => void) : null;
203
+ };
204
+
205
+ const target: Record<string, unknown> = {};
206
+
207
+ const declared = Object.entries(members) as [string, SystemSlotMemberSpec<unknown>][];
208
+ for (const [member, spec] of declared) {
209
+ switch (spec.kind) {
210
+ case 'action': {
211
+ // Refuse by name, twice over: no live adapter at all, or a live
212
+ // adapter that does not carry this member (only ever a foreign
213
+ // replacement — the blessed implementations carry all of theirs).
214
+ target[member] = (...args: never[]): unknown => {
215
+ if (!live) throw new Error(`${name}: ${unattachedMessage}`);
216
+ const fn = liveMember(member);
217
+ if (!fn) throw new Error(`${name}: the live adapter has no ${member}().`);
218
+ return fn.apply(live, args);
219
+ };
220
+ break;
221
+ }
222
+ case 'read': {
223
+ const { empty } = spec;
224
+ target[member] = (...args: never[]): unknown => {
225
+ const current = live;
226
+ const fn = liveMember(member);
227
+ const value = fn ? fn.apply(current, args) : undefined;
228
+ return value ?? empty;
229
+ };
230
+ break;
231
+ }
232
+ case 'subscribe': {
233
+ if (subscribeMember !== null) {
234
+ throw new Error(
235
+ `${name}: a slot declares at most one 'subscribe' member (${subscribeMember} and ${member}).`,
236
+ );
237
+ }
238
+ subscribeMember = member;
239
+ target[member] = (listener: () => void): (() => void) => {
240
+ listeners.add(listener);
241
+ bindLive();
242
+ return () => {
243
+ listeners.delete(listener);
244
+ bindLive();
245
+ };
246
+ };
247
+ break;
248
+ }
249
+ case 'optional': {
250
+ Object.defineProperty(target, member, {
251
+ enumerable: true,
252
+ configurable: true,
253
+ get(): AnyFn | undefined {
254
+ const current = live;
255
+ const fn = liveMember(member);
256
+ if (!fn) return undefined;
257
+ return (...args: never[]): unknown => fn.apply(current, args);
258
+ },
259
+ });
260
+ break;
261
+ }
262
+ }
263
+ }
264
+
265
+ const attach = (next: T): (() => void) => {
266
+ live = next;
267
+ bindLive();
268
+ notify();
269
+ return () => {
270
+ if (live !== next) return;
271
+ live = null;
272
+ bindLive();
273
+ notify();
274
+ };
275
+ };
276
+
277
+ // THE ONE CAST. `exactOptionalPropertyTypes` forbids an optional member
278
+ // whose getter can yield `undefined`, but that is precisely the honest
279
+ // shape here — the getter IS how presence mirrors the live adapter — and a
280
+ // reflectively built object cannot be typed structurally anyway. Callers
281
+ // see exactly the absent/present behavior the adapter interface documents.
282
+ return { attach, slot: target as T };
283
+ }
284
+
285
+ /** Optional mount binding for a static system declaration. The contract knows
286
+ * only an opaque identity; the implementing adapter owns its native scope. */
287
+ const scopedSystems = new WeakMap<object, (scope: object) => object>();
288
+
289
+ export function declareScopedSystem<T extends object>(
290
+ declaration: T,
291
+ bind: (scope: object) => T,
292
+ ): T {
293
+ scopedSystems.set(declaration, bind);
294
+ return declaration;
295
+ }
296
+
297
+ /** Ordinary, already-bound adapters retain their exact identity. */
298
+ export function bindScopedSystem<T extends object>(declaration: T, scope: object): T {
299
+ const bind = scopedSystems.get(declaration);
300
+ return bind ? (bind(scope) as T) : declaration;
301
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The neutral transform value used across adapter interfaces. Position + scale
3
+ * are xyz; rotation is a quaternion (xyzw) — matching `Object3D.quaternion`, so
4
+ * adapters never have to agree on an Euler convention.
5
+ */
6
+ export interface Transform {
7
+ position: [number, number, number];
8
+ rotation: [number, number, number, number];
9
+ scale: [number, number, number];
10
+ }
11
+
12
+ /**
13
+ * Who currently owns an object's transform — i.e. what writes it each frame.
14
+ * The editor uses this to decide whether a gizmo edit is durable (`editor`),
15
+ * needs simulation coordination (`physics`), or is best-effort/inspect-only
16
+ * (`script`/`network`/`external`).
17
+ */
18
+ /**
19
+ * Who drives a node's transform right now.
20
+ *
21
+ * `'unresolved'` is the answer for a node THIS ADAPTER DOES NOT KNOW, and it is
22
+ * a distinct answer on purpose: every other member names a driver, so returning
23
+ * one of them for an unknown id is a positive claim about a node the
24
+ * implementer has never seen. A game-scoped physics adapter is asked about
25
+ * every node the editor touches, including nodes belonging to another root —
26
+ * "not mine" has to be sayable.
27
+ */
28
+ export type TransformOwner =
29
+ | 'editor'
30
+ | 'physics'
31
+ | 'animation'
32
+ | 'script'
33
+ | 'network'
34
+ | 'external'
35
+ | 'unresolved';
@@ -0,0 +1,92 @@
1
+ /** The editor's OWN captured dock arrangements (Animate, Look), shared data used by
2
+ * imported layout components. A package's workspace ships its own beside its
3
+ * layout contribution (`@volter/editor-blender`'s `model-arrangement.json`). */
4
+
5
+ /**
6
+ * ONE EDITOR AREA of a workspace, and the document that fills it.
7
+ *
8
+ * **A Blender editor AREA is an editor group; the drawer holds utilities**
9
+ * (orchestrator ruling 2026-09-19, WORK.md I5). Blender's UV Editing is a
10
+ * side-by-side split of the centre (786/785 measured on the engine), Shading
11
+ * and Animation a top/bottom split, and both the dock and the frame have
12
+ * exactly that shape in editor groups — a second group beside or below the
13
+ * document, with the workspace's own split ratio. So the UV editor, the node
14
+ * editor and the image view are DOCUMENTS a workspace opens
15
+ * into editor groups, never drawer views; the drawer keeps the utilities that
16
+ * are not Blender areas (Console, Profiler, Network).
17
+ *
18
+ * THIS AMENDS "a workspace positions chrome and dock groups only; it never
19
+ * opens documents" (ARCHITECTURE-CORE §Editor chrome, 2026-08-30), and the
20
+ * amendment is narrow: a workspace opens the documents ITS OWN REFERENCE
21
+ * FRAME'S SCREEN HAS, named here, and never a session's content. That is why
22
+ * `normalizePresetLayout` still strips every `kind: 'document'` panel out of a
23
+ * captured arrangement — the capture carries the GROUP and its settled size,
24
+ * this list carries which document fills it, and the two never overlap.
25
+ */
26
+ export interface WorkspaceAreaContribution {
27
+ /** Stable area id, scoped to the workspace. BOTH hosts key an editor group by
28
+ * it: the dock's group id is `volter:area:<id>`, and under the Code-OSS frame
29
+ * it is the key of `volterDocuments.ts`'s `Map<areaId, IEditorGroup>` — the
30
+ * group `IEditorGroupsService.addGroup` created from this area's `place`
31
+ * (WORK.md §THE TIMELINE item 1, landed 2026-09-20). */
32
+ readonly id: string;
33
+ /** The `workspace.document` CONTRIBUTION id this area opens (the
34
+ * contribution's file-derived id, e.g. `blender-uv-editor.document`), not a
35
+ * document id: the document is minted per open subject. */
36
+ readonly document: string;
37
+ /** Where the group sits relative to the centre document's group. */
38
+ readonly place: 'left' | 'right' | 'above' | 'below';
39
+ /** The area's share of that split, 0..1 — Blender's own measured ratio,
40
+ * cited by the workspace that declares it. It is the BOOTSTRAP size only:
41
+ * once a capture of this workspace exists, the captured leaf's own size
42
+ * wins, exactly as the drawer's captured height wins over its default.
43
+ * Under the frame the same rule is `setSize` applied ONCE at group
44
+ * creation, against the two groups' combined extent — a person's drag on
45
+ * the sash is never overwritten, and a group they closed is re-created at
46
+ * this ratio on the next reconcile, because a Blender area is resized or
47
+ * joined, never closed. The one other stand-up is the area's document
48
+ * changing its job and asking for that job's size
49
+ * (`@volter/sdk/kit/workspace-areas`' `setWorkspaceAreaRatio`). */
50
+ readonly ratio: number;
51
+ }
52
+
53
+ export interface WorkspaceArrangement {
54
+ /** Persistence identity, not a lookup key for an engine-owned implementation. */
55
+ readonly id: string;
56
+ readonly title: string;
57
+ readonly regions?: {
58
+ /** The WORKSPACE tab strip in the top bar (Blender's); a click on a tab
59
+ * is the explicit act that switches. Absent means hidden. */
60
+ readonly workspaceTabs?: 'shown' | 'hidden';
61
+ readonly header?: 'shown' | 'hidden';
62
+ readonly shelf?: 'shown' | 'hidden';
63
+ readonly inspector?: 'column' | 'properties';
64
+ readonly drawer?: 'shown' | 'hidden';
65
+ /** The top bar's runtime TELEMETRY cluster — the frame-rate readout, its
66
+ * sparkline and the audio meter. `hidden` leaves the corner to the quiet
67
+ * product controls (Invite, Account); the Profiler utility remains the
68
+ * door to the same numbers. Absent means shown. */
69
+ readonly telemetry?: 'shown' | 'hidden';
70
+ /** The `·TypeName` a component-instance row prints after its name in the
71
+ * hierarchy. `hidden` leaves the row its NAME alone, the way Blender's
72
+ * Outliner does — there the type is the glyph. The type stays reachable
73
+ * through the row's own tooltip and the inspector. Absent means shown. */
74
+ readonly hierarchyTypeSuffix?: 'shown' | 'hidden';
75
+ /** The dotted RULE a component-instance row draws under its name in the
76
+ * hierarchy. `hidden` leaves the name unmarked, the way Blender's Outliner
77
+ * does — it marks an instance nowhere in that panel, in any selection
78
+ * state. The fact stays reachable through the row's own tooltip, its Go to
79
+ * Callsite / Open Component Source actions and the inspector. Absent means
80
+ * shown. */
81
+ readonly hierarchyInstanceRule?: 'shown' | 'hidden';
82
+ /** Which restriction columns a hierarchy row carries at its right edge:
83
+ * this editor's selection lock beside the eye (`select+viewport`), or
84
+ * Blender's default filter (`viewport+render`) — the eye in its own
85
+ * column with the render column reserved and blank, three.js having one
86
+ * visibility flag for viewport and render alike. Absent means the pair. */
87
+ readonly hierarchyRestrictions?: 'select+viewport' | 'viewport+render';
88
+ };
89
+ /** The workspace's EDITOR AREAS beside the centre document — see
90
+ * {@link WorkspaceAreaContribution}. Absent: the centre document alone. */
91
+ readonly areas?: readonly WorkspaceAreaContribution[];
92
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * THE SYSTEM-PHASE VOCABULARY — the order a frame runs in, and the shape a
3
+ * project's own system declares itself with.
4
+ *
5
+ * It lives in the CONTRACT because two runtimes speak it and neither owns it:
6
+ * `@volter/game-runtime`'s loop and system runner schedule by phase, and
7
+ * `@volter/threejs-runtime`'s animation clock orders its evaluators by the same
8
+ * `PHASE_ORDER`. A vocabulary two shipped twins both read is the contract's,
9
+ * or it is a value edge between twins — and that edge is the one thing the
10
+ * twin row may not have (docs/ARCHITECTURE-CORE.md §The target shape: a twin
11
+ * depends on the project contract and its medium's libraries).
12
+ *
13
+ * `scripts/generate-engine-manifest.ts` renders this file into
14
+ * `packages/project/schemas/engine-api.json`.
15
+ */
16
+
17
+ /**
18
+ * System execution phases, in order.
19
+ * Each frame, systems run in this exact sequence.
20
+ */
21
+ export const SystemPhase = {
22
+ INPUT: 'input',
23
+ PRE_PHYSICS: 'prePhysics',
24
+ PHYSICS: 'physics',
25
+ POST_PHYSICS: 'postPhysics',
26
+ GAME_LOGIC: 'gameLogic',
27
+ ANIMATION: 'animation',
28
+ PRE_RENDER: 'preRender',
29
+ RENDER: 'render',
30
+ } as const;
31
+
32
+ export type SystemPhaseName = (typeof SystemPhase)[keyof typeof SystemPhase];
33
+
34
+ /** Order of phase execution */
35
+ export const PHASE_ORDER: SystemPhaseName[] = [
36
+ SystemPhase.INPUT,
37
+ SystemPhase.PRE_PHYSICS,
38
+ SystemPhase.PHYSICS,
39
+ SystemPhase.POST_PHYSICS,
40
+ SystemPhase.GAME_LOGIC,
41
+ SystemPhase.ANIMATION,
42
+ SystemPhase.PRE_RENDER,
43
+ SystemPhase.RENDER,
44
+ ];
45
+
46
+ /** A system is just a function that takes delta time */
47
+ export type SystemFn = (dt: number) => void;
48
+
49
+ export interface SystemOptions {
50
+ /** Stable diagnostic label; falls back to the function name. */
51
+ readonly name?: string;
52
+ }