@hraness/slopcamera 3.2.7 → 3.3.1
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.md +1 -7
- package/PRIVACY.md +16 -7
- package/README.md +65 -20
- package/apps/desktop/README.md +14 -130
- package/apps/desktop/application/context.ts +6 -9
- package/apps/desktop/application/default-registry.ts +26 -10
- package/apps/desktop/application/operation.ts +14 -5
- package/apps/desktop/application/operations/index.ts +4 -1
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +528 -97
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +31 -2
- package/apps/desktop/cli/commands.ts +1215 -70
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +183 -23
- package/apps/desktop/cli/html-scene.ts +139 -28
- package/apps/desktop/cli/io.ts +2 -1
- package/apps/desktop/cli/main.ts +26 -50
- package/apps/desktop/cli/menubar.ts +244 -0
- package/apps/desktop/cli/music-analysis-service.ts +61 -0
- package/apps/desktop/cli/portable-surface.ts +18 -5
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +268 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/application-node-planner.ts +0 -47
- package/apps/desktop/code/host-source-layout.ts +46 -0
- package/apps/desktop/code/public.ts +0 -1
- package/apps/desktop/code/runtime-identity.ts +50 -8
- package/apps/desktop/code/semantic-builder.ts +24 -43
- package/apps/desktop/code/source-bundle.ts +23 -6
- package/apps/desktop/code/source-typecheck.ts +131 -43
- package/apps/desktop/code/worker-client.ts +14 -2
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/music-analysis.ts +124 -1
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +737 -291
- package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
- package/apps/desktop/html-overlay/index.ts +9 -0
- package/apps/desktop/html-overlay/runtime.ts +2 -0
- package/apps/desktop/html-overlay/scene.ts +29 -0
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +32 -434
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-cgpkyjqa.js +3 -0
- package/dist/index-f0ptv8dm.js +7 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index.js +1 -240
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +1 -1
- package/package.json +11 -24
- package/skills/slopcamera/SKILL.md +14 -1
- package/skills/slopcamera/references/diagrams.md +5 -14
- package/skills/slopcamera/references/directed-scenes.md +22 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +2 -2
- package/skills/slopcamera/references/music-video.md +23 -3
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/scene-building.md +32 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/support.md +25 -0
- package/skills/slopcamera/references/web-media-excerpts.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +250 -102
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -23
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1642 -130
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +233 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +109 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +618 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/support-completion.ts +16 -0
- package/src/support.ts +42 -0
- package/src/version.ts +1 -1
- package/apps/desktop/application/operations/recording/index.ts +0 -5
- package/apps/desktop/application/operations/recording/pause.ts +0 -34
- package/apps/desktop/application/operations/recording/resume.ts +0 -34
- package/apps/desktop/application/operations/recording/shared.ts +0 -199
- package/apps/desktop/application/operations/recording/start.ts +0 -169
- package/apps/desktop/application/operations/recording/stop.ts +0 -34
- package/apps/desktop/capture/protocol.ts +0 -689
- package/apps/desktop/cli/capture-bundle.ts +0 -1117
- package/apps/desktop/cli/recording-controller.ts +0 -1567
- package/apps/desktop/cli/recording-daemon.ts +0 -931
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-mcy8z0br.js +0 -192
- package/dist/index-p63wavx0.js +0 -2029
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-rvghzt7b.js +0 -2013
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-z7239b4h.js +0 -2363
- package/dist/skill-install-dh0ntqba.js +0 -10
- package/src/desktop.ts +0 -253
package/src/mcp/tools.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { rename, rm, writeFile } from "node:fs/promises"
|
|
2
2
|
import { dirname, join } from "node:path"
|
|
3
|
+
import { SlopcameraCodeError } from "../code/errors.js"
|
|
3
4
|
import { SlopcameraCloudError } from "../cloud-errors.js"
|
|
4
5
|
import {
|
|
5
6
|
generateSlopcameraImageFile,
|
|
6
7
|
type SlopcameraGenerateDependencies,
|
|
7
8
|
} from "../generate.js"
|
|
9
|
+
import { generateSlopcameraImageGallery } from "../image-gallery.js"
|
|
8
10
|
import { builtInIcons } from "../icons.js"
|
|
9
11
|
import { lintDiagram } from "../lint.js"
|
|
10
12
|
import {
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
searchSlopcameraOperations,
|
|
15
17
|
withSlopcameraOperationHostAdmission,
|
|
16
18
|
type CheckSlopcameraOperationInput,
|
|
19
|
+
type GallerySlopcameraOperationInput,
|
|
17
20
|
type GenerateSlopcameraOperationInput,
|
|
18
21
|
type SlopcameraOperationCode,
|
|
19
22
|
type RenderSlopcameraOperationInput,
|
|
@@ -21,11 +24,61 @@ import {
|
|
|
21
24
|
} from "../operations.js"
|
|
22
25
|
import {
|
|
23
26
|
createDefaultHostResourceCoordinator,
|
|
27
|
+
HostResourceError,
|
|
24
28
|
type HostResourceCoordinator,
|
|
25
29
|
type HostResourceLease,
|
|
26
30
|
} from "../host-resources.js"
|
|
27
31
|
import { DiagramValidationError, parseDiagramSpec } from "../parse.js"
|
|
28
32
|
import { renderPng, renderSvg } from "../render.js"
|
|
33
|
+
import {
|
|
34
|
+
auditSpatialScene,
|
|
35
|
+
normalizeSpatialAuditAssetBounds,
|
|
36
|
+
SPATIAL_AUDIT_LIMITS,
|
|
37
|
+
type SpatialAuditEntity,
|
|
38
|
+
} from "../spatial-scene/audit.js"
|
|
39
|
+
import {
|
|
40
|
+
checkSpatialBehavior,
|
|
41
|
+
} from "../spatial-scene/behavior.js"
|
|
42
|
+
import {
|
|
43
|
+
auditSpatialBehaviorTrace,
|
|
44
|
+
} from "../spatial-scene/behavior-audit.js"
|
|
45
|
+
import {
|
|
46
|
+
spatialBehaviorFnSignatures,
|
|
47
|
+
} from "../spatial-scene/behavior-fns.js"
|
|
48
|
+
import {
|
|
49
|
+
SPATIAL_SCENE_LIMITS,
|
|
50
|
+
type SpatialSceneV1,
|
|
51
|
+
} from "../spatial-scene/contracts.js"
|
|
52
|
+
import {
|
|
53
|
+
checkSpatialDirection,
|
|
54
|
+
compileSpatialDirection,
|
|
55
|
+
} from "../spatial-scene/direction-compile.js"
|
|
56
|
+
import {
|
|
57
|
+
createSpatialEvaluationContext,
|
|
58
|
+
evaluateSpatialScene,
|
|
59
|
+
evaluateSpatialSceneInContext,
|
|
60
|
+
} from "../spatial-scene/evaluate.js"
|
|
61
|
+
import {
|
|
62
|
+
planSpatialDirectionGallery,
|
|
63
|
+
SpatialGalleryAxisSchema,
|
|
64
|
+
} from "../spatial-scene/gallery.js"
|
|
65
|
+
import {
|
|
66
|
+
parseSpatialScene,
|
|
67
|
+
parseSpatialValue,
|
|
68
|
+
SpatialSceneError,
|
|
69
|
+
spatialValueSha256,
|
|
70
|
+
} from "../spatial-scene/identity.js"
|
|
71
|
+
import { inspectSpatialScene } from "../spatial-scene/inspect.js"
|
|
72
|
+
import { diffSpatialScenes } from "../spatial-scene/patch.js"
|
|
73
|
+
import {
|
|
74
|
+
checkSpatialRenderEffects,
|
|
75
|
+
planSpatialRenderEffects,
|
|
76
|
+
} from "../spatial-scene/render-effects.js"
|
|
77
|
+
import {
|
|
78
|
+
auditSpatialTemporalEvidence,
|
|
79
|
+
SPATIAL_TEMPORAL_AUDIT_LIMITS,
|
|
80
|
+
SpatialTemporalContactsFileSchema,
|
|
81
|
+
} from "../spatial-scene/temporal-audit.js"
|
|
29
82
|
import { serializeTldr } from "../tldr.js"
|
|
30
83
|
import type {
|
|
31
84
|
DiagramConfig,
|
|
@@ -53,6 +106,9 @@ export const mcpMaximumRenderedPixels = 16_777_216
|
|
|
53
106
|
export const mcpMaximumShapes = 64
|
|
54
107
|
export const mcpMaximumEdges = 128
|
|
55
108
|
export const mcpMaximumReturnedFindings = 40
|
|
109
|
+
export const mcpMaximumReturnedEntities = 256
|
|
110
|
+
export const mcpMaximumReturnedAuditSamples = 8_192
|
|
111
|
+
export const mcpMaximumReturnedDiffEntries = 1_024
|
|
56
112
|
|
|
57
113
|
const defaultScale = 2
|
|
58
114
|
const maximumShapeIdsPerFinding = 12
|
|
@@ -69,6 +125,30 @@ const findingSchema = {
|
|
|
69
125
|
},
|
|
70
126
|
} as const
|
|
71
127
|
|
|
128
|
+
const scenePathSchema = {
|
|
129
|
+
type: "string",
|
|
130
|
+
description:
|
|
131
|
+
"Root-relative path to a spatial scene JSON source (1 MiB maximum).",
|
|
132
|
+
} as const
|
|
133
|
+
|
|
134
|
+
const sceneCameraIdSchema = {
|
|
135
|
+
type: "string",
|
|
136
|
+
maxLength: 128,
|
|
137
|
+
pattern: "^camera_[a-zA-Z0-9][a-zA-Z0-9_-]*$",
|
|
138
|
+
description: "Identifier of a camera declared by the scene (camera_…).",
|
|
139
|
+
} as const
|
|
140
|
+
|
|
141
|
+
const entityTruncationSummarySchema = {
|
|
142
|
+
type: "object",
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
required: ["entityCount", "returnedEntityCount", "entitiesTruncated"],
|
|
145
|
+
properties: {
|
|
146
|
+
entityCount: { type: "integer", minimum: 0 },
|
|
147
|
+
returnedEntityCount: { type: "integer", minimum: 0 },
|
|
148
|
+
entitiesTruncated: { type: "boolean" },
|
|
149
|
+
},
|
|
150
|
+
} as const
|
|
151
|
+
|
|
72
152
|
function deepFreeze<T>(value: T): T {
|
|
73
153
|
if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
|
|
74
154
|
return value
|
|
@@ -299,180 +379,1063 @@ export const slopcameraMcpTools: readonly McpToolDefinition[] = deepFreeze([
|
|
|
299
379
|
openWorldHint: true,
|
|
300
380
|
},
|
|
301
381
|
},
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
382
|
+
{
|
|
383
|
+
name: "check_scene",
|
|
384
|
+
title: "Check scene",
|
|
385
|
+
description:
|
|
386
|
+
"Parse and validate one root-relative Slopcamera spatial scene JSON source without changing files.",
|
|
387
|
+
inputSchema: {
|
|
388
|
+
type: "object",
|
|
389
|
+
additionalProperties: false,
|
|
390
|
+
required: ["path"],
|
|
391
|
+
properties: { path: scenePathSchema },
|
|
392
|
+
},
|
|
393
|
+
outputSchema: {
|
|
394
|
+
type: "object",
|
|
395
|
+
additionalProperties: false,
|
|
396
|
+
required: ["ok", "source", "sceneId", "sceneSha256", "summary"],
|
|
397
|
+
properties: {
|
|
398
|
+
ok: { const: true },
|
|
399
|
+
source: { type: "string" },
|
|
400
|
+
sceneId: { type: "string" },
|
|
401
|
+
sceneSha256: { type: "string" },
|
|
402
|
+
summary: {
|
|
403
|
+
type: "object",
|
|
404
|
+
additionalProperties: false,
|
|
405
|
+
required: [
|
|
406
|
+
"durationUs",
|
|
407
|
+
"entityCount",
|
|
408
|
+
"cameraCount",
|
|
409
|
+
"assetCount",
|
|
410
|
+
"animationCount",
|
|
411
|
+
"generatorCount",
|
|
412
|
+
"overrideCount",
|
|
413
|
+
],
|
|
414
|
+
properties: {
|
|
415
|
+
durationUs: { type: "integer", minimum: 0 },
|
|
416
|
+
entityCount: { type: "integer", minimum: 0 },
|
|
417
|
+
cameraCount: { type: "integer", minimum: 0 },
|
|
418
|
+
assetCount: { type: "integer", minimum: 0 },
|
|
419
|
+
animationCount: { type: "integer", minimum: 0 },
|
|
420
|
+
generatorCount: { type: "integer", minimum: 0 },
|
|
421
|
+
overrideCount: { type: "integer", minimum: 0 },
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
annotations: {
|
|
427
|
+
title: "Check scene",
|
|
428
|
+
readOnlyHint: true,
|
|
429
|
+
destructiveHint: false,
|
|
430
|
+
idempotentHint: true,
|
|
431
|
+
openWorldHint: false,
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: "inspect_scene",
|
|
436
|
+
title: "Inspect scene",
|
|
437
|
+
description:
|
|
438
|
+
"Snapshot one root-relative spatial scene's entities, editable controls, placements, assets, and generators without changing files.",
|
|
439
|
+
inputSchema: {
|
|
440
|
+
type: "object",
|
|
441
|
+
additionalProperties: false,
|
|
442
|
+
required: ["path"],
|
|
443
|
+
properties: { path: scenePathSchema },
|
|
444
|
+
},
|
|
445
|
+
outputSchema: {
|
|
446
|
+
type: "object",
|
|
447
|
+
additionalProperties: false,
|
|
448
|
+
required: ["ok", "source", "inspection", "summary"],
|
|
449
|
+
properties: {
|
|
450
|
+
ok: { const: true },
|
|
451
|
+
source: { type: "string" },
|
|
452
|
+
inspection: { type: "object" },
|
|
453
|
+
summary: entityTruncationSummarySchema,
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
annotations: {
|
|
457
|
+
title: "Inspect scene",
|
|
458
|
+
readOnlyHint: true,
|
|
459
|
+
destructiveHint: false,
|
|
460
|
+
idempotentHint: true,
|
|
461
|
+
openWorldHint: false,
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "audit_scene",
|
|
466
|
+
title: "Audit scene",
|
|
467
|
+
description:
|
|
468
|
+
"Sample one root-relative spatial scene against a camera and report per-entity geometric coverage, visibility findings, and bounds gaps. Optional asset_bounds accepts a Record<assetId,{min,max}> map, a slopcamera.spatial-asset-admission document, a {manifest,facts} pair, or an array of those.",
|
|
469
|
+
inputSchema: {
|
|
470
|
+
type: "object",
|
|
471
|
+
additionalProperties: false,
|
|
472
|
+
required: ["path", "camera_id"],
|
|
473
|
+
properties: {
|
|
474
|
+
path: scenePathSchema,
|
|
475
|
+
camera_id: sceneCameraIdSchema,
|
|
476
|
+
times_us: {
|
|
477
|
+
type: "array",
|
|
478
|
+
items: {
|
|
479
|
+
type: "integer",
|
|
480
|
+
minimum: 0,
|
|
481
|
+
maximum: SPATIAL_SCENE_LIMITS.durationUs,
|
|
482
|
+
},
|
|
483
|
+
minItems: 1,
|
|
484
|
+
maxItems: SPATIAL_AUDIT_LIMITS.samples,
|
|
485
|
+
description:
|
|
486
|
+
"Optional sample times in microseconds. Defaults to evenly spaced coverage of the scene duration.",
|
|
487
|
+
},
|
|
488
|
+
asset_bounds: {
|
|
489
|
+
description:
|
|
490
|
+
"Optional decoded scene-space asset bounds: a Record<assetId,{min,max}> map, a slopcamera.spatial-asset-admission document, a {manifest,facts} pair, or an array of those documents.",
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
outputSchema: {
|
|
495
|
+
type: "object",
|
|
496
|
+
additionalProperties: false,
|
|
497
|
+
required: ["ok", "source", "report", "summary"],
|
|
498
|
+
properties: {
|
|
499
|
+
ok: { const: true },
|
|
500
|
+
source: { type: "string" },
|
|
501
|
+
report: { type: "object" },
|
|
502
|
+
summary: {
|
|
503
|
+
type: "object",
|
|
504
|
+
additionalProperties: false,
|
|
505
|
+
required: [
|
|
506
|
+
"findingCount",
|
|
507
|
+
"returnedFindingCount",
|
|
508
|
+
"findingsTruncated",
|
|
509
|
+
"entityCount",
|
|
510
|
+
"returnedEntityCount",
|
|
511
|
+
"entitiesTruncated",
|
|
512
|
+
],
|
|
513
|
+
properties: {
|
|
514
|
+
findingCount: { type: "integer", minimum: 0 },
|
|
515
|
+
returnedFindingCount: { type: "integer", minimum: 0 },
|
|
516
|
+
findingsTruncated: { type: "boolean" },
|
|
517
|
+
entityCount: { type: "integer", minimum: 0 },
|
|
518
|
+
returnedEntityCount: { type: "integer", minimum: 0 },
|
|
519
|
+
entitiesTruncated: { type: "boolean" },
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
annotations: {
|
|
525
|
+
title: "Audit scene",
|
|
526
|
+
readOnlyHint: true,
|
|
527
|
+
destructiveHint: false,
|
|
528
|
+
idempotentHint: true,
|
|
529
|
+
openWorldHint: false,
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: "diff_scenes",
|
|
534
|
+
title: "Diff scenes",
|
|
535
|
+
description:
|
|
536
|
+
"Compare two root-relative spatial scene JSON sources and report added, removed, and changed collection entries without changing files.",
|
|
537
|
+
inputSchema: {
|
|
538
|
+
type: "object",
|
|
539
|
+
additionalProperties: false,
|
|
540
|
+
required: ["path", "other"],
|
|
541
|
+
properties: {
|
|
542
|
+
path: scenePathSchema,
|
|
543
|
+
other: {
|
|
544
|
+
type: "string",
|
|
545
|
+
description:
|
|
546
|
+
"Root-relative path to the second spatial scene JSON source (1 MiB maximum).",
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
outputSchema: {
|
|
551
|
+
type: "object",
|
|
552
|
+
additionalProperties: false,
|
|
553
|
+
required: [
|
|
554
|
+
"ok",
|
|
555
|
+
"source",
|
|
556
|
+
"other",
|
|
557
|
+
"sceneSha256",
|
|
558
|
+
"otherSha256",
|
|
559
|
+
"diff",
|
|
560
|
+
"summary",
|
|
561
|
+
],
|
|
562
|
+
properties: {
|
|
563
|
+
ok: { const: true },
|
|
564
|
+
source: { type: "string" },
|
|
565
|
+
other: { type: "string" },
|
|
566
|
+
sceneSha256: { type: "string" },
|
|
567
|
+
otherSha256: { type: "string" },
|
|
568
|
+
diff: { type: "array" },
|
|
569
|
+
summary: {
|
|
570
|
+
type: "object",
|
|
571
|
+
additionalProperties: false,
|
|
572
|
+
required: ["entryCount", "returnedEntryCount", "diffTruncated"],
|
|
573
|
+
properties: {
|
|
574
|
+
entryCount: { type: "integer", minimum: 0 },
|
|
575
|
+
returnedEntryCount: { type: "integer", minimum: 0 },
|
|
576
|
+
diffTruncated: { type: "boolean" },
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
annotations: {
|
|
582
|
+
title: "Diff scenes",
|
|
583
|
+
readOnlyHint: true,
|
|
584
|
+
destructiveHint: false,
|
|
585
|
+
idempotentHint: true,
|
|
586
|
+
openWorldHint: false,
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: "evaluate_scene",
|
|
591
|
+
title: "Evaluate scene",
|
|
592
|
+
description:
|
|
593
|
+
"Evaluate one root-relative spatial scene at one camera and integer microsecond time into an immutable snapshot without changing files.",
|
|
594
|
+
inputSchema: {
|
|
595
|
+
type: "object",
|
|
596
|
+
additionalProperties: false,
|
|
597
|
+
required: ["path", "camera_id", "time_us"],
|
|
598
|
+
properties: {
|
|
599
|
+
path: scenePathSchema,
|
|
600
|
+
camera_id: sceneCameraIdSchema,
|
|
601
|
+
time_us: {
|
|
602
|
+
type: "integer",
|
|
603
|
+
minimum: 0,
|
|
604
|
+
maximum: SPATIAL_SCENE_LIMITS.durationUs,
|
|
605
|
+
description: "Evaluation time in integer microseconds.",
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
outputSchema: {
|
|
610
|
+
type: "object",
|
|
611
|
+
additionalProperties: false,
|
|
612
|
+
required: ["ok", "source", "snapshot", "summary"],
|
|
613
|
+
properties: {
|
|
614
|
+
ok: { const: true },
|
|
615
|
+
source: { type: "string" },
|
|
616
|
+
snapshot: { type: "object" },
|
|
617
|
+
summary: entityTruncationSummarySchema,
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
annotations: {
|
|
621
|
+
title: "Evaluate scene",
|
|
622
|
+
readOnlyHint: true,
|
|
623
|
+
destructiveHint: false,
|
|
624
|
+
idempotentHint: true,
|
|
625
|
+
openWorldHint: false,
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "check_scene_direction",
|
|
630
|
+
title: "Check scene direction",
|
|
631
|
+
description:
|
|
632
|
+
"Validate one authored direction document against one root-relative spatial scene: stale project digests, unresolved entity references, and intervals outside scene duration report as errors without compiling or changing files.",
|
|
633
|
+
inputSchema: {
|
|
634
|
+
type: "object",
|
|
635
|
+
additionalProperties: false,
|
|
636
|
+
required: ["scene", "direction"],
|
|
637
|
+
properties: {
|
|
638
|
+
scene: { ...scenePathSchema, description: "Root-relative path to the spatial scene JSON source (1 MiB maximum)." },
|
|
639
|
+
direction: { type: "string", description: "Root-relative path to the slopcamera.spatial-direction JSON document." },
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
outputSchema: {
|
|
643
|
+
type: "object",
|
|
644
|
+
additionalProperties: false,
|
|
645
|
+
required: ["ok", "source", "report", "summary"],
|
|
646
|
+
properties: {
|
|
647
|
+
ok: { const: true },
|
|
648
|
+
source: { type: "string" },
|
|
649
|
+
report: { type: "object" },
|
|
650
|
+
summary: {
|
|
651
|
+
type: "object",
|
|
652
|
+
additionalProperties: false,
|
|
653
|
+
required: ["findingCount", "returnedFindingCount", "findingsTruncated", "errorCount", "warningCount"],
|
|
654
|
+
properties: {
|
|
655
|
+
findingCount: { type: "integer", minimum: 0 },
|
|
656
|
+
returnedFindingCount: { type: "integer", minimum: 0 },
|
|
657
|
+
findingsTruncated: { type: "boolean" },
|
|
658
|
+
errorCount: { type: "integer", minimum: 0 },
|
|
659
|
+
warningCount: { type: "integer", minimum: 0 },
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
annotations: {
|
|
665
|
+
title: "Check scene direction",
|
|
666
|
+
readOnlyHint: true,
|
|
667
|
+
destructiveHint: false,
|
|
668
|
+
idempotentHint: true,
|
|
669
|
+
openWorldHint: false,
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: "plan_scene_direction",
|
|
674
|
+
title: "Plan scene direction",
|
|
675
|
+
description:
|
|
676
|
+
"Compile one authored direction document into a deterministic, unverified proposal document — performance actions, camera rigs, shots and looks bound to the actual scene digest. Proposals are advisory only; nothing is applied to the scene.",
|
|
677
|
+
inputSchema: {
|
|
678
|
+
type: "object",
|
|
679
|
+
additionalProperties: false,
|
|
680
|
+
required: ["scene", "direction"],
|
|
681
|
+
properties: {
|
|
682
|
+
scene: { ...scenePathSchema, description: "Root-relative path to the spatial scene JSON source (1 MiB maximum)." },
|
|
683
|
+
direction: { type: "string", description: "Root-relative path to the slopcamera.spatial-direction JSON document." },
|
|
684
|
+
camera_id: { ...sceneCameraIdSchema, description: "Optional camera used to resolve coverage; defaults to the sole or first sorted scene camera." },
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
outputSchema: {
|
|
688
|
+
type: "object",
|
|
689
|
+
additionalProperties: false,
|
|
690
|
+
required: ["ok", "source", "compilation", "summary"],
|
|
691
|
+
properties: {
|
|
692
|
+
ok: { const: true },
|
|
693
|
+
source: { type: "string" },
|
|
694
|
+
compilation: { type: "object" },
|
|
695
|
+
summary: {
|
|
696
|
+
type: "object",
|
|
697
|
+
additionalProperties: false,
|
|
698
|
+
required: ["advisoryCount", "unresolvedIntentCount", "proposalCounts"],
|
|
699
|
+
properties: {
|
|
700
|
+
advisoryCount: { type: "integer", minimum: 0 },
|
|
701
|
+
unresolvedIntentCount: { type: "integer", minimum: 0 },
|
|
702
|
+
proposalCounts: {
|
|
703
|
+
type: "object",
|
|
704
|
+
additionalProperties: false,
|
|
705
|
+
required: ["performance", "cameraRigs", "shots", "lookIntents"],
|
|
706
|
+
properties: {
|
|
707
|
+
performance: { type: "integer", minimum: 0 },
|
|
708
|
+
cameraRigs: { type: "integer", minimum: 0 },
|
|
709
|
+
shots: { type: "integer", minimum: 0 },
|
|
710
|
+
lookIntents: { type: "integer", minimum: 0 },
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
annotations: {
|
|
718
|
+
title: "Plan scene direction",
|
|
719
|
+
readOnlyHint: true,
|
|
720
|
+
destructiveHint: false,
|
|
721
|
+
idempotentHint: true,
|
|
722
|
+
openWorldHint: false,
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
name: "plan_scene_gallery",
|
|
727
|
+
title: "Plan scene gallery",
|
|
728
|
+
description:
|
|
729
|
+
"Compile a bounded set of single-axis direction variants into separate content-addressed candidate compilations plus a preview-reel plan. The planner never selects or promotes a candidate.",
|
|
730
|
+
inputSchema: {
|
|
731
|
+
type: "object",
|
|
732
|
+
additionalProperties: false,
|
|
733
|
+
required: ["scene", "direction", "axis"],
|
|
734
|
+
properties: {
|
|
735
|
+
scene: { ...scenePathSchema, description: "Root-relative path to the spatial scene JSON source (1 MiB maximum)." },
|
|
736
|
+
direction: { type: "string", description: "Root-relative path to the slopcamera.spatial-direction JSON document." },
|
|
737
|
+
axis: { type: "string", enum: [...SpatialGalleryAxisSchema.options], description: "Variation axis for the bounded candidate set." },
|
|
738
|
+
camera_id: { ...sceneCameraIdSchema, description: "Optional camera used to resolve coverage." },
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
outputSchema: {
|
|
742
|
+
type: "object",
|
|
743
|
+
additionalProperties: false,
|
|
744
|
+
required: ["ok", "source", "gallery", "summary"],
|
|
745
|
+
properties: {
|
|
746
|
+
ok: { const: true },
|
|
747
|
+
source: { type: "string" },
|
|
748
|
+
gallery: { type: "object" },
|
|
749
|
+
summary: {
|
|
750
|
+
type: "object",
|
|
751
|
+
additionalProperties: false,
|
|
752
|
+
required: ["axis", "candidateCount", "selected"],
|
|
753
|
+
properties: {
|
|
754
|
+
axis: { type: "string" },
|
|
755
|
+
candidateCount: { type: "integer", minimum: 0 },
|
|
756
|
+
selected: { const: false },
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
annotations: {
|
|
762
|
+
title: "Plan scene gallery",
|
|
763
|
+
readOnlyHint: true,
|
|
764
|
+
destructiveHint: false,
|
|
765
|
+
idempotentHint: true,
|
|
766
|
+
openWorldHint: false,
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: "check_scene_effects",
|
|
771
|
+
title: "Check scene effects",
|
|
772
|
+
description:
|
|
773
|
+
"Validate one slopcamera.spatial-render-effects document or {document, documentSha256} binding against one scene: scene-digest bindings, entity and asset closure, and nested plan/particle/simulation digests report as findings before any host work.",
|
|
774
|
+
inputSchema: {
|
|
775
|
+
type: "object",
|
|
776
|
+
additionalProperties: false,
|
|
777
|
+
required: ["scene", "effects"],
|
|
778
|
+
properties: {
|
|
779
|
+
scene: { ...scenePathSchema, description: "Root-relative path to the spatial scene JSON source (1 MiB maximum)." },
|
|
780
|
+
effects: { type: "string", description: "Root-relative path to the render-effects document or binding JSON." },
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
outputSchema: {
|
|
784
|
+
type: "object",
|
|
785
|
+
additionalProperties: false,
|
|
786
|
+
required: ["ok", "source", "report", "summary"],
|
|
787
|
+
properties: {
|
|
788
|
+
ok: { const: true },
|
|
789
|
+
source: { type: "string" },
|
|
790
|
+
report: { type: "object" },
|
|
791
|
+
summary: {
|
|
792
|
+
type: "object",
|
|
793
|
+
additionalProperties: false,
|
|
794
|
+
required: ["findingCount", "returnedFindingCount", "findingsTruncated", "errorCount", "warningCount"],
|
|
795
|
+
properties: {
|
|
796
|
+
findingCount: { type: "integer", minimum: 0 },
|
|
797
|
+
returnedFindingCount: { type: "integer", minimum: 0 },
|
|
798
|
+
findingsTruncated: { type: "boolean" },
|
|
799
|
+
errorCount: { type: "integer", minimum: 0 },
|
|
800
|
+
warningCount: { type: "integer", minimum: 0 },
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
annotations: {
|
|
806
|
+
title: "Check scene effects",
|
|
807
|
+
readOnlyHint: true,
|
|
808
|
+
destructiveHint: false,
|
|
809
|
+
idempotentHint: true,
|
|
810
|
+
openWorldHint: false,
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
name: "plan_scene_effects",
|
|
815
|
+
title: "Plan scene effects",
|
|
816
|
+
description:
|
|
817
|
+
"Assemble a canonical {document, documentSha256} render-effects binding for one scene from a draft JSON naming renderPlan, particleSystems and simulationBakes. Entity and asset closure rejects before the document is formed; the scene digest binds here, never by caller assertion.",
|
|
818
|
+
inputSchema: {
|
|
819
|
+
type: "object",
|
|
820
|
+
additionalProperties: false,
|
|
821
|
+
required: ["scene", "draft"],
|
|
822
|
+
properties: {
|
|
823
|
+
scene: { ...scenePathSchema, description: "Root-relative path to the spatial scene JSON source (1 MiB maximum)." },
|
|
824
|
+
draft: { type: "string", description: "Root-relative path to the effects draft JSON ({renderPlan, particleSystems?, simulationBakes?})." },
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
outputSchema: {
|
|
828
|
+
type: "object",
|
|
829
|
+
additionalProperties: false,
|
|
830
|
+
required: ["ok", "source", "binding", "summary"],
|
|
831
|
+
properties: {
|
|
832
|
+
ok: { const: true },
|
|
833
|
+
source: { type: "string" },
|
|
834
|
+
binding: { type: "object" },
|
|
835
|
+
summary: {
|
|
836
|
+
type: "object",
|
|
837
|
+
additionalProperties: false,
|
|
838
|
+
required: ["documentSha256", "particleSystemCount", "simulationBakeCount"],
|
|
839
|
+
properties: {
|
|
840
|
+
documentSha256: { type: "string" },
|
|
841
|
+
particleSystemCount: { type: "integer", minimum: 0 },
|
|
842
|
+
simulationBakeCount: { type: "integer", minimum: 0 },
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
annotations: {
|
|
848
|
+
title: "Plan scene effects",
|
|
849
|
+
readOnlyHint: true,
|
|
850
|
+
destructiveHint: false,
|
|
851
|
+
idempotentHint: true,
|
|
852
|
+
openWorldHint: false,
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
name: "check_scene_behavior",
|
|
857
|
+
title: "Check scene behavior",
|
|
858
|
+
description:
|
|
859
|
+
"Validate one slopcamera.spatial-behavior document against one scene: bake-safe organism profile (input/const/fn/repeat/each/organism cells only), manifest-closure digests, entity and scene-digest bindings, channel declarations, fn-catalog resolution, wiring, and budget bounds report as findings before any host work.",
|
|
860
|
+
inputSchema: {
|
|
861
|
+
type: "object",
|
|
862
|
+
additionalProperties: false,
|
|
863
|
+
required: ["scene", "behavior"],
|
|
864
|
+
properties: {
|
|
865
|
+
scene: { ...scenePathSchema, description: "Root-relative path to the spatial scene JSON source (1 MiB maximum)." },
|
|
866
|
+
behavior: { type: "string", description: "Root-relative path to the spatial behavior document JSON." },
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
outputSchema: {
|
|
870
|
+
type: "object",
|
|
871
|
+
additionalProperties: false,
|
|
872
|
+
required: ["ok", "source", "report", "summary"],
|
|
873
|
+
properties: {
|
|
874
|
+
ok: { const: true },
|
|
875
|
+
source: { type: "string" },
|
|
876
|
+
report: { type: "object" },
|
|
877
|
+
summary: {
|
|
878
|
+
type: "object",
|
|
879
|
+
additionalProperties: false,
|
|
880
|
+
required: ["findingCount", "returnedFindingCount", "findingsTruncated", "errorCount", "warningCount"],
|
|
881
|
+
properties: {
|
|
882
|
+
findingCount: { type: "integer", minimum: 0 },
|
|
883
|
+
returnedFindingCount: { type: "integer", minimum: 0 },
|
|
884
|
+
findingsTruncated: { type: "boolean" },
|
|
885
|
+
errorCount: { type: "integer", minimum: 0 },
|
|
886
|
+
warningCount: { type: "integer", minimum: 0 },
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
annotations: {
|
|
892
|
+
title: "Check scene behavior",
|
|
893
|
+
readOnlyHint: true,
|
|
894
|
+
destructiveHint: false,
|
|
895
|
+
idempotentHint: true,
|
|
896
|
+
openWorldHint: false,
|
|
897
|
+
},
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
name: "audit_scene_behavior",
|
|
901
|
+
title: "Audit scene behavior trace",
|
|
902
|
+
description:
|
|
903
|
+
"Analyze a baked behavior trace for pathological patterns: state thrash (rapid transitions), exact periodicity (trivially cyclic channels), dead channels (constant-valued), and unreachable states (declared in transitions but never visited). Advisory findings for gallery review — never selects or promotes.",
|
|
904
|
+
inputSchema: {
|
|
905
|
+
type: "object",
|
|
906
|
+
additionalProperties: false,
|
|
907
|
+
required: ["bake"],
|
|
908
|
+
properties: {
|
|
909
|
+
bake: { type: "string", description: "Root-relative path to the behavior bake JSON (slopcamera.spatial-behavior-bake)." },
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
outputSchema: {
|
|
913
|
+
type: "object",
|
|
914
|
+
additionalProperties: false,
|
|
915
|
+
required: ["ok", "source", "report", "summary"],
|
|
916
|
+
properties: {
|
|
917
|
+
ok: { const: true },
|
|
918
|
+
source: { type: "string" },
|
|
919
|
+
report: { type: "object" },
|
|
920
|
+
summary: {
|
|
921
|
+
type: "object",
|
|
922
|
+
additionalProperties: false,
|
|
923
|
+
required: ["findingCount", "channelCount", "emittedCount"],
|
|
924
|
+
properties: {
|
|
925
|
+
findingCount: { type: "integer", minimum: 0 },
|
|
926
|
+
channelCount: { type: "integer", minimum: 0 },
|
|
927
|
+
emittedCount: { type: "integer", minimum: 0 },
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
annotations: {
|
|
933
|
+
title: "Audit scene behavior trace",
|
|
934
|
+
readOnlyHint: true,
|
|
935
|
+
destructiveHint: false,
|
|
936
|
+
idempotentHint: true,
|
|
937
|
+
openWorldHint: false,
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
name: "audit_scene_temporal",
|
|
942
|
+
title: "Audit scene temporal evidence",
|
|
943
|
+
description:
|
|
944
|
+
"Evaluate one root-relative spatial scene at bounded sample times under one camera and report deterministic temporal findings: visibility flicker, transform discontinuity, foot-slide against declared contact windows, camera acceleration/jerk/angular velocity, exposure and focus jumps, and resource spikes.",
|
|
945
|
+
inputSchema: {
|
|
946
|
+
type: "object",
|
|
947
|
+
additionalProperties: false,
|
|
948
|
+
required: ["path", "camera_id"],
|
|
949
|
+
properties: {
|
|
950
|
+
path: scenePathSchema,
|
|
951
|
+
camera_id: sceneCameraIdSchema,
|
|
952
|
+
times_us: {
|
|
953
|
+
type: "array",
|
|
954
|
+
items: { type: "integer", minimum: 0, maximum: SPATIAL_SCENE_LIMITS.durationUs },
|
|
955
|
+
minItems: 2,
|
|
956
|
+
maxItems: SPATIAL_TEMPORAL_AUDIT_LIMITS.samples,
|
|
957
|
+
description: "Optional sample times in microseconds (2–256). Defaults to evenly spaced coverage of the scene duration.",
|
|
958
|
+
},
|
|
959
|
+
contacts: { type: "string", description: "Optional root-relative path to a {contacts: [...]} ground-contact evidence JSON." },
|
|
960
|
+
cut_before_us: {
|
|
961
|
+
type: "array",
|
|
962
|
+
items: { type: "integer", minimum: 0, maximum: SPATIAL_SCENE_LIMITS.durationUs },
|
|
963
|
+
maxItems: SPATIAL_TEMPORAL_AUDIT_LIMITS.cutBoundaries,
|
|
964
|
+
description: "Optional intentional cut boundaries; discontinuities exactly at a cut are not findings.",
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
outputSchema: {
|
|
969
|
+
type: "object",
|
|
970
|
+
additionalProperties: false,
|
|
971
|
+
required: ["ok", "source", "report", "summary"],
|
|
972
|
+
properties: {
|
|
973
|
+
ok: { const: true },
|
|
974
|
+
source: { type: "string" },
|
|
975
|
+
report: { type: "object" },
|
|
976
|
+
summary: {
|
|
977
|
+
type: "object",
|
|
978
|
+
additionalProperties: false,
|
|
979
|
+
required: ["findingCount", "returnedFindingCount", "findingsTruncated", "sampleCount"],
|
|
980
|
+
properties: {
|
|
981
|
+
findingCount: { type: "integer", minimum: 0 },
|
|
982
|
+
returnedFindingCount: { type: "integer", minimum: 0 },
|
|
983
|
+
findingsTruncated: { type: "boolean" },
|
|
984
|
+
sampleCount: { type: "integer", minimum: 2 },
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
annotations: {
|
|
990
|
+
title: "Audit scene temporal evidence",
|
|
991
|
+
readOnlyHint: true,
|
|
992
|
+
destructiveHint: false,
|
|
993
|
+
idempotentHint: true,
|
|
994
|
+
openWorldHint: false,
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
])
|
|
998
|
+
|
|
999
|
+
class ToolFailure extends Error {
|
|
1000
|
+
readonly code: string
|
|
1001
|
+
readonly issues?: readonly string[]
|
|
1002
|
+
|
|
1003
|
+
constructor(code: string, message: string, issues?: readonly string[]) {
|
|
1004
|
+
super(message)
|
|
1005
|
+
this.name = "ToolFailure"
|
|
1006
|
+
this.code = code
|
|
1007
|
+
if (issues !== undefined) this.issues = issues
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
interface ParsedCheckArguments {
|
|
1012
|
+
readonly path: string
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
interface ParsedRenderArguments extends ParsedCheckArguments {
|
|
1016
|
+
readonly outDirectory?: string
|
|
1017
|
+
readonly scale: number
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
interface ParsedSearchArguments {
|
|
1021
|
+
readonly query: string
|
|
1022
|
+
readonly limit: number
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
interface ParsedExecuteArguments {
|
|
1026
|
+
readonly operation: SlopcameraOperationCode
|
|
1027
|
+
readonly input: unknown
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
interface ParsedSceneSourceArguments {
|
|
1031
|
+
readonly path: string
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
interface ParsedAuditSceneArguments extends ParsedSceneSourceArguments {
|
|
1035
|
+
readonly cameraId: string
|
|
1036
|
+
readonly timesUs?: readonly number[]
|
|
1037
|
+
readonly assetBounds?: unknown
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
interface ParsedDiffScenesArguments extends ParsedSceneSourceArguments {
|
|
1041
|
+
readonly other: string
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
interface ParsedEvaluateSceneArguments extends ParsedSceneSourceArguments {
|
|
1045
|
+
readonly cameraId: string
|
|
1046
|
+
readonly timeUs: number
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
interface ParsedSceneDirectionArguments {
|
|
1050
|
+
readonly scene: string
|
|
1051
|
+
readonly direction: string
|
|
1052
|
+
readonly cameraId?: string
|
|
1053
|
+
readonly axis?: string
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
interface ParsedSceneEffectsArguments {
|
|
1057
|
+
readonly scene: string
|
|
1058
|
+
readonly effects: string
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
interface ParsedSceneBehaviorArguments {
|
|
1062
|
+
readonly scene: string
|
|
1063
|
+
readonly behavior: string
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
interface ParsedSceneBehaviorAuditArguments {
|
|
1067
|
+
readonly bake: string
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
interface ParsedSceneTemporalArguments extends ParsedSceneSourceArguments {
|
|
1071
|
+
readonly cameraId: string
|
|
1072
|
+
readonly timesUs?: readonly number[]
|
|
1073
|
+
readonly contacts?: string
|
|
1074
|
+
readonly cutBeforeUs?: readonly number[]
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
interface LoadedScene {
|
|
1078
|
+
readonly source: WorkspaceSource
|
|
1079
|
+
readonly scene: SpatialSceneV1
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
interface LoadedDiagram {
|
|
1083
|
+
readonly source: WorkspaceSource
|
|
1084
|
+
readonly spec: DiagramSpec
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
interface PublicArtifactPaths {
|
|
1088
|
+
readonly tldr: string
|
|
1089
|
+
readonly lightSvg: string
|
|
1090
|
+
readonly darkSvg: string
|
|
1091
|
+
readonly lightPng: string
|
|
1092
|
+
readonly darkPng: string
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1096
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function safeFragment(value: string, maximumLength = 160): string {
|
|
1100
|
+
return value
|
|
1101
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
1102
|
+
.replace(/\s+/g, " ")
|
|
1103
|
+
.trim()
|
|
1104
|
+
.slice(0, maximumLength)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function safeIssues(issues: readonly string[]): readonly string[] {
|
|
1108
|
+
return issues.slice(0, 24).map((issue) => safeFragment(issue, 240))
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function rejectUnknownKeys(
|
|
1112
|
+
value: Record<string, unknown>,
|
|
1113
|
+
allowed: ReadonlySet<string>,
|
|
1114
|
+
): void {
|
|
1115
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key))
|
|
1116
|
+
if (unknown.length > 0) {
|
|
1117
|
+
throw new ToolFailure(
|
|
1118
|
+
"INVALID_ARGUMENTS",
|
|
1119
|
+
`Unsupported argument: ${safeFragment(unknown[0] ?? "unknown")}.`,
|
|
1120
|
+
)
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function parsePath(value: unknown): string {
|
|
1125
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1126
|
+
throw new ToolFailure(
|
|
1127
|
+
"INVALID_ARGUMENTS",
|
|
1128
|
+
"path must be a non-empty root-relative string.",
|
|
1129
|
+
)
|
|
1130
|
+
}
|
|
1131
|
+
if (!value.toLowerCase().endsWith(".diagram.json")) {
|
|
1132
|
+
throw new ToolFailure(
|
|
1133
|
+
"INVALID_ARGUMENTS",
|
|
1134
|
+
"path must end in .diagram.json.",
|
|
1135
|
+
)
|
|
1136
|
+
}
|
|
1137
|
+
return value
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function parseCheckArguments(value: unknown): ParsedCheckArguments {
|
|
1141
|
+
if (!isRecord(value)) {
|
|
1142
|
+
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
1143
|
+
}
|
|
1144
|
+
rejectUnknownKeys(value, new Set(["path"]))
|
|
1145
|
+
return { path: parsePath(value.path) }
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function parseRenderArguments(value: unknown): ParsedRenderArguments {
|
|
1149
|
+
if (!isRecord(value)) {
|
|
1150
|
+
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
1151
|
+
}
|
|
1152
|
+
rejectUnknownKeys(value, new Set(["path", "out_dir", "scale"]))
|
|
1153
|
+
const outDirectory = value.out_dir
|
|
1154
|
+
if (
|
|
1155
|
+
outDirectory !== undefined &&
|
|
1156
|
+
(typeof outDirectory !== "string" || outDirectory.length === 0)
|
|
1157
|
+
) {
|
|
1158
|
+
throw new ToolFailure(
|
|
1159
|
+
"INVALID_ARGUMENTS",
|
|
1160
|
+
"out_dir must be a non-empty root-relative string when present.",
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
const scale = value.scale ?? defaultScale
|
|
1164
|
+
if (
|
|
1165
|
+
typeof scale !== "number" ||
|
|
1166
|
+
!Number.isFinite(scale) ||
|
|
1167
|
+
scale <= 0 ||
|
|
1168
|
+
scale > mcpMaximumScale
|
|
1169
|
+
) {
|
|
1170
|
+
throw new ToolFailure(
|
|
1171
|
+
"RENDER_LIMIT",
|
|
1172
|
+
`scale must be greater than zero and no more than ${mcpMaximumScale}.`,
|
|
1173
|
+
)
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
path: parsePath(value.path),
|
|
1177
|
+
...(outDirectory === undefined ? {} : { outDirectory }),
|
|
1178
|
+
scale,
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function parseSearchArguments(value: unknown): ParsedSearchArguments {
|
|
1183
|
+
if (!isRecord(value)) {
|
|
1184
|
+
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
1185
|
+
}
|
|
1186
|
+
rejectUnknownKeys(value, new Set(["query", "limit"]))
|
|
1187
|
+
const query = value.query ?? ""
|
|
1188
|
+
const limit = value.limit ?? slopcameraOperationCodes.length
|
|
1189
|
+
if (
|
|
1190
|
+
typeof query !== "string" ||
|
|
1191
|
+
query.length > 200 ||
|
|
1192
|
+
/[\u0000-\u001f\u007f]/u.test(query) ||
|
|
1193
|
+
!Number.isInteger(limit) ||
|
|
1194
|
+
(limit as number) < 1 ||
|
|
1195
|
+
(limit as number) > 20
|
|
1196
|
+
) {
|
|
1197
|
+
throw new ToolFailure(
|
|
1198
|
+
"INVALID_ARGUMENTS",
|
|
1199
|
+
"query must be a bounded string and limit must be an integer from 1 through 20.",
|
|
1200
|
+
)
|
|
1201
|
+
}
|
|
1202
|
+
return { query, limit: limit as number }
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function parseExecuteArguments(value: unknown): ParsedExecuteArguments {
|
|
1206
|
+
if (!isRecord(value)) {
|
|
1207
|
+
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
1208
|
+
}
|
|
1209
|
+
rejectUnknownKeys(value, new Set(["operation", "input"]))
|
|
1210
|
+
if (
|
|
1211
|
+
typeof value.operation !== "string" ||
|
|
1212
|
+
!slopcameraOperationCodes.includes(value.operation as SlopcameraOperationCode) ||
|
|
1213
|
+
!isRecord(value.input)
|
|
1214
|
+
) {
|
|
1215
|
+
throw new ToolFailure(
|
|
1216
|
+
"INVALID_ARGUMENTS",
|
|
1217
|
+
"operation must be an exact Slopcamera operation code and input must be an object.",
|
|
1218
|
+
)
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
operation: value.operation as SlopcameraOperationCode,
|
|
1222
|
+
input: value.input,
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const sceneCameraIdPattern = /^camera_[a-zA-Z0-9][a-zA-Z0-9_-]*$/u
|
|
1227
|
+
|
|
1228
|
+
function parseScenePath(value: unknown, label: string): string {
|
|
1229
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1230
|
+
throw new ToolFailure(
|
|
1231
|
+
"INVALID_ARGUMENTS",
|
|
1232
|
+
`${label} must be a non-empty root-relative string.`,
|
|
1233
|
+
)
|
|
1234
|
+
}
|
|
1235
|
+
if (!value.toLowerCase().endsWith(".json")) {
|
|
1236
|
+
throw new ToolFailure(
|
|
1237
|
+
"INVALID_ARGUMENTS",
|
|
1238
|
+
`${label} must end in .json.`,
|
|
1239
|
+
)
|
|
1240
|
+
}
|
|
1241
|
+
return value
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function parseSceneCameraId(value: unknown): string {
|
|
1245
|
+
if (
|
|
1246
|
+
typeof value !== "string" ||
|
|
1247
|
+
value.length > 128 ||
|
|
1248
|
+
!sceneCameraIdPattern.test(value)
|
|
1249
|
+
) {
|
|
1250
|
+
throw new ToolFailure(
|
|
1251
|
+
"INVALID_ARGUMENTS",
|
|
1252
|
+
"camera_id must be a scene camera identifier (camera_ followed by letters, digits, _ or -).",
|
|
1253
|
+
)
|
|
1254
|
+
}
|
|
1255
|
+
return value
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function parseSceneSourceArguments(value: unknown): ParsedSceneSourceArguments {
|
|
1259
|
+
if (!isRecord(value)) {
|
|
1260
|
+
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
1261
|
+
}
|
|
1262
|
+
rejectUnknownKeys(value, new Set(["path"]))
|
|
1263
|
+
return { path: parseScenePath(value.path, "path") }
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function parseAuditSceneArguments(value: unknown): ParsedAuditSceneArguments {
|
|
1267
|
+
if (!isRecord(value)) {
|
|
1268
|
+
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
1269
|
+
}
|
|
1270
|
+
rejectUnknownKeys(value, new Set(["path", "camera_id", "times_us", "asset_bounds"]))
|
|
1271
|
+
let timesUs: readonly number[] | undefined
|
|
1272
|
+
if (value.times_us !== undefined) {
|
|
1273
|
+
if (
|
|
1274
|
+
!Array.isArray(value.times_us) ||
|
|
1275
|
+
value.times_us.length === 0 ||
|
|
1276
|
+
value.times_us.length > SPATIAL_AUDIT_LIMITS.samples ||
|
|
1277
|
+
value.times_us.some((timeUs) => (
|
|
1278
|
+
!Number.isSafeInteger(timeUs) ||
|
|
1279
|
+
(timeUs as number) < 0 ||
|
|
1280
|
+
(timeUs as number) > SPATIAL_SCENE_LIMITS.durationUs
|
|
1281
|
+
))
|
|
1282
|
+
) {
|
|
1283
|
+
throw new ToolFailure(
|
|
1284
|
+
"INVALID_ARGUMENTS",
|
|
1285
|
+
`times_us must be an array of 1–${SPATIAL_AUDIT_LIMITS.samples} integer microsecond times within the scene duration bound.`,
|
|
1286
|
+
)
|
|
1287
|
+
}
|
|
1288
|
+
timesUs = [...value.times_us] as number[]
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
path: parseScenePath(value.path, "path"),
|
|
1292
|
+
cameraId: parseSceneCameraId(value.camera_id),
|
|
1293
|
+
...(timesUs === undefined ? {} : { timesUs }),
|
|
1294
|
+
...("asset_bounds" in value ? { assetBounds: value.asset_bounds } : {}),
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function parseDiffScenesArguments(value: unknown): ParsedDiffScenesArguments {
|
|
1299
|
+
if (!isRecord(value)) {
|
|
1300
|
+
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
1301
|
+
}
|
|
1302
|
+
rejectUnknownKeys(value, new Set(["path", "other"]))
|
|
1303
|
+
return {
|
|
1304
|
+
path: parseScenePath(value.path, "path"),
|
|
1305
|
+
other: parseScenePath(value.other, "other"),
|
|
1306
|
+
}
|
|
362
1307
|
}
|
|
363
1308
|
|
|
364
|
-
function
|
|
365
|
-
value
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
1309
|
+
function parseEvaluateSceneArguments(value: unknown): ParsedEvaluateSceneArguments {
|
|
1310
|
+
if (!isRecord(value)) {
|
|
1311
|
+
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
1312
|
+
}
|
|
1313
|
+
rejectUnknownKeys(value, new Set(["path", "camera_id", "time_us"]))
|
|
1314
|
+
const timeUs = value.time_us
|
|
1315
|
+
if (
|
|
1316
|
+
typeof timeUs !== "number" ||
|
|
1317
|
+
!Number.isSafeInteger(timeUs) ||
|
|
1318
|
+
timeUs < 0 ||
|
|
1319
|
+
timeUs > SPATIAL_SCENE_LIMITS.durationUs
|
|
1320
|
+
) {
|
|
370
1321
|
throw new ToolFailure(
|
|
371
1322
|
"INVALID_ARGUMENTS",
|
|
372
|
-
`
|
|
1323
|
+
`time_us must be an integer microsecond time from 0 through ${SPATIAL_SCENE_LIMITS.durationUs}.`,
|
|
373
1324
|
)
|
|
374
1325
|
}
|
|
1326
|
+
return {
|
|
1327
|
+
path: parseScenePath(value.path, "path"),
|
|
1328
|
+
cameraId: parseSceneCameraId(value.camera_id),
|
|
1329
|
+
timeUs,
|
|
1330
|
+
}
|
|
375
1331
|
}
|
|
376
1332
|
|
|
377
|
-
function
|
|
378
|
-
|
|
1333
|
+
function parseSceneDirectionArguments(
|
|
1334
|
+
value: unknown,
|
|
1335
|
+
requireAxis: boolean,
|
|
1336
|
+
): ParsedSceneDirectionArguments {
|
|
1337
|
+
if (!isRecord(value)) {
|
|
1338
|
+
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
1339
|
+
}
|
|
1340
|
+
rejectUnknownKeys(value, new Set(["scene", "direction", "camera_id", "axis"]))
|
|
1341
|
+
if (requireAxis && !SpatialGalleryAxisSchema.safeParse(value.axis).success) {
|
|
379
1342
|
throw new ToolFailure(
|
|
380
1343
|
"INVALID_ARGUMENTS",
|
|
381
|
-
|
|
1344
|
+
`axis must be one of ${SpatialGalleryAxisSchema.options.join(", ")}.`,
|
|
382
1345
|
)
|
|
383
1346
|
}
|
|
384
|
-
if (!value.
|
|
1347
|
+
if (!requireAxis && value.axis !== undefined) {
|
|
385
1348
|
throw new ToolFailure(
|
|
386
1349
|
"INVALID_ARGUMENTS",
|
|
387
|
-
"
|
|
1350
|
+
"axis is only valid for plan_scene_gallery.",
|
|
388
1351
|
)
|
|
389
1352
|
}
|
|
390
|
-
return
|
|
1353
|
+
return {
|
|
1354
|
+
scene: parseScenePath(value.scene, "scene"),
|
|
1355
|
+
direction: parseScenePath(value.direction, "direction"),
|
|
1356
|
+
...(value.camera_id === undefined ? {} : { cameraId: parseSceneCameraId(value.camera_id) }),
|
|
1357
|
+
...(value.axis === undefined ? {} : { axis: value.axis as string }),
|
|
1358
|
+
}
|
|
391
1359
|
}
|
|
392
1360
|
|
|
393
|
-
function
|
|
1361
|
+
function parseSceneEffectsArguments(
|
|
1362
|
+
value: unknown,
|
|
1363
|
+
effectsKey: "effects" | "draft",
|
|
1364
|
+
): ParsedSceneEffectsArguments {
|
|
394
1365
|
if (!isRecord(value)) {
|
|
395
1366
|
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
396
1367
|
}
|
|
397
|
-
rejectUnknownKeys(value, new Set(["
|
|
398
|
-
return {
|
|
1368
|
+
rejectUnknownKeys(value, new Set(["scene", effectsKey]))
|
|
1369
|
+
return {
|
|
1370
|
+
scene: parseScenePath(value.scene, "scene"),
|
|
1371
|
+
effects: parseScenePath(value[effectsKey], effectsKey),
|
|
1372
|
+
}
|
|
399
1373
|
}
|
|
400
1374
|
|
|
401
|
-
function
|
|
1375
|
+
function parseSceneBehaviorArguments(
|
|
1376
|
+
value: unknown,
|
|
1377
|
+
): ParsedSceneBehaviorArguments {
|
|
402
1378
|
if (!isRecord(value)) {
|
|
403
1379
|
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
404
1380
|
}
|
|
405
|
-
rejectUnknownKeys(value, new Set(["
|
|
406
|
-
const outDirectory = value.out_dir
|
|
407
|
-
if (
|
|
408
|
-
outDirectory !== undefined &&
|
|
409
|
-
(typeof outDirectory !== "string" || outDirectory.length === 0)
|
|
410
|
-
) {
|
|
411
|
-
throw new ToolFailure(
|
|
412
|
-
"INVALID_ARGUMENTS",
|
|
413
|
-
"out_dir must be a non-empty root-relative string when present.",
|
|
414
|
-
)
|
|
415
|
-
}
|
|
416
|
-
const scale = value.scale ?? defaultScale
|
|
417
|
-
if (
|
|
418
|
-
typeof scale !== "number" ||
|
|
419
|
-
!Number.isFinite(scale) ||
|
|
420
|
-
scale <= 0 ||
|
|
421
|
-
scale > mcpMaximumScale
|
|
422
|
-
) {
|
|
423
|
-
throw new ToolFailure(
|
|
424
|
-
"RENDER_LIMIT",
|
|
425
|
-
`scale must be greater than zero and no more than ${mcpMaximumScale}.`,
|
|
426
|
-
)
|
|
427
|
-
}
|
|
1381
|
+
rejectUnknownKeys(value, new Set(["scene", "behavior"]))
|
|
428
1382
|
return {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
scale,
|
|
1383
|
+
scene: parseScenePath(value.scene, "scene"),
|
|
1384
|
+
behavior: parseScenePath(value.behavior, "behavior"),
|
|
432
1385
|
}
|
|
433
1386
|
}
|
|
434
1387
|
|
|
435
|
-
function
|
|
1388
|
+
function parseSceneBehaviorAuditArguments(
|
|
1389
|
+
value: unknown,
|
|
1390
|
+
): ParsedSceneBehaviorAuditArguments {
|
|
436
1391
|
if (!isRecord(value)) {
|
|
437
1392
|
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
438
1393
|
}
|
|
439
|
-
rejectUnknownKeys(value, new Set(["
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
typeof query !== "string" ||
|
|
444
|
-
query.length > 200 ||
|
|
445
|
-
/[\u0000-\u001f\u007f]/u.test(query) ||
|
|
446
|
-
!Number.isInteger(limit) ||
|
|
447
|
-
(limit as number) < 1 ||
|
|
448
|
-
(limit as number) > 20
|
|
449
|
-
) {
|
|
450
|
-
throw new ToolFailure(
|
|
451
|
-
"INVALID_ARGUMENTS",
|
|
452
|
-
"query must be a bounded string and limit must be an integer from 1 through 20.",
|
|
453
|
-
)
|
|
1394
|
+
rejectUnknownKeys(value, new Set(["bake"]))
|
|
1395
|
+
return {
|
|
1396
|
+
bake: parseScenePath(value.bake, "bake"),
|
|
454
1397
|
}
|
|
455
|
-
return { query, limit: limit as number }
|
|
456
1398
|
}
|
|
457
1399
|
|
|
458
|
-
function
|
|
1400
|
+
function parseSceneTemporalArguments(value: unknown): ParsedSceneTemporalArguments {
|
|
459
1401
|
if (!isRecord(value)) {
|
|
460
1402
|
throw new ToolFailure("INVALID_ARGUMENTS", "Tool arguments must be an object.")
|
|
461
1403
|
}
|
|
462
|
-
rejectUnknownKeys(value, new Set(["
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
1404
|
+
rejectUnknownKeys(value, new Set(["path", "camera_id", "times_us", "contacts", "cut_before_us"]))
|
|
1405
|
+
const boundedTimes = (key: "times_us" | "cut_before_us", maximum: number): readonly number[] | undefined => {
|
|
1406
|
+
const entries = value[key]
|
|
1407
|
+
if (entries === undefined) return undefined
|
|
1408
|
+
if (
|
|
1409
|
+
!Array.isArray(entries) ||
|
|
1410
|
+
entries.length === 0 ||
|
|
1411
|
+
entries.length > maximum ||
|
|
1412
|
+
entries.some((timeUs) => (
|
|
1413
|
+
!Number.isSafeInteger(timeUs) ||
|
|
1414
|
+
(timeUs as number) < 0 ||
|
|
1415
|
+
(timeUs as number) > SPATIAL_SCENE_LIMITS.durationUs
|
|
1416
|
+
))
|
|
1417
|
+
) {
|
|
1418
|
+
throw new ToolFailure(
|
|
1419
|
+
"INVALID_ARGUMENTS",
|
|
1420
|
+
`${key} must be an array of 1–${maximum} integer microsecond times within the scene duration bound.`,
|
|
1421
|
+
)
|
|
1422
|
+
}
|
|
1423
|
+
return [...entries] as number[]
|
|
1424
|
+
}
|
|
1425
|
+
const timesUs = boundedTimes("times_us", SPATIAL_TEMPORAL_AUDIT_LIMITS.samples)
|
|
1426
|
+
const cutBeforeUs = boundedTimes("cut_before_us", SPATIAL_TEMPORAL_AUDIT_LIMITS.cutBoundaries)
|
|
1427
|
+
if (timesUs !== undefined && timesUs.length < 2) {
|
|
468
1428
|
throw new ToolFailure(
|
|
469
1429
|
"INVALID_ARGUMENTS",
|
|
470
|
-
"
|
|
1430
|
+
"times_us must contain at least 2 sample times.",
|
|
471
1431
|
)
|
|
472
1432
|
}
|
|
473
1433
|
return {
|
|
474
|
-
|
|
475
|
-
|
|
1434
|
+
path: parseScenePath(value.path, "path"),
|
|
1435
|
+
cameraId: parseSceneCameraId(value.camera_id),
|
|
1436
|
+
...(timesUs === undefined ? {} : { timesUs }),
|
|
1437
|
+
...(value.contacts === undefined ? {} : { contacts: parseScenePath(value.contacts, "contacts") }),
|
|
1438
|
+
...(cutBeforeUs === undefined ? {} : { cutBeforeUs }),
|
|
476
1439
|
}
|
|
477
1440
|
}
|
|
478
1441
|
|
|
@@ -530,6 +1493,10 @@ function assertRenderLimits(spec: DiagramSpec, scale: number): void {
|
|
|
530
1493
|
}
|
|
531
1494
|
}
|
|
532
1495
|
|
|
1496
|
+
function boundedSlice<T>(items: readonly T[], maximum: number): readonly T[] {
|
|
1497
|
+
return items.length > maximum ? items.slice(0, maximum) : items
|
|
1498
|
+
}
|
|
1499
|
+
|
|
533
1500
|
function publicFinding(finding: LintFinding): LintFinding {
|
|
534
1501
|
return {
|
|
535
1502
|
code: safeFragment(finding.code, 64),
|
|
@@ -603,6 +1570,12 @@ function failureResult(error: unknown): McpToolResult {
|
|
|
603
1570
|
code = "INVALID_DIAGRAM"
|
|
604
1571
|
message = "Diagram source did not pass validation."
|
|
605
1572
|
issues = safeIssues(error.issues)
|
|
1573
|
+
} else if (error instanceof SlopcameraCodeError) {
|
|
1574
|
+
code = error.code.toUpperCase().replace(/-/g, "_")
|
|
1575
|
+
message = safeFragment(error.message, 320)
|
|
1576
|
+
} else if (error instanceof HostResourceError) {
|
|
1577
|
+
code = `HOST_RESOURCE_${error.code}`
|
|
1578
|
+
message = "Host-resource admission failed safely."
|
|
606
1579
|
} else if (
|
|
607
1580
|
typeof error === "object" &&
|
|
608
1581
|
error !== null &&
|
|
@@ -680,6 +1653,44 @@ async function loadDiagram(
|
|
|
680
1653
|
return { source, spec }
|
|
681
1654
|
}
|
|
682
1655
|
|
|
1656
|
+
async function loadScene(
|
|
1657
|
+
boundary: WorkspaceBoundary,
|
|
1658
|
+
path: string,
|
|
1659
|
+
): Promise<LoadedScene> {
|
|
1660
|
+
const source = await boundary.readSource(path, "Scene source")
|
|
1661
|
+
const parsed = parseJsonText(source.text)
|
|
1662
|
+
try {
|
|
1663
|
+
return { source, scene: parseSpatialScene(parsed) }
|
|
1664
|
+
} catch (error) {
|
|
1665
|
+
if (error instanceof SpatialSceneError) {
|
|
1666
|
+
const where = error.path === "scene" ? "" : ` at ${error.path}`
|
|
1667
|
+
throw new ToolFailure(
|
|
1668
|
+
"INVALID_SCENE",
|
|
1669
|
+
`Scene source did not pass validation${where}: ${safeFragment(error.message, 240)}`,
|
|
1670
|
+
)
|
|
1671
|
+
}
|
|
1672
|
+
throw error
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function parseJsonText(text: string): unknown {
|
|
1677
|
+
try {
|
|
1678
|
+
return JSON.parse(text)
|
|
1679
|
+
} catch {
|
|
1680
|
+
throw new ToolFailure("INVALID_JSON", "Source is not valid JSON.")
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
/** Reads one bounded root-relative JSON document without interpreting it. */
|
|
1685
|
+
async function loadJson(
|
|
1686
|
+
boundary: WorkspaceBoundary,
|
|
1687
|
+
path: string,
|
|
1688
|
+
label: string,
|
|
1689
|
+
): Promise<{ readonly source: WorkspaceSource; readonly value: unknown }> {
|
|
1690
|
+
const source = await boundary.readSource(path, label)
|
|
1691
|
+
return { source, value: parseJsonText(source.text) }
|
|
1692
|
+
}
|
|
1693
|
+
|
|
683
1694
|
export class SlopcameraMcpToolRuntime {
|
|
684
1695
|
readonly boundary: WorkspaceBoundary
|
|
685
1696
|
readonly generateDependencies: SlopcameraGenerateDependencies
|
|
@@ -717,6 +1728,25 @@ export class SlopcameraMcpToolRuntime {
|
|
|
717
1728
|
})
|
|
718
1729
|
}
|
|
719
1730
|
|
|
1731
|
+
/**
|
|
1732
|
+
* Scene tools are dedicated read-only surface, not registry operations, so
|
|
1733
|
+
* they claim the diagram-check claim shape (cpu + local-io) directly.
|
|
1734
|
+
*/
|
|
1735
|
+
private async withSceneAdmission<T>(
|
|
1736
|
+
callback: (lease: HostResourceLease) => T | Promise<T>,
|
|
1737
|
+
): Promise<T> {
|
|
1738
|
+
return await this.hostResourceCoordinator.withLease(
|
|
1739
|
+
[
|
|
1740
|
+
{ resource: "cpu", amount: 1 },
|
|
1741
|
+
{ resource: "local-io", amount: 1 },
|
|
1742
|
+
],
|
|
1743
|
+
async (lease) => {
|
|
1744
|
+
await lease.assertOwned()
|
|
1745
|
+
return await callback(lease)
|
|
1746
|
+
},
|
|
1747
|
+
)
|
|
1748
|
+
}
|
|
1749
|
+
|
|
720
1750
|
private enqueueRender<T>(operation: () => Promise<T>): Promise<T> {
|
|
721
1751
|
const result = this.renderQueue.then(operation, operation)
|
|
722
1752
|
this.renderQueue = result.then(
|
|
@@ -759,6 +1789,84 @@ export class SlopcameraMcpToolRuntime {
|
|
|
759
1789
|
const options = parseExecuteArguments(argumentsValue)
|
|
760
1790
|
return await this.execute(options)
|
|
761
1791
|
}
|
|
1792
|
+
if (name === "check_scene") {
|
|
1793
|
+
const options = parseSceneSourceArguments(argumentsValue)
|
|
1794
|
+
return await this.withSceneAdmission(
|
|
1795
|
+
async () => await this.checkScene(options),
|
|
1796
|
+
)
|
|
1797
|
+
}
|
|
1798
|
+
if (name === "inspect_scene") {
|
|
1799
|
+
const options = parseSceneSourceArguments(argumentsValue)
|
|
1800
|
+
return await this.withSceneAdmission(
|
|
1801
|
+
async () => await this.inspectScene(options),
|
|
1802
|
+
)
|
|
1803
|
+
}
|
|
1804
|
+
if (name === "audit_scene") {
|
|
1805
|
+
const options = parseAuditSceneArguments(argumentsValue)
|
|
1806
|
+
return await this.withSceneAdmission(
|
|
1807
|
+
async () => await this.auditScene(options),
|
|
1808
|
+
)
|
|
1809
|
+
}
|
|
1810
|
+
if (name === "diff_scenes") {
|
|
1811
|
+
const options = parseDiffScenesArguments(argumentsValue)
|
|
1812
|
+
return await this.withSceneAdmission(
|
|
1813
|
+
async () => await this.diffScenes(options),
|
|
1814
|
+
)
|
|
1815
|
+
}
|
|
1816
|
+
if (name === "evaluate_scene") {
|
|
1817
|
+
const options = parseEvaluateSceneArguments(argumentsValue)
|
|
1818
|
+
return await this.withSceneAdmission(
|
|
1819
|
+
async () => await this.evaluateScene(options),
|
|
1820
|
+
)
|
|
1821
|
+
}
|
|
1822
|
+
if (name === "check_scene_direction") {
|
|
1823
|
+
const options = parseSceneDirectionArguments(argumentsValue, false)
|
|
1824
|
+
return await this.withSceneAdmission(
|
|
1825
|
+
async () => await this.checkSceneDirection(options),
|
|
1826
|
+
)
|
|
1827
|
+
}
|
|
1828
|
+
if (name === "plan_scene_direction") {
|
|
1829
|
+
const options = parseSceneDirectionArguments(argumentsValue, false)
|
|
1830
|
+
return await this.withSceneAdmission(
|
|
1831
|
+
async () => await this.planSceneDirection(options),
|
|
1832
|
+
)
|
|
1833
|
+
}
|
|
1834
|
+
if (name === "plan_scene_gallery") {
|
|
1835
|
+
const options = parseSceneDirectionArguments(argumentsValue, true)
|
|
1836
|
+
return await this.withSceneAdmission(
|
|
1837
|
+
async () => await this.planSceneGallery(options),
|
|
1838
|
+
)
|
|
1839
|
+
}
|
|
1840
|
+
if (name === "check_scene_effects") {
|
|
1841
|
+
const options = parseSceneEffectsArguments(argumentsValue, "effects")
|
|
1842
|
+
return await this.withSceneAdmission(
|
|
1843
|
+
async () => await this.checkSceneEffects(options),
|
|
1844
|
+
)
|
|
1845
|
+
}
|
|
1846
|
+
if (name === "plan_scene_effects") {
|
|
1847
|
+
const options = parseSceneEffectsArguments(argumentsValue, "draft")
|
|
1848
|
+
return await this.withSceneAdmission(
|
|
1849
|
+
async () => await this.planSceneEffects(options),
|
|
1850
|
+
)
|
|
1851
|
+
}
|
|
1852
|
+
if (name === "check_scene_behavior") {
|
|
1853
|
+
const options = parseSceneBehaviorArguments(argumentsValue)
|
|
1854
|
+
return await this.withSceneAdmission(
|
|
1855
|
+
async () => await this.checkSceneBehavior(options),
|
|
1856
|
+
)
|
|
1857
|
+
}
|
|
1858
|
+
if (name === "audit_scene_behavior") {
|
|
1859
|
+
const options = parseSceneBehaviorAuditArguments(argumentsValue)
|
|
1860
|
+
return await this.withSceneAdmission(
|
|
1861
|
+
async () => await this.auditSceneBehavior(options),
|
|
1862
|
+
)
|
|
1863
|
+
}
|
|
1864
|
+
if (name === "audit_scene_temporal") {
|
|
1865
|
+
const options = parseSceneTemporalArguments(argumentsValue)
|
|
1866
|
+
return await this.withSceneAdmission(
|
|
1867
|
+
async () => await this.auditSceneTemporal(options),
|
|
1868
|
+
)
|
|
1869
|
+
}
|
|
762
1870
|
throw new ToolFailure("UNKNOWN_TOOL", "Requested tool is not available.")
|
|
763
1871
|
} catch (error) {
|
|
764
1872
|
return failureResult(error)
|
|
@@ -854,6 +1962,30 @@ export class SlopcameraMcpToolRuntime {
|
|
|
854
1962
|
},
|
|
855
1963
|
))
|
|
856
1964
|
}
|
|
1965
|
+
if (options.operation === "slopcamera.image.gallery") {
|
|
1966
|
+
const input = parseSlopcameraOperationInput(
|
|
1967
|
+
options.operation,
|
|
1968
|
+
options.input,
|
|
1969
|
+
) as GallerySlopcameraOperationInput
|
|
1970
|
+
return await this.withHostAdmission(options.operation, async () => {
|
|
1971
|
+
const output = await this.boundary.prepareOutputDirectory(input.outputDir)
|
|
1972
|
+
const receipt = await generateSlopcameraImageGallery(
|
|
1973
|
+
{ ...input, outputDir: output.absolutePath },
|
|
1974
|
+
this.generateDependencies,
|
|
1975
|
+
)
|
|
1976
|
+
return successResult(
|
|
1977
|
+
`Executed ${options.operation}: ${receipt.counts.generated}/${receipt.counts.requested} candidates in ${output.relativePath}.`,
|
|
1978
|
+
{
|
|
1979
|
+
ok: true,
|
|
1980
|
+
operation: options.operation,
|
|
1981
|
+
result: {
|
|
1982
|
+
...receipt,
|
|
1983
|
+
outputDir: output.relativePath,
|
|
1984
|
+
},
|
|
1985
|
+
},
|
|
1986
|
+
)
|
|
1987
|
+
})
|
|
1988
|
+
}
|
|
857
1989
|
return await this.withHostAdmission(options.operation, async () => {
|
|
858
1990
|
const input = parseSlopcameraOperationInput(
|
|
859
1991
|
options.operation,
|
|
@@ -960,4 +2092,384 @@ export class SlopcameraMcpToolRuntime {
|
|
|
960
2092
|
summary,
|
|
961
2093
|
})
|
|
962
2094
|
}
|
|
2095
|
+
|
|
2096
|
+
private async checkScene(
|
|
2097
|
+
options: ParsedSceneSourceArguments,
|
|
2098
|
+
): Promise<McpToolResult> {
|
|
2099
|
+
const { source, scene } = await loadScene(this.boundary, options.path)
|
|
2100
|
+
const summary = {
|
|
2101
|
+
durationUs: scene.durationUs,
|
|
2102
|
+
entityCount: scene.entities.length,
|
|
2103
|
+
cameraCount: scene.cameras.length,
|
|
2104
|
+
assetCount: scene.assets.length,
|
|
2105
|
+
animationCount: scene.animations.length,
|
|
2106
|
+
generatorCount: scene.generators.length,
|
|
2107
|
+
overrideCount: scene.overrides.length,
|
|
2108
|
+
}
|
|
2109
|
+
return successResult(
|
|
2110
|
+
`Checked ${source.relativePath}: scene ${safeFragment(scene.sceneId, 128)} is valid (${summary.entityCount} entities, ${summary.cameraCount} cameras).`,
|
|
2111
|
+
{
|
|
2112
|
+
ok: true,
|
|
2113
|
+
source: source.relativePath,
|
|
2114
|
+
sceneId: scene.sceneId,
|
|
2115
|
+
sceneSha256: spatialValueSha256(scene),
|
|
2116
|
+
summary,
|
|
2117
|
+
},
|
|
2118
|
+
)
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
private async inspectScene(
|
|
2122
|
+
options: ParsedSceneSourceArguments,
|
|
2123
|
+
): Promise<McpToolResult> {
|
|
2124
|
+
const { source, scene } = await loadScene(this.boundary, options.path)
|
|
2125
|
+
const inspection = inspectSpatialScene(scene)
|
|
2126
|
+
const entities = boundedSlice(
|
|
2127
|
+
inspection.entities,
|
|
2128
|
+
mcpMaximumReturnedEntities,
|
|
2129
|
+
)
|
|
2130
|
+
const summary = {
|
|
2131
|
+
entityCount: inspection.entities.length,
|
|
2132
|
+
returnedEntityCount: entities.length,
|
|
2133
|
+
entitiesTruncated: entities.length < inspection.entities.length,
|
|
2134
|
+
}
|
|
2135
|
+
return successResult(
|
|
2136
|
+
`Inspected ${source.relativePath}: ${summary.entityCount} entities${summary.entitiesTruncated ? `, first ${summary.returnedEntityCount} returned in structured content` : ""}.`,
|
|
2137
|
+
{
|
|
2138
|
+
ok: true,
|
|
2139
|
+
source: source.relativePath,
|
|
2140
|
+
inspection: { ...inspection, entities },
|
|
2141
|
+
summary,
|
|
2142
|
+
},
|
|
2143
|
+
)
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
private async auditScene(
|
|
2147
|
+
options: ParsedAuditSceneArguments,
|
|
2148
|
+
): Promise<McpToolResult> {
|
|
2149
|
+
const { source, scene } = await loadScene(this.boundary, options.path)
|
|
2150
|
+
const report = auditSpatialScene(scene, {
|
|
2151
|
+
cameraId: options.cameraId,
|
|
2152
|
+
...(options.timesUs === undefined ? {} : { timesUs: options.timesUs }),
|
|
2153
|
+
...(options.assetBounds === undefined
|
|
2154
|
+
? {}
|
|
2155
|
+
: {
|
|
2156
|
+
assetBounds: normalizeSpatialAuditAssetBounds(options.assetBounds),
|
|
2157
|
+
}),
|
|
2158
|
+
})
|
|
2159
|
+
// Report rows are capped by count and by cumulative samples so one result
|
|
2160
|
+
// stays bounded even at the portable entitySamples limit.
|
|
2161
|
+
let sampleBudget = mcpMaximumReturnedAuditSamples
|
|
2162
|
+
const entities: SpatialAuditEntity[] = []
|
|
2163
|
+
for (const entity of report.entities) {
|
|
2164
|
+
if (
|
|
2165
|
+
entities.length >= mcpMaximumReturnedEntities ||
|
|
2166
|
+
entity.samples.length > sampleBudget
|
|
2167
|
+
) {
|
|
2168
|
+
break
|
|
2169
|
+
}
|
|
2170
|
+
sampleBudget -= entity.samples.length
|
|
2171
|
+
entities.push(entity)
|
|
2172
|
+
}
|
|
2173
|
+
const findings = boundedSlice(
|
|
2174
|
+
report.findings,
|
|
2175
|
+
mcpMaximumReturnedFindings,
|
|
2176
|
+
)
|
|
2177
|
+
const findingCount = report.findings.length + report.omittedFindings
|
|
2178
|
+
const summary = {
|
|
2179
|
+
findingCount,
|
|
2180
|
+
returnedFindingCount: findings.length,
|
|
2181
|
+
findingsTruncated: findings.length < findingCount,
|
|
2182
|
+
entityCount: report.entities.length,
|
|
2183
|
+
returnedEntityCount: entities.length,
|
|
2184
|
+
entitiesTruncated: entities.length < report.entities.length,
|
|
2185
|
+
}
|
|
2186
|
+
return successResult(
|
|
2187
|
+
`Audited ${source.relativePath} under ${options.cameraId}: ${findingCount} finding${findingCount === 1 ? "" : "s"} across ${report.entities.length} entities and ${report.timesUs.length} samples${summary.findingsTruncated || summary.entitiesTruncated ? " (truncated)" : ""}.`,
|
|
2188
|
+
{
|
|
2189
|
+
ok: true,
|
|
2190
|
+
source: source.relativePath,
|
|
2191
|
+
report: { ...report, entities, findings },
|
|
2192
|
+
summary,
|
|
2193
|
+
},
|
|
2194
|
+
)
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
private async diffScenes(
|
|
2198
|
+
options: ParsedDiffScenesArguments,
|
|
2199
|
+
): Promise<McpToolResult> {
|
|
2200
|
+
const { source, scene } = await loadScene(this.boundary, options.path)
|
|
2201
|
+
const other = await loadScene(this.boundary, options.other)
|
|
2202
|
+
const entries = diffSpatialScenes(scene, other.scene)
|
|
2203
|
+
const diff = boundedSlice(entries, mcpMaximumReturnedDiffEntries)
|
|
2204
|
+
const summary = {
|
|
2205
|
+
entryCount: entries.length,
|
|
2206
|
+
returnedEntryCount: diff.length,
|
|
2207
|
+
diffTruncated: diff.length < entries.length,
|
|
2208
|
+
}
|
|
2209
|
+
return successResult(
|
|
2210
|
+
`Diffed ${source.relativePath} against ${other.source.relativePath}: ${summary.entryCount} entr${summary.entryCount === 1 ? "y" : "ies"}${summary.diffTruncated ? `, first ${summary.returnedEntryCount} returned in structured content` : ""}.`,
|
|
2211
|
+
{
|
|
2212
|
+
ok: true,
|
|
2213
|
+
source: source.relativePath,
|
|
2214
|
+
other: other.source.relativePath,
|
|
2215
|
+
sceneSha256: spatialValueSha256(scene),
|
|
2216
|
+
otherSha256: spatialValueSha256(other.scene),
|
|
2217
|
+
diff,
|
|
2218
|
+
summary,
|
|
2219
|
+
},
|
|
2220
|
+
)
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
private async evaluateScene(
|
|
2224
|
+
options: ParsedEvaluateSceneArguments,
|
|
2225
|
+
): Promise<McpToolResult> {
|
|
2226
|
+
const { source, scene } = await loadScene(this.boundary, options.path)
|
|
2227
|
+
const snapshot = evaluateSpatialScene(scene, {
|
|
2228
|
+
timeUs: options.timeUs,
|
|
2229
|
+
cameraId: options.cameraId,
|
|
2230
|
+
})
|
|
2231
|
+
const entities = boundedSlice(
|
|
2232
|
+
snapshot.entities,
|
|
2233
|
+
mcpMaximumReturnedEntities,
|
|
2234
|
+
)
|
|
2235
|
+
const summary = {
|
|
2236
|
+
entityCount: snapshot.entities.length,
|
|
2237
|
+
returnedEntityCount: entities.length,
|
|
2238
|
+
entitiesTruncated: entities.length < snapshot.entities.length,
|
|
2239
|
+
}
|
|
2240
|
+
return successResult(
|
|
2241
|
+
`Evaluated ${source.relativePath} at timeUs ${options.timeUs} under ${options.cameraId}: ${summary.entityCount} entities${summary.entitiesTruncated ? `, first ${summary.returnedEntityCount} returned in structured content` : ""}.`,
|
|
2242
|
+
{
|
|
2243
|
+
ok: true,
|
|
2244
|
+
source: source.relativePath,
|
|
2245
|
+
snapshot: { ...snapshot, entities },
|
|
2246
|
+
summary,
|
|
2247
|
+
},
|
|
2248
|
+
)
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
private async checkSceneDirection(
|
|
2252
|
+
options: ParsedSceneDirectionArguments,
|
|
2253
|
+
): Promise<McpToolResult> {
|
|
2254
|
+
const { source, scene } = await loadScene(this.boundary, options.scene)
|
|
2255
|
+
const direction = await loadJson(this.boundary, options.direction, "Direction source")
|
|
2256
|
+
const report = checkSpatialDirection({ direction: direction.value, scene })
|
|
2257
|
+
const findings = boundedSlice(report.findings, mcpMaximumReturnedFindings)
|
|
2258
|
+
const summary = {
|
|
2259
|
+
findingCount: report.findings.length,
|
|
2260
|
+
returnedFindingCount: findings.length,
|
|
2261
|
+
findingsTruncated: findings.length < report.findings.length,
|
|
2262
|
+
errorCount: report.counts.errors,
|
|
2263
|
+
warningCount: report.counts.warnings,
|
|
2264
|
+
}
|
|
2265
|
+
return successResult(
|
|
2266
|
+
`Checked direction ${direction.source.relativePath} against ${source.relativePath}: ${summary.findingCount} finding${summary.findingCount === 1 ? "" : "s"} (${summary.errorCount} errors).`,
|
|
2267
|
+
{
|
|
2268
|
+
ok: summary.errorCount === 0,
|
|
2269
|
+
source: source.relativePath,
|
|
2270
|
+
report: { ...report, findings },
|
|
2271
|
+
summary,
|
|
2272
|
+
},
|
|
2273
|
+
)
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
private async planSceneDirection(
|
|
2277
|
+
options: ParsedSceneDirectionArguments,
|
|
2278
|
+
): Promise<McpToolResult> {
|
|
2279
|
+
const { source, scene } = await loadScene(this.boundary, options.scene)
|
|
2280
|
+
const direction = await loadJson(this.boundary, options.direction, "Direction source")
|
|
2281
|
+
const compilation = compileSpatialDirection({
|
|
2282
|
+
direction: direction.value,
|
|
2283
|
+
scene,
|
|
2284
|
+
...(options.cameraId === undefined ? {} : { cameraId: options.cameraId }),
|
|
2285
|
+
})
|
|
2286
|
+
const summary = {
|
|
2287
|
+
advisoryCount: compilation.advisories.length,
|
|
2288
|
+
unresolvedIntentCount: compilation.unresolvedIntents.length,
|
|
2289
|
+
proposalCounts: {
|
|
2290
|
+
performance: compilation.proposals.performance.length,
|
|
2291
|
+
cameraRigs: compilation.proposals.cameraRigs.length,
|
|
2292
|
+
shots: compilation.proposals.shots.length,
|
|
2293
|
+
lookIntents: compilation.proposals.lookIntents.length,
|
|
2294
|
+
},
|
|
2295
|
+
}
|
|
2296
|
+
return successResult(
|
|
2297
|
+
`Compiled direction ${direction.source.relativePath}: ${compilation.proposals.shots.length} shot proposals, ${compilation.advisories.length} advisories; all proposals remain unverified.`,
|
|
2298
|
+
{
|
|
2299
|
+
ok: true,
|
|
2300
|
+
source: source.relativePath,
|
|
2301
|
+
compilation,
|
|
2302
|
+
summary,
|
|
2303
|
+
},
|
|
2304
|
+
)
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
private async planSceneGallery(
|
|
2308
|
+
options: ParsedSceneDirectionArguments,
|
|
2309
|
+
): Promise<McpToolResult> {
|
|
2310
|
+
const { source, scene } = await loadScene(this.boundary, options.scene)
|
|
2311
|
+
const direction = await loadJson(this.boundary, options.direction, "Direction source")
|
|
2312
|
+
const gallery = planSpatialDirectionGallery({
|
|
2313
|
+
axis: options.axis,
|
|
2314
|
+
direction: direction.value,
|
|
2315
|
+
scene,
|
|
2316
|
+
...(options.cameraId === undefined ? {} : { cameraId: options.cameraId }),
|
|
2317
|
+
})
|
|
2318
|
+
const summary = {
|
|
2319
|
+
axis: gallery.axis,
|
|
2320
|
+
candidateCount: gallery.candidates.length,
|
|
2321
|
+
selected: false as const,
|
|
2322
|
+
}
|
|
2323
|
+
return successResult(
|
|
2324
|
+
`Planned a ${gallery.axis} gallery from ${direction.source.relativePath}: ${gallery.candidates.length} content-addressed candidates; none selected.`,
|
|
2325
|
+
{
|
|
2326
|
+
ok: true,
|
|
2327
|
+
source: source.relativePath,
|
|
2328
|
+
gallery,
|
|
2329
|
+
summary,
|
|
2330
|
+
},
|
|
2331
|
+
)
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
private async checkSceneEffects(
|
|
2335
|
+
options: ParsedSceneEffectsArguments,
|
|
2336
|
+
): Promise<McpToolResult> {
|
|
2337
|
+
const { source, scene } = await loadScene(this.boundary, options.scene)
|
|
2338
|
+
const effects = await loadJson(this.boundary, options.effects, "Effects source")
|
|
2339
|
+
const report = checkSpatialRenderEffects({ effects: effects.value, scene })
|
|
2340
|
+
const findings = boundedSlice(report.findings, mcpMaximumReturnedFindings)
|
|
2341
|
+
const summary = {
|
|
2342
|
+
findingCount: report.findings.length,
|
|
2343
|
+
returnedFindingCount: findings.length,
|
|
2344
|
+
findingsTruncated: findings.length < report.findings.length,
|
|
2345
|
+
errorCount: report.counts.errors,
|
|
2346
|
+
warningCount: report.counts.warnings,
|
|
2347
|
+
}
|
|
2348
|
+
return successResult(
|
|
2349
|
+
`Checked effects ${effects.source.relativePath} against ${source.relativePath}: ${summary.findingCount} finding${summary.findingCount === 1 ? "" : "s"} (${summary.errorCount} errors).`,
|
|
2350
|
+
{
|
|
2351
|
+
ok: summary.errorCount === 0,
|
|
2352
|
+
source: source.relativePath,
|
|
2353
|
+
report: { ...report, findings },
|
|
2354
|
+
summary,
|
|
2355
|
+
},
|
|
2356
|
+
)
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
private async planSceneEffects(
|
|
2360
|
+
options: ParsedSceneEffectsArguments,
|
|
2361
|
+
): Promise<McpToolResult> {
|
|
2362
|
+
const { source, scene } = await loadScene(this.boundary, options.scene)
|
|
2363
|
+
const draft = await loadJson(this.boundary, options.effects, "Effects draft")
|
|
2364
|
+
if (!isRecord(draft.value)) {
|
|
2365
|
+
throw new ToolFailure("INVALID_ARGUMENTS", "Effects draft must be a JSON object with renderPlan, particleSystems and simulationBakes fields.")
|
|
2366
|
+
}
|
|
2367
|
+
const binding = planSpatialRenderEffects({ ...draft.value, scene })
|
|
2368
|
+
const summary = {
|
|
2369
|
+
documentSha256: binding.documentSha256,
|
|
2370
|
+
particleSystemCount: binding.document.particleSystems.length,
|
|
2371
|
+
simulationBakeCount: binding.document.simulationBakes.length,
|
|
2372
|
+
}
|
|
2373
|
+
return successResult(
|
|
2374
|
+
`Bound effects draft ${draft.source.relativePath} to scene ${summary.documentSha256.slice(0, 16)}…: ${summary.particleSystemCount} particle systems, ${summary.simulationBakeCount} simulation bakes.`,
|
|
2375
|
+
{
|
|
2376
|
+
ok: true,
|
|
2377
|
+
source: source.relativePath,
|
|
2378
|
+
binding,
|
|
2379
|
+
summary,
|
|
2380
|
+
},
|
|
2381
|
+
)
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
private async checkSceneBehavior(
|
|
2385
|
+
options: ParsedSceneBehaviorArguments,
|
|
2386
|
+
): Promise<McpToolResult> {
|
|
2387
|
+
const { source, scene } = await loadScene(this.boundary, options.scene)
|
|
2388
|
+
const behavior = await loadJson(this.boundary, options.behavior, "Behavior source")
|
|
2389
|
+
const report = checkSpatialBehavior({ behavior: behavior.value, scene }, spatialBehaviorFnSignatures())
|
|
2390
|
+
const findings = boundedSlice(report.findings, mcpMaximumReturnedFindings)
|
|
2391
|
+
const summary = {
|
|
2392
|
+
findingCount: report.findings.length,
|
|
2393
|
+
returnedFindingCount: findings.length,
|
|
2394
|
+
findingsTruncated: findings.length < report.findings.length,
|
|
2395
|
+
errorCount: report.counts.errors,
|
|
2396
|
+
warningCount: report.counts.warnings,
|
|
2397
|
+
}
|
|
2398
|
+
return successResult(
|
|
2399
|
+
`Checked behavior ${behavior.source.relativePath} against ${source.relativePath}: ${summary.findingCount} finding${summary.findingCount === 1 ? "" : "s"} (${summary.errorCount} errors).`,
|
|
2400
|
+
{
|
|
2401
|
+
ok: summary.errorCount === 0,
|
|
2402
|
+
source: source.relativePath,
|
|
2403
|
+
report: { ...report, findings },
|
|
2404
|
+
summary,
|
|
2405
|
+
},
|
|
2406
|
+
)
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
private async auditSceneBehavior(
|
|
2410
|
+
options: ParsedSceneBehaviorAuditArguments,
|
|
2411
|
+
): Promise<McpToolResult> {
|
|
2412
|
+
const bake = await loadJson(this.boundary, options.bake, "Behavior bake source")
|
|
2413
|
+
const report = auditSpatialBehaviorTrace(bake.value)
|
|
2414
|
+
const findings = boundedSlice(report.findings, mcpMaximumReturnedFindings)
|
|
2415
|
+
const summary = {
|
|
2416
|
+
findingCount: report.findings.length,
|
|
2417
|
+
channelCount: report.channelCount,
|
|
2418
|
+
emittedCount: report.emittedCount,
|
|
2419
|
+
}
|
|
2420
|
+
return successResult(
|
|
2421
|
+
`Audited behavior bake ${bake.source.relativePath}: ${summary.findingCount} finding${summary.findingCount === 1 ? "" : "s"} across ${summary.channelCount} channel${summary.channelCount === 1 ? "" : "s"}.`,
|
|
2422
|
+
{
|
|
2423
|
+
ok: true,
|
|
2424
|
+
source: bake.source.relativePath,
|
|
2425
|
+
report: { ...report, findings },
|
|
2426
|
+
summary,
|
|
2427
|
+
},
|
|
2428
|
+
)
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
private async auditSceneTemporal(
|
|
2432
|
+
options: ParsedSceneTemporalArguments,
|
|
2433
|
+
): Promise<McpToolResult> {
|
|
2434
|
+
const { source, scene } = await loadScene(this.boundary, options.path)
|
|
2435
|
+
const times = options.timesUs ?? (() => {
|
|
2436
|
+
const count = Math.min(17, Math.max(2, Math.ceil(scene.durationUs / 1_000_000) + 1))
|
|
2437
|
+
return Array.from({ length: count }, (_value, index) => Math.floor((scene.durationUs * index) / (count - 1)))
|
|
2438
|
+
})()
|
|
2439
|
+
const contacts = options.contacts === undefined
|
|
2440
|
+
? []
|
|
2441
|
+
: parseSpatialValue(
|
|
2442
|
+
SpatialTemporalContactsFileSchema,
|
|
2443
|
+
(await loadJson(this.boundary, options.contacts, "Contacts source")).value,
|
|
2444
|
+
"temporal contacts",
|
|
2445
|
+
).contacts
|
|
2446
|
+
const context = createSpatialEvaluationContext(scene)
|
|
2447
|
+
const snapshots = [...times]
|
|
2448
|
+
.sort((left, right) => left - right)
|
|
2449
|
+
.map((timeUs) => evaluateSpatialSceneInContext(context, { cameraId: options.cameraId, timeUs }))
|
|
2450
|
+
const report = auditSpatialTemporalEvidence({
|
|
2451
|
+
snapshots,
|
|
2452
|
+
options: {
|
|
2453
|
+
contacts,
|
|
2454
|
+
...(options.cutBeforeUs === undefined ? {} : { cutBeforeUs: [...options.cutBeforeUs] }),
|
|
2455
|
+
},
|
|
2456
|
+
})
|
|
2457
|
+
const findings = boundedSlice(report.findings, mcpMaximumReturnedFindings)
|
|
2458
|
+
const findingCount = report.findings.length + report.omittedFindings
|
|
2459
|
+
const summary = {
|
|
2460
|
+
findingCount,
|
|
2461
|
+
returnedFindingCount: findings.length,
|
|
2462
|
+
findingsTruncated: findings.length < findingCount,
|
|
2463
|
+
sampleCount: report.sampleCount,
|
|
2464
|
+
}
|
|
2465
|
+
return successResult(
|
|
2466
|
+
`Audited ${source.relativePath} over ${report.sampleCount} samples under ${options.cameraId}: ${findingCount} temporal finding${findingCount === 1 ? "" : "s"}${summary.findingsTruncated ? " (truncated)" : ""}.`,
|
|
2467
|
+
{
|
|
2468
|
+
ok: true,
|
|
2469
|
+
source: source.relativePath,
|
|
2470
|
+
report: { ...report, findings },
|
|
2471
|
+
summary,
|
|
2472
|
+
},
|
|
2473
|
+
)
|
|
2474
|
+
}
|
|
963
2475
|
}
|