@volter/project 0.5.211 → 0.5.212
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.
- package/NOTICE +2 -2
- package/README.md +8 -15
- package/package.json +5 -26
- package/schemas/volter-project.schema.json +33 -145
- package/schemas/volter-settings.schema.json +11 -49
- package/src/adapter/adapter-module.ts +204 -465
- package/src/adapter/adapter-surface.ts +4 -32
- package/src/adapter/authoring-seam-contract.ts +7 -24
- package/src/adapter/authoring.ts +126 -265
- package/src/adapter/editor-looks.ts +96 -127
- package/src/adapter/finders/registry.ts +6 -21
- package/src/adapter/host-context.ts +17 -72
- package/src/adapter/index.ts +4 -77
- package/src/adapter/manifest-interpreter.ts +7 -50
- package/src/adapter/physics-adapter-2d.ts +4 -21
- package/src/adapter/root-adapter.ts +37 -111
- package/src/adapter/root-seam-contract.ts +3 -6
- package/src/adapter/root-stacking.ts +4 -46
- package/src/adapter/seam-evidence.ts +0 -16
- package/src/adapter/system-adapter.ts +24 -656
- package/src/adapter/system-seam-contract.ts +1 -91
- package/src/adapter/transform.ts +4 -3
- package/src/adapter/workspace-arrangement.ts +29 -49
- package/src/manifest/editor-port.ts +10 -36
- package/src/manifest/filename.ts +3 -42
- package/src/manifest/load-file.ts +9 -15
- package/src/manifest/load.ts +44 -179
- package/src/manifest/locate.ts +6 -29
- package/src/manifest/schema.ts +155 -427
- package/src/semver.ts +16 -0
- package/src/settings/keys.ts +9 -11
- package/src/settings/schema.ts +55 -44
- package/schemas/engine-api.json +0 -57
- package/schemas/engine-api.md +0 -22
- package/src/adapter/binding.ts +0 -445
- package/src/adapter/debug-registry-ref.ts +0 -19
- package/src/adapter/frame-capture.ts +0 -80
- package/src/adapter/ingest/contract-system-slots.ts +0 -28
- package/src/adapter/ingest/game-contract-seams.ts +0 -86
- package/src/adapter/ingest/game-contract.ts +0 -506
- package/src/adapter/ingest/mount-readiness.ts +0 -140
- package/src/adapter/loop-gate-report.ts +0 -53
- package/src/adapter/native-entry-surface.ts +0 -100
- package/src/adapter/render-audio.ts +0 -20
- package/src/adapter/render-memory.ts +0 -34
- package/src/adapter/renderer-config.ts +0 -84
- package/src/adapter/system-slot.ts +0 -301
- package/src/core/system-phase.ts +0 -52
- package/src/inspection.ts +0 -281
- package/src/manifest/configuration-kinds.ts +0 -241
- package/src/manifest/kind-modules.ts +0 -58
- package/src/manifest/runtime-environment.ts +0 -48
package/src/manifest/schema.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The project manifest schema (`volter.project.json`).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// reader (T4.1 policy): it parses via this schema, then layers the
|
|
6
|
-
// cross-field checks and default resolution §3/§5 describe. Every field below
|
|
7
|
-
// has a `.describe()` (repo policy — powers `scripts/generate-schema.ts` and
|
|
8
|
-
// the T4.1 schema-walk coverage test).
|
|
3
|
+
// load.ts parses this schema, checks cross-field constraints and resolves defaults.
|
|
4
|
+
// schemas/volter-project.schema.json is the matching hand-maintained JSON schema.
|
|
9
5
|
//
|
|
10
6
|
// Naming: `XxxSchema` is the Zod schema, `Xxx` the inferred type. Every schema
|
|
11
7
|
// file in this repo reads that way, so the bare names stay reserved for the
|
|
12
8
|
// types.
|
|
13
9
|
//
|
|
14
10
|
// Adapter surface values mirror `AdapterSurface` in
|
|
15
|
-
// `
|
|
11
|
+
// `adapter/adapter-surface.ts` (`'stage' | 'canvas' | 'dom'`)
|
|
16
12
|
// by value, not by import. The adapter remains the sole root discriminator.
|
|
17
13
|
|
|
18
14
|
import { z } from 'zod';
|
|
@@ -21,7 +17,7 @@ import { z } from 'zod';
|
|
|
21
17
|
* Current on-disk `volter.project.json` format. Exported so editor/server
|
|
22
18
|
* compatibility checks do not duplicate the schema's literal value.
|
|
23
19
|
*/
|
|
24
|
-
export const
|
|
20
|
+
export const PROJECT_MANIFEST_VERSION = 2 as const;
|
|
25
21
|
|
|
26
22
|
// ---------------------------------------------------------------------------
|
|
27
23
|
// Root adapter identity. The adapter is the sole discriminator: a root never
|
|
@@ -29,206 +25,117 @@ export const GAME_MANIFEST_VERSION = 2 as const;
|
|
|
29
25
|
// adapters carry the surface their adapter contract exposes because the host
|
|
30
26
|
// must allocate a layer before the module has mounted.
|
|
31
27
|
//
|
|
32
|
-
// The surface vocabulary is `
|
|
28
|
+
// The surface vocabulary is `stage | canvas | dom` — what the HOST HANDS the
|
|
33
29
|
// root, not which library it uses. `canvas` and `dom` are platform primitives:
|
|
34
|
-
// a canvas root may be
|
|
30
|
+
// a canvas root may be any 2D or 3D library, raw WebGL/WebGPU or a 2D context,
|
|
35
31
|
// and a dom root may be React, Vue, Svelte or plain HTML, all through the
|
|
36
|
-
// identical host.
|
|
37
|
-
// that has nothing to do with either, which foreclosed the rest by
|
|
38
|
-
// construction.
|
|
32
|
+
// identical host.
|
|
39
33
|
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// (`editor-game/src/host/roots/r3f-root.tsx`), `canvas` through the Pixi lane
|
|
43
|
-
// (`@volter/game-runtime/canvas-react`), `dom` through the React lane. In every case the
|
|
44
|
-
// entry's document is its own TSX source. A root the engine does NOT mount
|
|
45
|
-
// arrives as an `{ ingest }` root or a `{ module }` adapter the project
|
|
46
|
-
// supplies, both of which name their `surface` explicitly.
|
|
34
|
+
// Bare adapter strings declare the host surface; module and ingest adapters
|
|
35
|
+
// also state the surface explicitly. React UI roots use the DOM surface.
|
|
47
36
|
//
|
|
48
|
-
// `
|
|
49
|
-
// platform cannot: the ONE shared
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// The pre-rename spellings `threejs`/`pixijs`/`react` are REMOVED, not
|
|
54
|
-
// normalized (the legacy-removal doctrine at
|
|
55
|
-
// ARCHITECTURE-CORE §Vocabulary). Normalization was a second way to say the
|
|
56
|
-
// same thing: two spellings both parsed, so both kept being written, and
|
|
57
|
-
// SECOND readers grew (the dev server's surface probe, the hosted-editor
|
|
58
|
-
// bundler's alias sets) each re-implementing the same map. A manifest naming
|
|
59
|
-
// an old spelling now fails to parse with an issue that names the canonical
|
|
60
|
-
// value and the exact one-line edit.
|
|
37
|
+
// `stage` earns a kind of its own because the host must provide something the
|
|
38
|
+
// platform cannot: the ONE shared stage library instance (identity matters —
|
|
39
|
+
// ingest projects bring their own, and a React reconciler over it, e.g. the 3D
|
|
40
|
+
// integration's, depends on dedupe). React needs no equivalent, which is
|
|
41
|
+
// exactly why react is a `dom` root rather than its own kind.
|
|
61
42
|
// ---------------------------------------------------------------------------
|
|
62
43
|
|
|
63
|
-
/** Removed spellings → the canonical value each one must be rewritten to. */
|
|
64
|
-
const REMOVED_SURFACE_SPELLING: Readonly<Record<string, 'three' | 'canvas' | 'dom'>> = {
|
|
65
|
-
threejs: 'three',
|
|
66
|
-
pixijs: 'canvas',
|
|
67
|
-
react: 'dom',
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/** Removed spellings legal as a BARE adapter (a root the engine itself mounts). */
|
|
71
|
-
const REMOVED_BARE_ADAPTER_SPELLING: Readonly<Record<string, 'three' | 'canvas' | 'dom'>> = {
|
|
72
|
-
threejs: 'three',
|
|
73
|
-
pixijs: 'canvas',
|
|
74
|
-
react: 'dom',
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
function removedSpellingMessage(bad: string, where: 'adapter' | 'adapter.surface'): string {
|
|
78
|
-
const canonical =
|
|
79
|
-
where === 'adapter' ? REMOVED_BARE_ADAPTER_SPELLING[bad] : REMOVED_SURFACE_SPELLING[bad];
|
|
80
|
-
return (
|
|
81
|
-
`Game manifest: \`${where}\`: "${bad}" — that spelling was REMOVED ` +
|
|
82
|
-
'(ARCHITECTURE-CORE §Vocabulary). The surface vocabulary is ' +
|
|
83
|
-
"'three' | 'canvas' | 'dom' — what the HOST HANDS the root, never which " +
|
|
84
|
-
`library it uses.\nFix: change \`${where}\` to "${canonical}".`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Reject a removed surface spelling wherever one may appear — the bare
|
|
89
|
-
* shorthand, or the `surface` field of a module/ingest adapter — with an issue
|
|
90
|
-
* naming the canonical value. Runs as a Zod preprocess so the message wins
|
|
91
|
-
* over the union's generic discriminant error. */
|
|
92
|
-
function rejectRemovedRootAdapterSpelling(value: unknown, ctx: z.core.$RefinementCtx): unknown {
|
|
93
|
-
if (typeof value === 'string' && REMOVED_BARE_ADAPTER_SPELLING[value]) {
|
|
94
|
-
ctx.addIssue({ code: 'custom', message: removedSpellingMessage(value, 'adapter') });
|
|
95
|
-
return z.NEVER;
|
|
96
|
-
}
|
|
97
|
-
if (value && typeof value === 'object' && 'surface' in value) {
|
|
98
|
-
const surface = (value as { surface?: unknown }).surface;
|
|
99
|
-
if (typeof surface === 'string' && REMOVED_SURFACE_SPELLING[surface]) {
|
|
100
|
-
ctx.addIssue({
|
|
101
|
-
code: 'custom',
|
|
102
|
-
message: removedSpellingMessage(surface, 'adapter.surface'),
|
|
103
|
-
});
|
|
104
|
-
return z.NEVER;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return value;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
44
|
export const RootAdapterSchema = z
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
z
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
module
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
surface
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
surface
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
'imports instead would be a large non-seam-shaped diff against unmodified ' +
|
|
201
|
-
'source — this declares the fact rather than editing it away.',
|
|
202
|
-
),
|
|
203
|
-
domStubs: z
|
|
204
|
-
.array(z.string())
|
|
205
|
-
.optional()
|
|
206
|
-
.describe(
|
|
207
|
-
'DOM API stub ids this ingested game requires to run headlessly/in-realm',
|
|
208
|
-
),
|
|
209
|
-
captureTimeoutMs: z
|
|
210
|
-
.number()
|
|
211
|
-
.optional()
|
|
212
|
-
.describe(
|
|
213
|
-
"How long the host waits for this game's first captured frame before the mount " +
|
|
214
|
-
'FAILS by name. A game that needs a long boot (streamed world assets, a menu ' +
|
|
215
|
-
'the player must clear) raises it; the default is 10s.',
|
|
216
|
-
),
|
|
217
|
-
})
|
|
218
|
-
.strict()
|
|
219
|
-
.describe(
|
|
220
|
-
'Ingest adapter configuration for an unmodified game (a repo-vendored game or your ' +
|
|
221
|
-
'own external folder)',
|
|
222
|
-
),
|
|
223
|
-
})
|
|
224
|
-
.strict(),
|
|
225
|
-
]),
|
|
226
|
-
)
|
|
45
|
+
.union([
|
|
46
|
+
z.enum(['stage', 'canvas', 'dom']),
|
|
47
|
+
z
|
|
48
|
+
.object({
|
|
49
|
+
module: z
|
|
50
|
+
.string()
|
|
51
|
+
.describe('Project-folder-relative path to a custom adapter module (trust boundary)'),
|
|
52
|
+
surface: z
|
|
53
|
+
.enum(['stage', 'canvas', 'dom'])
|
|
54
|
+
.describe('Native surface exposed by this custom adapter contract'),
|
|
55
|
+
})
|
|
56
|
+
.strict(),
|
|
57
|
+
z
|
|
58
|
+
.object({
|
|
59
|
+
surface: z
|
|
60
|
+
.enum(['stage', 'canvas', 'dom'])
|
|
61
|
+
.describe('Native surface captured from the unmodified project'),
|
|
62
|
+
ingest: z
|
|
63
|
+
.object({
|
|
64
|
+
dataWriter: z
|
|
65
|
+
.string()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe(
|
|
68
|
+
"Project-relative path to a HOST-ADDED ES module that writes this project's own " +
|
|
69
|
+
'DATA file back — the sibling of a source write for a project whose authorable ' +
|
|
70
|
+
'truth is not source. A level-based project places its enemies, pickups and ' +
|
|
71
|
+
'objectives in binary records inside a level file, reaching no source ' +
|
|
72
|
+
'literal, so an ' +
|
|
73
|
+
'authored move of one is an edit to that file and nothing else can write it. ' +
|
|
74
|
+
'The module declares two exports and the host knows nothing else about it: ' +
|
|
75
|
+
'`dataFile` (a project-relative string — the ONE file this writer edits) and ' +
|
|
76
|
+
'`planDataEdit(bytes, {record, property, baseline, next})`, which returns ' +
|
|
77
|
+
'`{changed: true, bytes}` or `{changed: false, reason}` and may be async. ' +
|
|
78
|
+
'It is loaded in the editor realm beside the project, so it may `import()` the ' +
|
|
79
|
+
"project's own modules to resolve what only a running project knows (a level " +
|
|
80
|
+
'writer asks the project`s own spatial lookup which cell a moved object ' +
|
|
81
|
+
'landed in, and refuses "outside the level" rather than writing data the project ' +
|
|
82
|
+
'would read back wrong). It NEVER fabricates: a record it cannot address, or ' +
|
|
83
|
+
'a property it does not model, is a named refusal. ' +
|
|
84
|
+
'An object is anchored to a record by carrying `userData.volterRecordIndex` — ' +
|
|
85
|
+
'the project declares that identity itself (a recorded patch or a shim); the ' +
|
|
86
|
+
'host never infers one.',
|
|
87
|
+
),
|
|
88
|
+
assets: z
|
|
89
|
+
.record(z.string(), z.string())
|
|
90
|
+
.optional()
|
|
91
|
+
.describe(
|
|
92
|
+
'Path-substring -> served-URL rewrites for this ingested project',
|
|
93
|
+
),
|
|
94
|
+
moduleAliases: z
|
|
95
|
+
.record(z.string(), z.string())
|
|
96
|
+
.optional()
|
|
97
|
+
.describe(
|
|
98
|
+
"The project's OWN bundler path aliases, restated as a mounting fact so its " +
|
|
99
|
+
'source resolves unedited. A project built with its own Vite/webpack config ' +
|
|
100
|
+
'routinely imports itself through an alias (`~/lib/config`, `@/store/x`); the ' +
|
|
101
|
+
"editor boots Vite from the ENGINE's config and never reads a project's own, so " +
|
|
102
|
+
'without this every such import is an unresolvable specifier and the mount dies ' +
|
|
103
|
+
'at transform time. Key = the specifier PREFIX exactly as the project writes it ' +
|
|
104
|
+
'("~/"); value = the repo-root-relative directory it means ' +
|
|
105
|
+
'("/vendor/<id>/src/"). SELF-SCOPING: the host honors an entry only for ' +
|
|
106
|
+
"importers inside the vendored folder the value points into, so one project's " +
|
|
107
|
+
"alias can never capture another module's import. Rewriting the project's own " +
|
|
108
|
+
'imports instead would be a large non-seam-shaped diff against unmodified ' +
|
|
109
|
+
'source — this declares the fact rather than editing it away.',
|
|
110
|
+
),
|
|
111
|
+
domStubs: z
|
|
112
|
+
.array(z.string())
|
|
113
|
+
.optional()
|
|
114
|
+
.describe(
|
|
115
|
+
'DOM API stub ids this ingested project requires to run headlessly/in-realm',
|
|
116
|
+
),
|
|
117
|
+
captureTimeoutMs: z
|
|
118
|
+
.number()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe(
|
|
121
|
+
"How long the host waits for this project's first captured frame before the mount " +
|
|
122
|
+
'FAILS by name. A project that needs a long boot (streamed world assets, a menu ' +
|
|
123
|
+
'the player must clear) raises it; the default is 10s.',
|
|
124
|
+
),
|
|
125
|
+
})
|
|
126
|
+
.strict()
|
|
127
|
+
.describe(
|
|
128
|
+
'Ingest adapter configuration for an unmodified project (a repo-vendored one or your ' +
|
|
129
|
+
'own external folder)',
|
|
130
|
+
),
|
|
131
|
+
})
|
|
132
|
+
.strict(),
|
|
133
|
+
])
|
|
227
134
|
.describe(
|
|
228
|
-
"Root adapter identity: built-in '
|
|
229
|
-
"the root — 'canvas' covers
|
|
135
|
+
"Root adapter identity: built-in 'stage'/'canvas'/'dom' (what the host hands " +
|
|
136
|
+
"the root — 'canvas' covers any canvas library or raw WebGL, 'dom' covers " +
|
|
230
137
|
'React/Vue/plain HTML), a custom { module, surface } adapter, or an ' +
|
|
231
|
-
'{ ingest, surface } adapter for an unmodified
|
|
138
|
+
'{ ingest, surface } adapter for an unmodified project.',
|
|
232
139
|
);
|
|
233
140
|
export type RootAdapter = z.infer<typeof RootAdapterSchema>;
|
|
234
141
|
|
|
@@ -241,14 +148,13 @@ export const AdapterRootSchema = z
|
|
|
241
148
|
id: z
|
|
242
149
|
.string()
|
|
243
150
|
.describe(
|
|
244
|
-
'Unique id for this root within the manifest (enforced-unique, D-V6 —
|
|
151
|
+
'Unique id for this root within the manifest (enforced-unique, D-V6 — ProjectManifestSchema.roots rejects duplicates)',
|
|
245
152
|
),
|
|
246
153
|
description: z
|
|
247
154
|
.string()
|
|
248
155
|
.optional()
|
|
249
156
|
.describe(
|
|
250
|
-
"Optional one-line description of this root's
|
|
251
|
-
"blurb — the manifest-native replacement for a registry entry's description field)",
|
|
157
|
+
"Optional one-line description of this root's content",
|
|
252
158
|
),
|
|
253
159
|
adapter: RootAdapterSchema.describe(
|
|
254
160
|
"This root's adapter and sole rendering/lifecycle discriminator",
|
|
@@ -257,27 +163,28 @@ export const AdapterRootSchema = z
|
|
|
257
163
|
.string()
|
|
258
164
|
.optional()
|
|
259
165
|
.describe(
|
|
260
|
-
'Module exporting a world root — a TSX
|
|
261
|
-
'React component (dom). Required for every
|
|
166
|
+
'Module exporting a world root — a TSX component, which the built-in stage dialect ' +
|
|
167
|
+
'renders, or a setup()/adapter (stage), or a React component (dom). Required for every ' +
|
|
168
|
+
'built-in adapter root.',
|
|
262
169
|
),
|
|
263
170
|
styles: z
|
|
264
171
|
.array(z.string())
|
|
265
172
|
.optional()
|
|
266
173
|
.describe(
|
|
267
174
|
"Project-root-relative CSS files this root's mount needs — the PAGE-level stylesheet a " +
|
|
268
|
-
'
|
|
175
|
+
'project written to own a browser tab keeps its layout in (an ingested project is the ' +
|
|
269
176
|
'standing case: its own entry imported the sheet, the host shim that replaces that ' +
|
|
270
177
|
'entry cannot, because `html`/`body`/`*` rules would restyle the editor). Declared ' +
|
|
271
178
|
'here, the editor serves each file rewritten to apply ONLY inside the container it ' +
|
|
272
|
-
"mounts the root's DOM in (`@scope`; packages/
|
|
273
|
-
"the HUD is styled on the UI board's story cards and on the
|
|
179
|
+
"mounts the root's DOM in (`@scope`; packages/sdk/src/scoped-project-css.ts), so " +
|
|
180
|
+
"the HUD is styled on the UI board's story cards and on the play surface without the " +
|
|
274
181
|
'editor seeing a single one of those rules. The files themselves are never modified.',
|
|
275
182
|
),
|
|
276
183
|
world: z
|
|
277
184
|
.object({
|
|
278
185
|
entry: z
|
|
279
186
|
.string()
|
|
280
|
-
.describe("Project-relative path to the
|
|
187
|
+
.describe("Project-relative path to the project's OWN module exporting that component"),
|
|
281
188
|
export: z
|
|
282
189
|
.string()
|
|
283
190
|
.optional()
|
|
@@ -288,21 +195,14 @@ export const AdapterRootSchema = z
|
|
|
288
195
|
.strict()
|
|
289
196
|
.optional()
|
|
290
197
|
.describe(
|
|
291
|
-
"The component this root's WORLD is, in the
|
|
198
|
+
"The component this root's WORLD is, in the project's own source. Two readers, and they " +
|
|
292
199
|
"mount different things: the EDITOR mounts THIS component alone as the root's " +
|
|
293
|
-
'design-time document, while `entry` above stays what PLAY mounts (the whole
|
|
200
|
+
'design-time document, while `entry` above stays what PLAY mounts (the whole project — ' +
|
|
294
201
|
'its own composition, its own HUD, its own loop). It sits on the ROOT rather than ' +
|
|
295
|
-
'inside any one adapter because the split it names is not about ingest: a
|
|
202
|
+
'inside any one adapter because the split it names is not about ingest: a project whose ' +
|
|
296
203
|
'runtime entry is a SHELL over the thing an author actually edits has this shape ' +
|
|
297
|
-
'whether the source is vendored or first-party.
|
|
298
|
-
'
|
|
299
|
-
'alone; and a translated Unity/Godot port, whose `entry` is a scene HOST that mounts ' +
|
|
300
|
-
'the scene the built player starts on (`EditorBuildSettings` index 0 — routinely a ' +
|
|
301
|
-
'menu with no 3D content at all) while the named export opens the scene an author ' +
|
|
302
|
-
"edits. That is Unity's own split: build settings say where the PLAYER starts; the " +
|
|
303
|
-
'Scene view opens a scene, and Unity keeps that fact somewhere else entirely. A game ' +
|
|
304
|
-
'whose world is not a separate component omits this and keeps mounting through ' +
|
|
305
|
-
'`entry` in both modes.',
|
|
204
|
+
'whether the source is vendored or first-party. A project whose world is not a separate ' +
|
|
205
|
+
'component omits this and keeps mounting through `entry` in both modes.',
|
|
306
206
|
),
|
|
307
207
|
zOrder: z
|
|
308
208
|
.number()
|
|
@@ -328,55 +228,52 @@ export type AdapterRoot = z.infer<typeof AdapterRootSchema>;
|
|
|
328
228
|
* The MEDIUM a root occupies — its surface, i.e. what the host hands it. A
|
|
329
229
|
* bare adapter string names its own surface; `{ module, surface }` and
|
|
330
230
|
* `{ ingest, surface }` name it explicitly. Bucketing by surface is what makes
|
|
331
|
-
* a first-party
|
|
231
|
+
* a first-party canvas world, an ingested canvas project and a custom
|
|
332
232
|
* canvas module ONE medium: they all compete for the same canvas surface, and
|
|
333
233
|
* which library draws on it is not what the rule below is about.
|
|
334
234
|
*/
|
|
335
|
-
function rootMedium(root: AdapterRoot): '
|
|
235
|
+
function rootMedium(root: AdapterRoot): 'stage' | 'canvas' | 'dom' {
|
|
336
236
|
return typeof root.adapter === 'string' ? root.adapter : root.adapter.surface;
|
|
337
237
|
}
|
|
338
238
|
|
|
339
239
|
// ---------------------------------------------------------------------------
|
|
340
|
-
// Learn metadata
|
|
240
|
+
// Learn metadata
|
|
341
241
|
// ---------------------------------------------------------------------------
|
|
342
242
|
|
|
343
243
|
export const LearnKindSchema = z
|
|
344
|
-
.enum(['starter', 'feature', 'sample-
|
|
244
|
+
.enum(['starter', 'feature', 'sample-project', 'lesson-companion'])
|
|
345
245
|
.describe(
|
|
346
|
-
'Content kind
|
|
347
|
-
'feature (demonstrates a subsystem), sample-
|
|
246
|
+
'Content kind: starter (minimal per-genre starting point), ' +
|
|
247
|
+
'feature (demonstrates a subsystem), sample-project (a complete project as an architectural ' +
|
|
348
248
|
'reference), lesson-companion (exists to back a Learn-site lesson)',
|
|
349
249
|
);
|
|
350
|
-
export type LearnKind = z.infer<typeof LearnKindSchema>;
|
|
351
250
|
|
|
352
251
|
export const LearnDifficultySchema = z
|
|
353
252
|
.enum(['beginner', 'intermediate', 'advanced'])
|
|
354
253
|
.describe('Learner-facing difficulty rating');
|
|
355
|
-
export type LearnDifficulty = z.infer<typeof LearnDifficultySchema>;
|
|
356
254
|
|
|
357
255
|
export const LearnMetadataSchema = z
|
|
358
256
|
.object({
|
|
359
257
|
kind: LearnKindSchema,
|
|
360
|
-
title: z.string().min(1).describe('Learner-facing display title
|
|
361
|
-
summary: z.string().min(1).describe('One-sentence learner-facing summary
|
|
258
|
+
title: z.string().min(1).describe('Learner-facing display title'),
|
|
259
|
+
summary: z.string().min(1).describe('One-sentence learner-facing summary'),
|
|
362
260
|
difficulty: LearnDifficultySchema,
|
|
363
261
|
features: z
|
|
364
262
|
.array(z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'features tags must be kebab-case'))
|
|
365
|
-
.describe('Short kebab-case
|
|
263
|
+
.describe('Short kebab-case tags describing project features'),
|
|
366
264
|
thumbnail: z
|
|
367
265
|
.string()
|
|
368
266
|
.optional()
|
|
369
267
|
.describe(
|
|
370
268
|
'Optional thumbnail image reference, PROJECT-relative (canonical path: ' +
|
|
371
|
-
'learn/thumbnail.png
|
|
372
|
-
'render tooling (scripts/generate-learn-thumbnails.ts), never hand-made',
|
|
269
|
+
'learn/thumbnail.png)',
|
|
373
270
|
),
|
|
374
271
|
preview: z
|
|
375
272
|
.string()
|
|
376
273
|
.optional()
|
|
377
274
|
.describe(
|
|
378
275
|
'Optional short preview video/loop reference, PROJECT-relative (canonical path: ' +
|
|
379
|
-
'learn/preview.mp4
|
|
276
|
+
'learn/preview.mp4)',
|
|
380
277
|
),
|
|
381
278
|
lesson: z
|
|
382
279
|
.string()
|
|
@@ -388,45 +285,38 @@ export const LearnMetadataSchema = z
|
|
|
388
285
|
})
|
|
389
286
|
.strict()
|
|
390
287
|
.describe(
|
|
391
|
-
'Learner-facing metadata
|
|
392
|
-
'gallery, New Project wizard, and Learn-site catalog. Registries are generated from this ' +
|
|
393
|
-
'block; there is no parallel hand-edited catalog.',
|
|
288
|
+
'Learner-facing metadata carried by example project manifests.',
|
|
394
289
|
);
|
|
395
290
|
export type LearnMetadata = z.infer<typeof LearnMetadataSchema>;
|
|
396
291
|
|
|
397
292
|
// ---------------------------------------------------------------------------
|
|
398
|
-
//
|
|
293
|
+
// Project manifest (volter.project.json)
|
|
399
294
|
// ---------------------------------------------------------------------------
|
|
400
295
|
|
|
401
|
-
|
|
402
|
-
const ConfigurationEnvelopeSchema = z
|
|
403
|
-
.object({
|
|
404
|
-
id: z.string().min(1).describe('Configuration id, unique in the manifest'),
|
|
405
|
-
kind: z.string().min(1).describe('A registered run kind'),
|
|
406
|
-
})
|
|
407
|
-
.passthrough();
|
|
408
|
-
|
|
409
|
-
export const GameManifestSchema = z
|
|
296
|
+
export const ProjectManifestSchema = z
|
|
410
297
|
.object({
|
|
411
298
|
$schema: z
|
|
412
299
|
.string()
|
|
413
300
|
.optional()
|
|
414
301
|
.describe('Optional JSON Schema URI for editor autocomplete and validation'),
|
|
415
302
|
manifestVersion: z
|
|
416
|
-
.
|
|
417
|
-
.
|
|
418
|
-
|
|
303
|
+
.number()
|
|
304
|
+
.int()
|
|
305
|
+
.describe(
|
|
306
|
+
`The manifest format version the project was written with (this editor writes ${PROJECT_MANIFEST_VERSION}); a manifest is read by its shape, never refused for its number`,
|
|
307
|
+
),
|
|
308
|
+
name: z.string().describe('Project display name'),
|
|
419
309
|
appId: z
|
|
420
310
|
.string()
|
|
421
311
|
.optional()
|
|
422
312
|
.describe('Stable reverse-domain application id used by packaged targets'),
|
|
423
|
-
version: z.string().describe("The
|
|
313
|
+
version: z.string().describe("The project's own version"),
|
|
424
314
|
engine: z
|
|
425
315
|
.object({
|
|
426
316
|
version: z
|
|
427
317
|
.string()
|
|
428
318
|
.describe(
|
|
429
|
-
'Exact @volter/project semver this
|
|
319
|
+
'Exact @volter/project semver this project was scaffolded/last upgraded against — an ' +
|
|
430
320
|
'identity pin, not a range',
|
|
431
321
|
),
|
|
432
322
|
})
|
|
@@ -435,14 +325,8 @@ export const GameManifestSchema = z
|
|
|
435
325
|
roots: z
|
|
436
326
|
.array(AdapterRootSchema)
|
|
437
327
|
.superRefine((roots, ctx) => {
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
// cross-field check caught a collision, so any OTHER consumer of this
|
|
441
|
-
// schema (the generated JSON Schema, editor tooling) saw duplicate
|
|
442
|
-
// ids as legal. Enforced HERE instead, naming every colliding id (not
|
|
443
|
-
// just the first found) — a root id is the key every per-world editor
|
|
444
|
-
// surface is addressed by, so a silently-permitted duplicate would
|
|
445
|
-
// have two roots silently collide.
|
|
328
|
+
// Enforce unique root ids in the schema so every reader receives the same
|
|
329
|
+
// validation. Report every colliding id because editor surfaces key by it.
|
|
446
330
|
const counts = new Map<string, number>();
|
|
447
331
|
for (const root of roots) counts.set(root.id, (counts.get(root.id) ?? 0) + 1);
|
|
448
332
|
const duplicates = [...counts.entries()].filter(([, n]) => n > 1).map(([id]) => id);
|
|
@@ -452,22 +336,13 @@ export const GameManifestSchema = z
|
|
|
452
336
|
code: z.ZodIssueCode.custom,
|
|
453
337
|
message:
|
|
454
338
|
duplicates.length === 1
|
|
455
|
-
? `
|
|
456
|
-
: `
|
|
339
|
+
? `Project manifest: duplicate root id ${idsText} — root ids must be unique across the manifest (enforced-unique).`
|
|
340
|
+
: `Project manifest: duplicate root ids: ${idsText} — root ids must be unique across the manifest (enforced-unique).`,
|
|
457
341
|
});
|
|
458
342
|
}
|
|
459
343
|
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
// the canvas surface are the three buckets — a first-party canvas
|
|
463
|
-
// world, a canvas ingest and a canvas module are the SAME medium.
|
|
464
|
-
//
|
|
465
|
-
// Enforced here rather than left to the reader because the editor
|
|
466
|
-
// mounts exactly one root per medium (`resolveFocusedThreejsRootId`
|
|
467
|
-
// is a `find`, and the design-time layer stack hosts the one dom
|
|
468
|
-
// root): a second root of the same medium parsed cleanly and was then
|
|
469
|
-
// silently never mounted, which is the failure mode this rejects.
|
|
470
|
-
//
|
|
344
|
+
// The design-time stack hosts one root per medium. Reject a second root
|
|
345
|
+
// of the same surface before mounting.
|
|
471
346
|
const idsByMedium = new Map<string, string[]>();
|
|
472
347
|
for (const root of roots) {
|
|
473
348
|
const medium = rootMedium(root);
|
|
@@ -480,10 +355,10 @@ export const GameManifestSchema = z
|
|
|
480
355
|
ctx.addIssue({
|
|
481
356
|
code: z.ZodIssueCode.custom,
|
|
482
357
|
message:
|
|
483
|
-
`
|
|
358
|
+
`Project manifest: a project has at most one ${medium} world root — ` +
|
|
484
359
|
`${ids.map((id) => `"${id}"`).join(', ')} all occupy the ${medium} surface. ` +
|
|
485
360
|
'Keep one and remove the rest, or move a root to a different medium ' +
|
|
486
|
-
"(
|
|
361
|
+
"(stage / canvas / dom); only one root per medium is ever mounted, so a second one's " +
|
|
487
362
|
'content would never appear.',
|
|
488
363
|
});
|
|
489
364
|
}
|
|
@@ -491,7 +366,7 @@ export const GameManifestSchema = z
|
|
|
491
366
|
.describe(
|
|
492
367
|
"The project's explicit adapter-root composition — ZERO OR MORE roots, and no implicit root: a project with no roots mounts nothing (a library of models, a folder of documents) and the editor derives its chrome from what IS declared. " +
|
|
493
368
|
'Root ids are unique, and a project declares at most ONE world root per medium — ' +
|
|
494
|
-
'one `
|
|
369
|
+
'one `stage`, one `dom`, one canvas-surface root (first-party canvas, canvas ingest ' +
|
|
495
370
|
'and canvas module are the same medium).',
|
|
496
371
|
),
|
|
497
372
|
authoring: z
|
|
@@ -525,14 +400,6 @@ export const GameManifestSchema = z
|
|
|
525
400
|
.strict()
|
|
526
401
|
.optional()
|
|
527
402
|
.describe('Editor-only authoring metadata; never runtime ownership'),
|
|
528
|
-
configurations: z
|
|
529
|
-
.array(ConfigurationEnvelopeSchema)
|
|
530
|
-
.optional()
|
|
531
|
-
.describe(
|
|
532
|
-
"The project's run configurations — its ENTRYPOINTS beyond the host mount (`play`): each an " +
|
|
533
|
-
'id and a registered kind (`process`, `compound`, and whatever a capability registers) with ' +
|
|
534
|
-
"that kind's own fields. The editor's transport and a harness start the same declaration.",
|
|
535
|
-
),
|
|
536
403
|
resolution: z
|
|
537
404
|
.object({
|
|
538
405
|
width: z.number().describe('Canvas width in pixels'),
|
|
@@ -541,150 +408,11 @@ export const GameManifestSchema = z
|
|
|
541
408
|
.strict()
|
|
542
409
|
.optional()
|
|
543
410
|
.describe('Canvas resolution'),
|
|
544
|
-
rendering: z
|
|
545
|
-
.object({
|
|
546
|
-
antialias: z
|
|
547
|
-
.boolean()
|
|
548
|
-
.describe(
|
|
549
|
-
'Whether every three root is built with a multisampled drawing buffer. This is the ' +
|
|
550
|
-
'ONE render property a world cannot declare for itself (adapter/' +
|
|
551
|
-
'renderer-config.ts): a WebGL context fixes its sample count at CREATION from this ' +
|
|
552
|
-
'boolean, long before a world mounts, so it belongs to the PROJECT. The runtime ' +
|
|
553
|
-
'reader is mount-manifest.ts, which threads it into createHostRenderer for each ' +
|
|
554
|
-
'three root. WebGL exposes no sample COUNT — the implementation picks one (4x on ' +
|
|
555
|
-
'every desktop browser measured), so a source engine that authored 8x/16x gets ' +
|
|
556
|
-
'multisampling but not its exact count.',
|
|
557
|
-
),
|
|
558
|
-
})
|
|
559
|
-
.strict()
|
|
560
|
-
.optional()
|
|
561
|
-
.describe(
|
|
562
|
-
'Construction-time renderer properties, which no world can declare after the fact. ' +
|
|
563
|
-
'Everything a world CAN declare (tone mapping, output colour space, clear colour, ' +
|
|
564
|
-
"shadow filter) lives on the world's own renderer config instead.",
|
|
565
|
-
),
|
|
566
|
-
debug: z
|
|
567
|
-
.object({
|
|
568
|
-
allowInProduction: z
|
|
569
|
-
.boolean()
|
|
570
|
-
.describe(
|
|
571
|
-
'Allow the ?volter-debug=1 introspection bridge and debug-command invocation in ' +
|
|
572
|
-
'production builds. Default false: the bridge only installs in dev builds. The ' +
|
|
573
|
-
'runtime reader is the debug-bridge installer (D18).',
|
|
574
|
-
),
|
|
575
|
-
})
|
|
576
|
-
.strict()
|
|
577
|
-
.optional()
|
|
578
|
-
.describe(
|
|
579
|
-
'Debug-bridge production gating (D18) — governs whether ?volter-debug=1 installs ' +
|
|
580
|
-
'window.__volter outside dev builds.',
|
|
581
|
-
),
|
|
582
|
-
determinism: z
|
|
583
|
-
.object({
|
|
584
|
-
seededRandom: z
|
|
585
|
-
.boolean()
|
|
586
|
-
.describe(
|
|
587
|
-
'Declares that ALL gameplay RNG in this project flows through ctx.random (the ' +
|
|
588
|
-
'named-stream seeded PRNG) rather than raw ' +
|
|
589
|
-
'Math.random/Date.now/performance.now. Three runtime enforcers key off this flag: ' +
|
|
590
|
-
'the boot-time seeding reader (mount-manifest.ts seeds ctx.random from ' +
|
|
591
|
-
'defaultSeed/?volter-seed=/explicit config, in that precedence, only when true), the ' +
|
|
592
|
-
'gameplay-rng-ban burn-down scan (test/gameplay-rng-ban.test.ts — lints this ' +
|
|
593
|
-
"project's src/ for raw RNG/wall-clock calls), and the dev-mode Math.random phase " +
|
|
594
|
-
'trap (runtime/gameplay-rng-trap.ts — warns once per call site during a gameplay ' +
|
|
595
|
-
'frame, never throws). Default false: an undeclared project gets no determinism ' +
|
|
596
|
-
'contract at all (that IS the opt-out — no dead field).',
|
|
597
|
-
),
|
|
598
|
-
defaultSeed: z
|
|
599
|
-
.number()
|
|
600
|
-
.int()
|
|
601
|
-
.optional()
|
|
602
|
-
.describe(
|
|
603
|
-
'The seed ctx.random boots from when seededRandom is true, unless overridden by ' +
|
|
604
|
-
'?volter-seed=<int> (the query param always wins over this manifest default) or an ' +
|
|
605
|
-
'even higher-precedence explicit config value a host passes directly. Optional — ' +
|
|
606
|
-
"omit to fall back to the runtime's own fixed default seed.",
|
|
607
|
-
),
|
|
608
|
-
})
|
|
609
|
-
.strict()
|
|
610
|
-
.optional()
|
|
611
|
-
.describe(
|
|
612
|
-
'Seeded-RNG determinism contract (D15) — governs ' +
|
|
613
|
-
'whether ctx.random is boot-seeded from a reproducible seed and whether the ' +
|
|
614
|
-
'gameplay-rng-ban scan / dev-mode Math.random phase trap are active for this project.',
|
|
615
|
-
),
|
|
616
|
-
budget: z
|
|
617
|
-
.object({
|
|
618
|
-
frameCpuMsP95: z
|
|
619
|
-
.number()
|
|
620
|
-
.positive()
|
|
621
|
-
.optional()
|
|
622
|
-
.describe(
|
|
623
|
-
'Maximum allowed p95 of per-frame CPU time (ms) across the measured frames of a ' +
|
|
624
|
-
'`volter perf` seeded headless run. CPU-side only: headless runs rasterize under ' +
|
|
625
|
-
'SwiftShader, so this is main-thread frame cost, NOT real GPU frame time. ' +
|
|
626
|
-
'Machine-dependent — prefer the baseline-diff tolerance band for regression ' +
|
|
627
|
-
'gating and use this as a coarse absolute ceiling.',
|
|
628
|
-
),
|
|
629
|
-
physicsMsP95: z
|
|
630
|
-
.number()
|
|
631
|
-
.positive()
|
|
632
|
-
.optional()
|
|
633
|
-
.describe(
|
|
634
|
-
"Maximum allowed p95 of the profiler's `physics` phase time (ms) per fixed-step " +
|
|
635
|
-
'frame in a `volter perf` run. Same CPU-timing caveats as frameCpuMsP95.',
|
|
636
|
-
),
|
|
637
|
-
maxEntities: z
|
|
638
|
-
.number()
|
|
639
|
-
.int()
|
|
640
|
-
.nonnegative()
|
|
641
|
-
.optional()
|
|
642
|
-
.describe(
|
|
643
|
-
'Maximum allowed entity-tagged scene-graph nodes (nodes carrying ' +
|
|
644
|
-
"userData.entityId) summed across the game's three/canvas worlds at the end " +
|
|
645
|
-
'of a `volter perf` run. Exact and deterministic under a seed — no tolerance band.',
|
|
646
|
-
),
|
|
647
|
-
maxDrawCalls: z
|
|
648
|
-
.number()
|
|
649
|
-
.int()
|
|
650
|
-
.positive()
|
|
651
|
-
.optional()
|
|
652
|
-
.describe(
|
|
653
|
-
'Maximum allowed renderer draw calls in any measured frame of a `volter perf` run ' +
|
|
654
|
-
"(the profiler's per-frame render.drawCalls, from renderer.info). Exact and " +
|
|
655
|
-
'deterministic under a seed.',
|
|
656
|
-
),
|
|
657
|
-
maxTriangles: z
|
|
658
|
-
.number()
|
|
659
|
-
.int()
|
|
660
|
-
.positive()
|
|
661
|
-
.optional()
|
|
662
|
-
.describe(
|
|
663
|
-
'Maximum allowed rendered triangles in any measured frame of a `volter perf` run ' +
|
|
664
|
-
"(the profiler's per-frame render.triangles, from renderer.info). Exact and " +
|
|
665
|
-
'deterministic under a seed.',
|
|
666
|
-
),
|
|
667
|
-
})
|
|
668
|
-
.strict()
|
|
669
|
-
.refine((budget) => Object.values(budget).some((value) => value !== undefined), {
|
|
670
|
-
message:
|
|
671
|
-
'Game manifest: `budget` must set at least one metric (frameCpuMsP95, physicsMsP95, ' +
|
|
672
|
-
'maxEntities, maxDrawCalls, maxTriangles) — an empty budget block gates nothing ' +
|
|
673
|
-
'(the no-dead-fields policy).',
|
|
674
|
-
})
|
|
675
|
-
.optional()
|
|
676
|
-
.describe(
|
|
677
|
-
'Performance budget (W3d, F11 perf regression gates) asserted by ' +
|
|
678
|
-
'`volter perf --assert-budget`: a seeded, fixed-step headless run samples the engine ' +
|
|
679
|
-
'profiler and fails (nonzero exit) when any metric here is exceeded. Only ' +
|
|
680
|
-
'headlessly-measurable metrics are budgetable; real GPU frame time is not (SwiftShader).',
|
|
681
|
-
),
|
|
682
411
|
learn: LearnMetadataSchema.optional().describe(
|
|
683
|
-
'Optional learner-facing metadata
|
|
684
|
-
'gallery/wizard/Learn-catalog registries can be generated from the manifest itself',
|
|
412
|
+
'Optional learner-facing metadata for example projects.',
|
|
685
413
|
),
|
|
686
414
|
})
|
|
687
415
|
.strict()
|
|
688
|
-
.describe('
|
|
416
|
+
.describe('Project manifest (volter.project.json) — the complete v2 project configuration');
|
|
689
417
|
|
|
690
|
-
export type
|
|
418
|
+
export type ProjectManifest = z.infer<typeof ProjectManifestSchema>;
|