@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.
Files changed (188) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +65 -20
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +6 -9
  6. package/apps/desktop/application/default-registry.ts +26 -10
  7. package/apps/desktop/application/operation.ts +14 -5
  8. package/apps/desktop/application/operations/index.ts +4 -1
  9. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  10. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  11. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  12. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  13. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  14. package/apps/desktop/application/registry.ts +2 -2
  15. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  16. package/apps/desktop/application/spatial-assets.ts +160 -31
  17. package/apps/desktop/application/spatial-render.ts +217 -20
  18. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  19. package/apps/desktop/application/spatial-review.ts +361 -0
  20. package/apps/desktop/application/spatial-spz.ts +48 -3
  21. package/apps/desktop/application/spatial-world-import.ts +26 -5
  22. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  23. package/apps/desktop/cli/args.ts +528 -97
  24. package/apps/desktop/cli/capability-manifest.ts +374 -0
  25. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  26. package/apps/desktop/cli/command-host-resources.ts +31 -2
  27. package/apps/desktop/cli/commands.ts +1215 -70
  28. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  29. package/apps/desktop/cli/help.ts +183 -23
  30. package/apps/desktop/cli/html-scene.ts +139 -28
  31. package/apps/desktop/cli/io.ts +2 -1
  32. package/apps/desktop/cli/main.ts +26 -50
  33. package/apps/desktop/cli/menubar.ts +244 -0
  34. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  35. package/apps/desktop/cli/portable-surface.ts +18 -5
  36. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  37. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  38. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  39. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  40. package/apps/desktop/cli/workflow-code.ts +1 -1
  41. package/apps/desktop/code/application-node-planner.ts +0 -47
  42. package/apps/desktop/code/host-source-layout.ts +46 -0
  43. package/apps/desktop/code/public.ts +0 -1
  44. package/apps/desktop/code/runtime-identity.ts +50 -8
  45. package/apps/desktop/code/semantic-builder.ts +24 -43
  46. package/apps/desktop/code/source-bundle.ts +23 -6
  47. package/apps/desktop/code/source-typecheck.ts +131 -43
  48. package/apps/desktop/code/worker-client.ts +14 -2
  49. package/apps/desktop/contracts/cinema.ts +736 -0
  50. package/apps/desktop/contracts/index.ts +1 -0
  51. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  52. package/apps/desktop/contracts/spatial-world.ts +26 -3
  53. package/apps/desktop/core/cinema-plan.ts +1320 -0
  54. package/apps/desktop/core/index.ts +1 -0
  55. package/apps/desktop/core/music-analysis.ts +124 -1
  56. package/apps/desktop/core/storage.ts +75 -0
  57. package/apps/desktop/dist/cli/main.js +737 -291
  58. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  59. package/apps/desktop/html-overlay/index.ts +9 -0
  60. package/apps/desktop/html-overlay/runtime.ts +2 -0
  61. package/apps/desktop/html-overlay/scene.ts +29 -0
  62. package/apps/desktop/html-overlay/spatial.ts +619 -57
  63. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  64. package/apps/desktop/workflows/index.ts +6 -0
  65. package/dist/cli.js +32 -434
  66. package/dist/code/advanced.js +1 -269
  67. package/dist/code/index.js +16 -2239
  68. package/dist/generate.js +1 -23
  69. package/dist/host-resources.js +1 -25
  70. package/dist/index-0bbesn4m.js +35 -0
  71. package/dist/index-296qhpf2.js +3 -0
  72. package/dist/index-3cxf2c58.js +3 -0
  73. package/dist/index-77fjfg9f.js +3 -0
  74. package/dist/index-80a36bc0.js +3 -0
  75. package/dist/index-a3vvs3rf.js +3 -0
  76. package/dist/index-b3geqczn.js +5 -0
  77. package/dist/index-cgpkyjqa.js +3 -0
  78. package/dist/index-f0ptv8dm.js +7 -0
  79. package/dist/index-grk47tgs.js +3 -0
  80. package/dist/index-qry58nj2.js +10 -0
  81. package/dist/index.js +1 -240
  82. package/dist/operations.js +1 -28
  83. package/dist/skill-install-gx7afxsd.js +2 -0
  84. package/dist/vectorize/worker.js +1 -141
  85. package/dist/workflow.js +1 -15
  86. package/docs/README.md +5 -1
  87. package/docs/studio.md +1 -1
  88. package/package.json +11 -24
  89. package/skills/slopcamera/SKILL.md +14 -1
  90. package/skills/slopcamera/references/diagrams.md +5 -14
  91. package/skills/slopcamera/references/directed-scenes.md +22 -2
  92. package/skills/slopcamera/references/gateway-media.md +15 -1
  93. package/skills/slopcamera/references/image-galleries.md +166 -0
  94. package/skills/slopcamera/references/install.md +2 -2
  95. package/skills/slopcamera/references/music-video.md +23 -3
  96. package/skills/slopcamera/references/parametric-design.md +14 -0
  97. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  98. package/skills/slopcamera/references/scene-building.md +32 -0
  99. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  100. package/skills/slopcamera/references/support.md +25 -0
  101. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  102. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  103. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  104. package/src/capability-manifest.ts +361 -0
  105. package/src/cli.ts +250 -102
  106. package/src/code/canonical-json.ts +9 -0
  107. package/src/generate.ts +2 -2
  108. package/src/icon.ts +1111 -0
  109. package/src/image-gallery.ts +1049 -0
  110. package/src/index.ts +11 -23
  111. package/src/mcp/boundary.ts +11 -11
  112. package/src/mcp/index.ts +3 -0
  113. package/src/mcp/server.ts +2 -1
  114. package/src/mcp/tools.ts +1642 -130
  115. package/src/mcp/types.ts +19 -0
  116. package/src/operations.ts +412 -0
  117. package/src/portable-capability-manifest.ts +67 -0
  118. package/src/scene-gallery.ts +184 -0
  119. package/src/spatial-scene/asset-admission.ts +183 -0
  120. package/src/spatial-scene/audit-rendered.ts +692 -0
  121. package/src/spatial-scene/audit.ts +617 -0
  122. package/src/spatial-scene/behavior-audit.ts +252 -0
  123. package/src/spatial-scene/behavior-bake.ts +233 -0
  124. package/src/spatial-scene/behavior-fns.ts +585 -0
  125. package/src/spatial-scene/behavior-gallery.ts +109 -0
  126. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  127. package/src/spatial-scene/behavior-trace.ts +325 -0
  128. package/src/spatial-scene/behavior.ts +618 -0
  129. package/src/spatial-scene/build.ts +632 -0
  130. package/src/spatial-scene/camera-rig.ts +125 -0
  131. package/src/spatial-scene/camera-track.ts +4 -2
  132. package/src/spatial-scene/character.ts +124 -0
  133. package/src/spatial-scene/contracts.ts +75 -7
  134. package/src/spatial-scene/design-templates.ts +351 -0
  135. package/src/spatial-scene/design.ts +327 -0
  136. package/src/spatial-scene/direction-compile.ts +584 -0
  137. package/src/spatial-scene/direction.ts +122 -0
  138. package/src/spatial-scene/effects.ts +149 -0
  139. package/src/spatial-scene/evaluate.ts +109 -29
  140. package/src/spatial-scene/gallery.ts +277 -0
  141. package/src/spatial-scene/generate.ts +239 -0
  142. package/src/spatial-scene/geometry-native.ts +165 -0
  143. package/src/spatial-scene/geometry.ts +1216 -0
  144. package/src/spatial-scene/gltf.ts +600 -77
  145. package/src/spatial-scene/identity.ts +55 -2
  146. package/src/spatial-scene/index.ts +36 -0
  147. package/src/spatial-scene/inspect.ts +35 -6
  148. package/src/spatial-scene/material-lighting.ts +609 -0
  149. package/src/spatial-scene/math.ts +21 -4
  150. package/src/spatial-scene/motion-evidence.ts +88 -0
  151. package/src/spatial-scene/parametric.ts +765 -0
  152. package/src/spatial-scene/particle-preparation.ts +183 -0
  153. package/src/spatial-scene/particle.ts +195 -0
  154. package/src/spatial-scene/patch.ts +109 -15
  155. package/src/spatial-scene/performance.ts +1613 -0
  156. package/src/spatial-scene/probe.ts +137 -0
  157. package/src/spatial-scene/recipe-pack.ts +86 -0
  158. package/src/spatial-scene/render-effects.ts +240 -0
  159. package/src/spatial-scene/review.ts +639 -0
  160. package/src/spatial-scene/simulation-bake.ts +206 -0
  161. package/src/spatial-scene/simulation.ts +187 -0
  162. package/src/spatial-scene/solve.ts +366 -0
  163. package/src/spatial-scene/temporal-audit.ts +354 -0
  164. package/src/support-completion.ts +16 -0
  165. package/src/support.ts +42 -0
  166. package/src/version.ts +1 -1
  167. package/apps/desktop/application/operations/recording/index.ts +0 -5
  168. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  169. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  170. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  171. package/apps/desktop/application/operations/recording/start.ts +0 -169
  172. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  173. package/apps/desktop/capture/protocol.ts +0 -689
  174. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  175. package/apps/desktop/cli/recording-controller.ts +0 -1567
  176. package/apps/desktop/cli/recording-daemon.ts +0 -931
  177. package/dist/index-42zsesc1.js +0 -2257
  178. package/dist/index-7308egqr.js +0 -48
  179. package/dist/index-8txs6fkn.js +0 -712
  180. package/dist/index-mcy8z0br.js +0 -192
  181. package/dist/index-p63wavx0.js +0 -2029
  182. package/dist/index-r7gdhmsp.js +0 -424
  183. package/dist/index-rvghzt7b.js +0 -2013
  184. package/dist/index-sh6xbav6.js +0 -1219
  185. package/dist/index-z1w83f81.js +0 -4
  186. package/dist/index-z7239b4h.js +0 -2363
  187. package/dist/skill-install-dh0ntqba.js +0 -10
  188. 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
- class ToolFailure extends Error {
305
- readonly code: string
306
- readonly issues?: readonly string[]
307
-
308
- constructor(code: string, message: string, issues?: readonly string[]) {
309
- super(message)
310
- this.name = "ToolFailure"
311
- this.code = code
312
- if (issues !== undefined) this.issues = issues
313
- }
314
- }
315
-
316
- interface ParsedCheckArguments {
317
- readonly path: string
318
- }
319
-
320
- interface ParsedRenderArguments extends ParsedCheckArguments {
321
- readonly outDirectory?: string
322
- readonly scale: number
323
- }
324
-
325
- interface ParsedSearchArguments {
326
- readonly query: string
327
- readonly limit: number
328
- }
329
-
330
- interface ParsedExecuteArguments {
331
- readonly operation: SlopcameraOperationCode
332
- readonly input: unknown
333
- }
334
-
335
- interface LoadedDiagram {
336
- readonly source: WorkspaceSource
337
- readonly spec: DiagramSpec
338
- }
339
-
340
- interface PublicArtifactPaths {
341
- readonly tldr: string
342
- readonly lightSvg: string
343
- readonly darkSvg: string
344
- readonly lightPng: string
345
- readonly darkPng: string
346
- }
347
-
348
- function isRecord(value: unknown): value is Record<string, unknown> {
349
- return typeof value === "object" && value !== null && !Array.isArray(value)
350
- }
351
-
352
- function safeFragment(value: string, maximumLength = 160): string {
353
- return value
354
- .replace(/[\u0000-\u001f\u007f]/g, " ")
355
- .replace(/\s+/g, " ")
356
- .trim()
357
- .slice(0, maximumLength)
358
- }
359
-
360
- function safeIssues(issues: readonly string[]): readonly string[] {
361
- return issues.slice(0, 24).map((issue) => safeFragment(issue, 240))
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 rejectUnknownKeys(
365
- value: Record<string, unknown>,
366
- allowed: ReadonlySet<string>,
367
- ): void {
368
- const unknown = Object.keys(value).filter((key) => !allowed.has(key))
369
- if (unknown.length > 0) {
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
- `Unsupported argument: ${safeFragment(unknown[0] ?? "unknown")}.`,
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 parsePath(value: unknown): string {
378
- if (typeof value !== "string" || value.length === 0) {
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
- "path must be a non-empty root-relative string.",
1344
+ `axis must be one of ${SpatialGalleryAxisSchema.options.join(", ")}.`,
382
1345
  )
383
1346
  }
384
- if (!value.toLowerCase().endsWith(".diagram.json")) {
1347
+ if (!requireAxis && value.axis !== undefined) {
385
1348
  throw new ToolFailure(
386
1349
  "INVALID_ARGUMENTS",
387
- "path must end in .diagram.json.",
1350
+ "axis is only valid for plan_scene_gallery.",
388
1351
  )
389
1352
  }
390
- return value
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 parseCheckArguments(value: unknown): ParsedCheckArguments {
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(["path"]))
398
- return { path: parsePath(value.path) }
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 parseRenderArguments(value: unknown): ParsedRenderArguments {
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(["path", "out_dir", "scale"]))
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
- path: parsePath(value.path),
430
- ...(outDirectory === undefined ? {} : { outDirectory }),
431
- scale,
1383
+ scene: parseScenePath(value.scene, "scene"),
1384
+ behavior: parseScenePath(value.behavior, "behavior"),
432
1385
  }
433
1386
  }
434
1387
 
435
- function parseSearchArguments(value: unknown): ParsedSearchArguments {
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(["query", "limit"]))
440
- const query = value.query ?? ""
441
- const limit = value.limit ?? slopcameraOperationCodes.length
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 parseExecuteArguments(value: unknown): ParsedExecuteArguments {
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(["operation", "input"]))
463
- if (
464
- typeof value.operation !== "string" ||
465
- !slopcameraOperationCodes.includes(value.operation as SlopcameraOperationCode) ||
466
- !isRecord(value.input)
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
- "operation must be an exact Slopcamera operation code and input must be an object.",
1430
+ "times_us must contain at least 2 sample times.",
471
1431
  )
472
1432
  }
473
1433
  return {
474
- operation: value.operation as SlopcameraOperationCode,
475
- input: value.input,
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
  }