@vgai/sdk 0.5.2 → 0.5.3
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/package.json +3 -3
- package/src/account.ts +20 -0
- package/src/cinematic/gsap-operations.ts +3 -3
- package/src/cinematic/index.ts +3 -4
- package/src/cinematic/theatre-operations.ts +1 -1
- package/src/editor/index.ts +11 -4
- package/src/editor/inspection-operations.ts +189 -0
- package/src/editor/open-operations.ts +4 -44
- package/src/editor/transport.ts +94 -10
- package/src/operations.ts +10 -5
- package/src/perf/perf-run.ts +4 -4
- package/src/play/control-operations.ts +19 -19
- package/src/play/debug-command-operations.ts +2 -3
- package/src/play/index.ts +4 -5
- package/src/play/input-operations.ts +12 -12
- package/src/play/lifecycle-operations.ts +3 -3
- package/src/play/run-ticks-operations.ts +4 -4
- package/src/play/state-operations.ts +4 -4
- package/src/play/status-operations.ts +13 -13
- package/src/play/transport.ts +29 -38
- package/src/project/index.ts +15 -19
- package/src/project/inspection-node.ts +6 -5
- package/src/project/inspection-operation.ts +1 -1
- package/src/project/inspection.ts +6 -6
- package/src/project/manifest-operations.ts +12 -17
- package/src/project/shared.ts +4 -39
- package/src/registry.ts +9 -3
- package/src/render/index.ts +3 -4
- package/src/render/render-cinematic.ts +27 -28
- package/src/types.ts +7 -3
- package/src/project/component-operations.ts +0 -337
- package/src/project/entity-operations.ts +0 -366
- package/src/project/scene-operations.ts +0 -426
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `project.scene.*` (B2, §8 B2 "scene read/validate/diff/apply").
|
|
3
|
-
*
|
|
4
|
-
* REUSE, not reimplementation: every op here calls straight into the engine's
|
|
5
|
-
* existing scene machinery — `parseSceneFile`/`SceneParseError`
|
|
6
|
-
* (`@vgai/engine/scene/parse`), `applyDiff`/`ApplyDiffError`
|
|
7
|
-
* (`@vgai/engine/scene/scene-apply`), and `SceneDiffSchema`
|
|
8
|
-
* (`@vgai/engine/scene/scene-diff-schema`) — the exact same functions
|
|
9
|
-
* `vgai apply-diff` (`packages/vgai-cli/src/apply-diff.ts`) already uses. This
|
|
10
|
-
* module only adds the operation-registry envelope (dry-run, external-change
|
|
11
|
-
* detection, files-changed) around that reused core; it does not reimplement
|
|
12
|
-
* scene patching or validation.
|
|
13
|
-
*
|
|
14
|
-
* `project.scene.diff` (mutates:false) previews what a patch WOULD do —
|
|
15
|
-
* applies it in memory, re-validates the result, reports what would change —
|
|
16
|
-
* WITHOUT ever writing. `project.scene.apply` (mutates:true,
|
|
17
|
-
* supportsDryRun:true) is the real (or dry-run) write.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { existsSync } from 'node:fs';
|
|
21
|
-
import { dereferenceNewAssetRefs } from '@vgai/engine/scene/asset-ref-check';
|
|
22
|
-
import { parseSceneFile, SceneParseError } from '@vgai/engine/scene/parse';
|
|
23
|
-
import { ApplyDiffError, applyDiff } from '@vgai/engine/scene/scene-apply';
|
|
24
|
-
import { SceneDiffSchema } from '@vgai/engine/scene/scene-diff-schema';
|
|
25
|
-
import type { SceneFile } from '@vgai/engine/scene/scene-types';
|
|
26
|
-
import { z } from 'zod';
|
|
27
|
-
import { ToolError } from '../errors.js';
|
|
28
|
-
import { defineTool, type ToolRegistry } from '../registry.js';
|
|
29
|
-
import {
|
|
30
|
-
BaseHashField,
|
|
31
|
-
CONFLICT_ERROR,
|
|
32
|
-
checkNotStale,
|
|
33
|
-
DryRunField,
|
|
34
|
-
FILE_NOT_FOUND_ERROR,
|
|
35
|
-
mutationResultSchema,
|
|
36
|
-
NO_PROJECT_ROOT_ERROR,
|
|
37
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
38
|
-
readFileWithHash,
|
|
39
|
-
requireProjectRoot,
|
|
40
|
-
resolveProjectPath,
|
|
41
|
-
writeFileAtomic,
|
|
42
|
-
} from './shared.js';
|
|
43
|
-
|
|
44
|
-
/** Format Zod/scene-parse issues the same one-line-per-issue shape the CLI uses. */
|
|
45
|
-
export function formatIssues(issues: { path: PropertyKey[]; message: string }[]): string[] {
|
|
46
|
-
return issues.map((i) => `${i.path.join('.')}: ${i.message}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const SCENE_INVALID_ERROR = {
|
|
50
|
-
code: 'SCENE_INVALID',
|
|
51
|
-
summary: 'The scene file/document fails schema validation.',
|
|
52
|
-
data: z.object({ issues: z.array(z.string()) }),
|
|
53
|
-
} as const;
|
|
54
|
-
|
|
55
|
-
export const APPLY_DIFF_FAILED_ERROR = {
|
|
56
|
-
code: 'APPLY_DIFF_FAILED',
|
|
57
|
-
summary: 'applyDiff rejected the patch (bad id, bad parent, duplicate id, etc).',
|
|
58
|
-
data: z.object({
|
|
59
|
-
opIndex: z.number(),
|
|
60
|
-
opType: z.string(),
|
|
61
|
-
entityId: z.string(),
|
|
62
|
-
message: z.string(),
|
|
63
|
-
}),
|
|
64
|
-
} as const;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* B6 follow-up to B2 #20: `project.scene.apply` now shares the SAME
|
|
68
|
-
* asset-reference integrity check `vgai apply-diff` has always run
|
|
69
|
-
* (`@vgai/engine/scene/asset-ref-check`'s `dereferenceNewAssetRefs`) — a
|
|
70
|
-
* patch that introduces a `materialRef`/file-path `prefab`/binary reference
|
|
71
|
-
* to a missing or schema-invalid file is refused BEFORE any write, exactly
|
|
72
|
-
* like the CLI verb, so the two writers can never silently diverge in what
|
|
73
|
-
* they guarantee. (`animation.animGraph` was one such checked ref until E5
|
|
74
|
-
* removed the AnimGraph format entirely — an authored `animGraph` now fails
|
|
75
|
-
* earlier, at scene parse, via the removal-migration guard.)
|
|
76
|
-
*/
|
|
77
|
-
export const ASSET_REF_INVALID_ERROR = {
|
|
78
|
-
code: 'ASSET_REF_INVALID',
|
|
79
|
-
summary:
|
|
80
|
-
'The patch introduces one or more asset references (materialRef/prefab/binary) ' +
|
|
81
|
-
'that are missing on disk or fail their own schema — refused before any write.',
|
|
82
|
-
data: z.object({
|
|
83
|
-
errors: z.array(
|
|
84
|
-
z.object({
|
|
85
|
-
field: z.string(),
|
|
86
|
-
path: z.string(),
|
|
87
|
-
diskPath: z.string(),
|
|
88
|
-
reason: z.string(),
|
|
89
|
-
}),
|
|
90
|
-
),
|
|
91
|
-
}),
|
|
92
|
-
} as const;
|
|
93
|
-
|
|
94
|
-
/** Read + Zod-parse a scene file at `scenePath` (project-relative), returning both the raw hash and the validated document. */
|
|
95
|
-
export function readAndParseScene(
|
|
96
|
-
projectRoot: string,
|
|
97
|
-
scenePath: string,
|
|
98
|
-
): { absPath: string; hash: string; scene: SceneFile } {
|
|
99
|
-
const absPath = resolveProjectPath(projectRoot, scenePath);
|
|
100
|
-
const { raw, hash } = readFileWithHash(absPath);
|
|
101
|
-
let json: unknown;
|
|
102
|
-
try {
|
|
103
|
-
json = JSON.parse(raw);
|
|
104
|
-
} catch (err) {
|
|
105
|
-
throw new ToolError(
|
|
106
|
-
'SCENE_INVALID',
|
|
107
|
-
`${scenePath} is not valid JSON: ${err instanceof Error ? err.message : err}`,
|
|
108
|
-
{
|
|
109
|
-
issues: [String(err)],
|
|
110
|
-
},
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
const scene = parseSceneFile(json, scenePath);
|
|
115
|
-
return { absPath, hash, scene };
|
|
116
|
-
} catch (err) {
|
|
117
|
-
if (err instanceof SceneParseError) {
|
|
118
|
-
throw new ToolError('SCENE_INVALID', err.message, { issues: formatIssues(err.issues) });
|
|
119
|
-
}
|
|
120
|
-
throw err;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Apply `patch` to `scene`, mapping `ApplyDiffError` into the declared `APPLY_DIFF_FAILED` shape. */
|
|
125
|
-
export function applyDiffOrThrow(
|
|
126
|
-
scene: SceneFile,
|
|
127
|
-
patch: z.infer<typeof SceneDiffSchema>,
|
|
128
|
-
): SceneFile {
|
|
129
|
-
try {
|
|
130
|
-
return applyDiff(scene, patch);
|
|
131
|
-
} catch (err) {
|
|
132
|
-
if (err instanceof ApplyDiffError) {
|
|
133
|
-
throw new ToolError('APPLY_DIFF_FAILED', err.message, {
|
|
134
|
-
opIndex: err.opIndex,
|
|
135
|
-
opType: err.opType,
|
|
136
|
-
entityId: err.entityId,
|
|
137
|
-
message: err.message,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
throw err;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
// project.scene.read
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
const ProjectSceneReadInput = z
|
|
149
|
-
.object({ scenePath: z.string().describe('Project-relative path to a .vscn.json scene file.') })
|
|
150
|
-
.describe('Read + validate a scene file.');
|
|
151
|
-
|
|
152
|
-
const ProjectSceneReadResult = z
|
|
153
|
-
.object({
|
|
154
|
-
scenePath: z.string(),
|
|
155
|
-
contentHash: z
|
|
156
|
-
.string()
|
|
157
|
-
.describe('sha256 of the raw file bytes — pass as baseHash to a later mutation.'),
|
|
158
|
-
scene: z.unknown().describe('The validated scene document (SceneFile).'),
|
|
159
|
-
})
|
|
160
|
-
.describe('Scene read result.');
|
|
161
|
-
|
|
162
|
-
export const projectSceneRead = defineTool({
|
|
163
|
-
name: 'project.scene.read',
|
|
164
|
-
summary: 'Read + validate a .vscn.json scene file.',
|
|
165
|
-
description:
|
|
166
|
-
'Reuses @vgai/engine/scene/parse’s parseSceneFile — the same validator the runtime loader uses.',
|
|
167
|
-
input: ProjectSceneReadInput,
|
|
168
|
-
result: ProjectSceneReadResult,
|
|
169
|
-
errors: [
|
|
170
|
-
NO_PROJECT_ROOT_ERROR,
|
|
171
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
172
|
-
FILE_NOT_FOUND_ERROR,
|
|
173
|
-
SCENE_INVALID_ERROR,
|
|
174
|
-
],
|
|
175
|
-
requires: { project: true },
|
|
176
|
-
host: 'node',
|
|
177
|
-
mutates: false,
|
|
178
|
-
supportsDryRun: false,
|
|
179
|
-
permission: { risk: 'read', summary: 'Reads one scene file; no writes.' },
|
|
180
|
-
async impl(input, ctx) {
|
|
181
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
182
|
-
const { absPath, hash, scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
183
|
-
return { scenePath: absPath, contentHash: hash, scene };
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
// project.scene.validate
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
|
|
191
|
-
const ProjectSceneValidateInput = z
|
|
192
|
-
.object({
|
|
193
|
-
scenePath: z
|
|
194
|
-
.string()
|
|
195
|
-
.optional()
|
|
196
|
-
.describe('Validate this project-relative scene file instead of `scene`.'),
|
|
197
|
-
scene: z
|
|
198
|
-
.unknown()
|
|
199
|
-
.optional()
|
|
200
|
-
.describe('Validate this raw document instead of reading `scenePath`.'),
|
|
201
|
-
})
|
|
202
|
-
.describe('Exactly one of scenePath/scene must be given.');
|
|
203
|
-
|
|
204
|
-
const ProjectSceneValidateResult = z
|
|
205
|
-
.object({ valid: z.boolean(), issues: z.array(z.string()) })
|
|
206
|
-
.describe('Structured validation result — never throws for a malformed scene.');
|
|
207
|
-
|
|
208
|
-
export const projectSceneValidate = defineTool({
|
|
209
|
-
name: 'project.scene.validate',
|
|
210
|
-
summary: 'Validate a scene file or raw scene document against the real scene schema.',
|
|
211
|
-
description:
|
|
212
|
-
'Reuses parseSceneFile/SceneParseError. Never throws for a malformed document — failures come back as structured issues.',
|
|
213
|
-
input: ProjectSceneValidateInput,
|
|
214
|
-
result: ProjectSceneValidateResult,
|
|
215
|
-
errors: [
|
|
216
|
-
NO_PROJECT_ROOT_ERROR,
|
|
217
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
218
|
-
{
|
|
219
|
-
code: 'BAD_INPUT',
|
|
220
|
-
summary: 'Neither scenePath nor scene was given.',
|
|
221
|
-
data: z.object({}),
|
|
222
|
-
},
|
|
223
|
-
],
|
|
224
|
-
requires: { project: true },
|
|
225
|
-
host: 'node',
|
|
226
|
-
mutates: false,
|
|
227
|
-
supportsDryRun: false,
|
|
228
|
-
permission: {
|
|
229
|
-
risk: 'read',
|
|
230
|
-
summary: 'Reads a scene file (or validates a given document); no writes.',
|
|
231
|
-
},
|
|
232
|
-
async impl(input, ctx) {
|
|
233
|
-
let json: unknown;
|
|
234
|
-
if (input.scene !== undefined) {
|
|
235
|
-
json = input.scene;
|
|
236
|
-
} else if (input.scenePath !== undefined) {
|
|
237
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
238
|
-
const absPath = resolveProjectPath(projectRoot, input.scenePath);
|
|
239
|
-
if (!existsSync(absPath)) {
|
|
240
|
-
return { valid: false, issues: [`File not found: ${absPath}`] };
|
|
241
|
-
}
|
|
242
|
-
json = JSON.parse(readFileWithHash(absPath).raw);
|
|
243
|
-
} else {
|
|
244
|
-
throw new ToolError('BAD_INPUT', 'One of scenePath or scene must be given.', {});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
parseSceneFile(json, input.scenePath);
|
|
249
|
-
return { valid: true, issues: [] };
|
|
250
|
-
} catch (err) {
|
|
251
|
-
if (err instanceof SceneParseError) return { valid: false, issues: formatIssues(err.issues) };
|
|
252
|
-
return { valid: false, issues: [err instanceof Error ? err.message : String(err)] };
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// project.scene.diff — preview only, never writes (mutates:false).
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
const ProjectSceneDiffInput = z
|
|
262
|
-
.object({
|
|
263
|
-
scenePath: z.string().describe('Project-relative path to the base .vscn.json scene file.'),
|
|
264
|
-
patch: SceneDiffSchema,
|
|
265
|
-
})
|
|
266
|
-
.describe('Preview applying `patch` to the scene at scenePath — computed only, never written.');
|
|
267
|
-
|
|
268
|
-
const ProjectSceneDiffResult = z
|
|
269
|
-
.object({
|
|
270
|
-
wouldBeValid: z.boolean(),
|
|
271
|
-
issues: z.array(z.string()),
|
|
272
|
-
opCounts: z.object({
|
|
273
|
-
add: z.number(),
|
|
274
|
-
remove: z.number(),
|
|
275
|
-
update: z.number(),
|
|
276
|
-
move: z.number(),
|
|
277
|
-
}),
|
|
278
|
-
entitiesTouched: z.array(z.string()),
|
|
279
|
-
})
|
|
280
|
-
.describe('What applying this patch would do — no write, ever.');
|
|
281
|
-
|
|
282
|
-
export const projectSceneDiff = defineTool({
|
|
283
|
-
name: 'project.scene.diff',
|
|
284
|
-
summary: 'Preview a SceneDiff patch against a scene file without writing anything.',
|
|
285
|
-
description:
|
|
286
|
-
'Reuses applyDiff in memory + re-validates the result via parseSceneFile — identical logic to project.scene.apply, minus the write.',
|
|
287
|
-
input: ProjectSceneDiffInput,
|
|
288
|
-
result: ProjectSceneDiffResult,
|
|
289
|
-
errors: [
|
|
290
|
-
NO_PROJECT_ROOT_ERROR,
|
|
291
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
292
|
-
FILE_NOT_FOUND_ERROR,
|
|
293
|
-
SCENE_INVALID_ERROR,
|
|
294
|
-
],
|
|
295
|
-
requires: { project: true },
|
|
296
|
-
host: 'node',
|
|
297
|
-
mutates: false,
|
|
298
|
-
supportsDryRun: false,
|
|
299
|
-
permission: {
|
|
300
|
-
risk: 'read',
|
|
301
|
-
summary: 'Reads a scene file; computes a hypothetical result; never writes.',
|
|
302
|
-
},
|
|
303
|
-
async impl(input, ctx) {
|
|
304
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
305
|
-
const { scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
306
|
-
|
|
307
|
-
const opCounts = { add: 0, remove: 0, update: 0, move: 0 };
|
|
308
|
-
const entitiesTouched: string[] = [];
|
|
309
|
-
for (const op of input.patch.ops) {
|
|
310
|
-
opCounts[op.type]++;
|
|
311
|
-
entitiesTouched.push(op.type === 'add' ? (op.entity.id ?? '(auto-generated)') : op.id);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
const result = applyDiffOrThrow(scene, input.patch);
|
|
316
|
-
parseSceneFile(result, input.scenePath);
|
|
317
|
-
return { wouldBeValid: true, issues: [], opCounts, entitiesTouched };
|
|
318
|
-
} catch (err) {
|
|
319
|
-
const message =
|
|
320
|
-
err instanceof ToolError
|
|
321
|
-
? err.message
|
|
322
|
-
: err instanceof SceneParseError
|
|
323
|
-
? err.message
|
|
324
|
-
: err instanceof Error
|
|
325
|
-
? err.message
|
|
326
|
-
: String(err);
|
|
327
|
-
return { wouldBeValid: false, issues: [message], opCounts, entitiesTouched };
|
|
328
|
-
}
|
|
329
|
-
},
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// ---------------------------------------------------------------------------
|
|
333
|
-
// project.scene.apply — the real (or dry-run) write.
|
|
334
|
-
// ---------------------------------------------------------------------------
|
|
335
|
-
|
|
336
|
-
const ProjectSceneApplyInput = z
|
|
337
|
-
.object({
|
|
338
|
-
scenePath: z.string().describe('Project-relative path to the .vscn.json scene file to modify.'),
|
|
339
|
-
patch: SceneDiffSchema,
|
|
340
|
-
dryRun: DryRunField,
|
|
341
|
-
baseHash: BaseHashField,
|
|
342
|
-
})
|
|
343
|
-
.describe('Apply a SceneDiff patch to a scene file.');
|
|
344
|
-
|
|
345
|
-
const ProjectSceneApplyResult = mutationResultSchema(
|
|
346
|
-
z
|
|
347
|
-
.object({ scene: z.unknown() })
|
|
348
|
-
.describe('The scene document after the patch (or that WOULD result).'),
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
export const projectSceneApply = defineTool({
|
|
352
|
-
name: 'project.scene.apply',
|
|
353
|
-
summary: 'Apply a SceneDiff patch to a scene file — dry-run capable, conflict-checked.',
|
|
354
|
-
description:
|
|
355
|
-
'Reuses applyDiff (@vgai/engine/scene/scene-apply) exactly as `vgai apply-diff` does: re-' +
|
|
356
|
-
'validates the result via parseSceneFile before ever writing, refuses a stale write ' +
|
|
357
|
-
'(CONFLICT) when baseHash no longer matches, and never writes when dryRun is true.',
|
|
358
|
-
input: ProjectSceneApplyInput,
|
|
359
|
-
result: ProjectSceneApplyResult,
|
|
360
|
-
errors: [
|
|
361
|
-
NO_PROJECT_ROOT_ERROR,
|
|
362
|
-
PATH_OUTSIDE_PROJECT_ERROR,
|
|
363
|
-
FILE_NOT_FOUND_ERROR,
|
|
364
|
-
SCENE_INVALID_ERROR,
|
|
365
|
-
APPLY_DIFF_FAILED_ERROR,
|
|
366
|
-
CONFLICT_ERROR,
|
|
367
|
-
ASSET_REF_INVALID_ERROR,
|
|
368
|
-
],
|
|
369
|
-
requires: { project: true },
|
|
370
|
-
host: 'node',
|
|
371
|
-
mutates: true,
|
|
372
|
-
supportsDryRun: true,
|
|
373
|
-
permission: { risk: 'write', summary: 'Writes the target scene file.' },
|
|
374
|
-
async impl(input, ctx) {
|
|
375
|
-
const projectRoot = requireProjectRoot(ctx);
|
|
376
|
-
const { absPath, hash, scene } = readAndParseScene(projectRoot, input.scenePath);
|
|
377
|
-
checkNotStale(absPath, hash, input.baseHash);
|
|
378
|
-
|
|
379
|
-
const result = applyDiffOrThrow(scene, input.patch);
|
|
380
|
-
try {
|
|
381
|
-
parseSceneFile(result, input.scenePath);
|
|
382
|
-
} catch (err) {
|
|
383
|
-
if (err instanceof SceneParseError) {
|
|
384
|
-
throw new ToolError(
|
|
385
|
-
'SCENE_INVALID',
|
|
386
|
-
`applyDiff produced a scene that fails validation: ${err.message}`,
|
|
387
|
-
{ issues: formatIssues(err.issues) },
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
throw err;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Shared asset-ref integrity check (B6 / B2 #20) — refuse BEFORE any
|
|
394
|
-
// write (dry-run or real) exactly like `vgai apply-diff`.
|
|
395
|
-
const refErrors = dereferenceNewAssetRefs(scene, result, absPath);
|
|
396
|
-
if (refErrors.length > 0) {
|
|
397
|
-
throw new ToolError(
|
|
398
|
-
'ASSET_REF_INVALID',
|
|
399
|
-
`The patch introduces ${refErrors.length} broken asset reference(s): ` +
|
|
400
|
-
refErrors.map((e) => `${e.field} -> "${e.path}": ${e.reason}`).join('; '),
|
|
401
|
-
{ errors: refErrors },
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const dryRun = input.dryRun ?? false;
|
|
406
|
-
if (!dryRun) {
|
|
407
|
-
writeFileAtomic(absPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return {
|
|
411
|
-
dryRun,
|
|
412
|
-
written: !dryRun,
|
|
413
|
-
filesChanged: [absPath],
|
|
414
|
-
before: { scene },
|
|
415
|
-
after: { scene: result },
|
|
416
|
-
};
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
/** Register every B2 `project.scene.*` operation onto `registry`. */
|
|
421
|
-
export function registerSceneOperations(registry: ToolRegistry): void {
|
|
422
|
-
registry.register(projectSceneRead);
|
|
423
|
-
registry.register(projectSceneValidate);
|
|
424
|
-
registry.register(projectSceneDiff);
|
|
425
|
-
registry.register(projectSceneApply);
|
|
426
|
-
}
|