@hraness/slopcamera 3.2.8 → 3.3.2

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 (153) hide show
  1. package/README.md +29 -9
  2. package/apps/desktop/application/context.ts +6 -0
  3. package/apps/desktop/application/default-registry.ts +26 -0
  4. package/apps/desktop/application/operation.ts +14 -1
  5. package/apps/desktop/application/operations/index.ts +4 -0
  6. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  7. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  8. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  9. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  10. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  11. package/apps/desktop/application/registry.ts +2 -2
  12. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  13. package/apps/desktop/application/spatial-assets.ts +160 -31
  14. package/apps/desktop/application/spatial-render.ts +217 -20
  15. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  16. package/apps/desktop/application/spatial-review.ts +361 -0
  17. package/apps/desktop/application/spatial-spz.ts +48 -3
  18. package/apps/desktop/application/spatial-world-import.ts +26 -5
  19. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  20. package/apps/desktop/cli/args.ts +514 -11
  21. package/apps/desktop/cli/capability-manifest.ts +374 -0
  22. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  23. package/apps/desktop/cli/command-host-resources.ts +29 -1
  24. package/apps/desktop/cli/commands.ts +1188 -4
  25. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  26. package/apps/desktop/cli/help.ts +172 -6
  27. package/apps/desktop/cli/main.ts +3 -3
  28. package/apps/desktop/cli/portable-surface.ts +6 -1
  29. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  30. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  31. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  32. package/apps/desktop/cli/spatial-scene-service.ts +271 -4
  33. package/apps/desktop/cli/workflow-code.ts +1 -1
  34. package/apps/desktop/code/semantic-builder.ts +24 -0
  35. package/apps/desktop/contracts/cinema.ts +736 -0
  36. package/apps/desktop/contracts/index.ts +1 -0
  37. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  38. package/apps/desktop/contracts/spatial-world.ts +26 -3
  39. package/apps/desktop/core/cinema-plan.ts +1320 -0
  40. package/apps/desktop/core/index.ts +1 -0
  41. package/apps/desktop/core/storage.ts +75 -0
  42. package/apps/desktop/dist/cli/main.js +725 -273
  43. package/apps/desktop/html-overlay/spatial.ts +619 -57
  44. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  45. package/apps/desktop/workflows/index.ts +6 -0
  46. package/dist/cli.js +25 -429
  47. package/dist/code/advanced.js +1 -269
  48. package/dist/code/index.js +16 -2239
  49. package/dist/generate.js +1 -23
  50. package/dist/host-resources.js +1 -25
  51. package/dist/index-0bbesn4m.js +35 -0
  52. package/dist/index-296qhpf2.js +3 -0
  53. package/dist/index-3cxf2c58.js +3 -0
  54. package/dist/index-77fjfg9f.js +3 -0
  55. package/dist/index-80a36bc0.js +3 -0
  56. package/dist/index-a3vvs3rf.js +3 -0
  57. package/dist/index-b3geqczn.js +5 -0
  58. package/dist/index-grk47tgs.js +3 -0
  59. package/dist/index-gs48afpt.js +7 -0
  60. package/dist/index-qry58nj2.js +10 -0
  61. package/dist/index-zhg36tf9.js +3 -0
  62. package/dist/index.js +1 -226
  63. package/dist/operations.js +1 -28
  64. package/dist/skill-install-gx7afxsd.js +2 -0
  65. package/dist/vectorize/worker.js +1 -141
  66. package/dist/workflow.js +1 -15
  67. package/docs/README.md +5 -1
  68. package/docs/studio.md +12 -0
  69. package/package.json +7 -5
  70. package/skills/slopcamera/SKILL.md +7 -1
  71. package/skills/slopcamera/references/directed-scenes.md +48 -2
  72. package/skills/slopcamera/references/gateway-media.md +15 -1
  73. package/skills/slopcamera/references/image-galleries.md +166 -0
  74. package/skills/slopcamera/references/install.md +1 -1
  75. package/skills/slopcamera/references/music-video.md +4 -0
  76. package/skills/slopcamera/references/native-studio.md +19 -0
  77. package/skills/slopcamera/references/parametric-design.md +14 -0
  78. package/skills/slopcamera/references/scene-building.md +172 -0
  79. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  80. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  81. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  82. package/src/capability-manifest.ts +361 -0
  83. package/src/cli.ts +222 -4
  84. package/src/code/canonical-json.ts +9 -0
  85. package/src/generate.ts +2 -2
  86. package/src/icon.ts +1111 -0
  87. package/src/image-gallery.ts +1049 -0
  88. package/src/index.ts +11 -0
  89. package/src/mcp/boundary.ts +11 -11
  90. package/src/mcp/index.ts +3 -0
  91. package/src/mcp/server.ts +2 -1
  92. package/src/mcp/tools.ts +1649 -132
  93. package/src/mcp/types.ts +19 -0
  94. package/src/operations.ts +412 -0
  95. package/src/portable-capability-manifest.ts +67 -0
  96. package/src/scene-gallery.ts +184 -0
  97. package/src/spatial-scene/asset-admission.ts +183 -0
  98. package/src/spatial-scene/audit-rendered.ts +692 -0
  99. package/src/spatial-scene/audit.ts +617 -0
  100. package/src/spatial-scene/behavior-audit.ts +252 -0
  101. package/src/spatial-scene/behavior-bake.ts +232 -0
  102. package/src/spatial-scene/behavior-fns.ts +585 -0
  103. package/src/spatial-scene/behavior-gallery.ts +108 -0
  104. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  105. package/src/spatial-scene/behavior-trace.ts +325 -0
  106. package/src/spatial-scene/behavior.ts +617 -0
  107. package/src/spatial-scene/build.ts +632 -0
  108. package/src/spatial-scene/camera-rig.ts +125 -0
  109. package/src/spatial-scene/camera-track.ts +4 -2
  110. package/src/spatial-scene/character.ts +124 -0
  111. package/src/spatial-scene/contracts.ts +75 -7
  112. package/src/spatial-scene/design-templates.ts +351 -0
  113. package/src/spatial-scene/design.ts +327 -0
  114. package/src/spatial-scene/direction-compile.ts +584 -0
  115. package/src/spatial-scene/direction.ts +122 -0
  116. package/src/spatial-scene/effects.ts +149 -0
  117. package/src/spatial-scene/evaluate.ts +109 -29
  118. package/src/spatial-scene/gallery.ts +277 -0
  119. package/src/spatial-scene/generate.ts +239 -0
  120. package/src/spatial-scene/geometry-native.ts +165 -0
  121. package/src/spatial-scene/geometry.ts +1216 -0
  122. package/src/spatial-scene/gltf.ts +600 -77
  123. package/src/spatial-scene/identity.ts +55 -2
  124. package/src/spatial-scene/index.ts +36 -0
  125. package/src/spatial-scene/inspect.ts +35 -6
  126. package/src/spatial-scene/material-lighting.ts +609 -0
  127. package/src/spatial-scene/math.ts +21 -4
  128. package/src/spatial-scene/motion-evidence.ts +88 -0
  129. package/src/spatial-scene/parametric.ts +765 -0
  130. package/src/spatial-scene/particle-preparation.ts +183 -0
  131. package/src/spatial-scene/particle.ts +195 -0
  132. package/src/spatial-scene/patch.ts +109 -15
  133. package/src/spatial-scene/performance.ts +1613 -0
  134. package/src/spatial-scene/probe.ts +137 -0
  135. package/src/spatial-scene/recipe-pack.ts +86 -0
  136. package/src/spatial-scene/render-effects.ts +240 -0
  137. package/src/spatial-scene/review.ts +639 -0
  138. package/src/spatial-scene/simulation-bake.ts +206 -0
  139. package/src/spatial-scene/simulation.ts +187 -0
  140. package/src/spatial-scene/solve.ts +366 -0
  141. package/src/spatial-scene/temporal-audit.ts +354 -0
  142. package/src/version.ts +1 -1
  143. package/dist/index-42zsesc1.js +0 -2257
  144. package/dist/index-7131pg1c.js +0 -1820
  145. package/dist/index-7308egqr.js +0 -48
  146. package/dist/index-8txs6fkn.js +0 -712
  147. package/dist/index-fava6pge.js +0 -192
  148. package/dist/index-h1k0fnjq.js +0 -2363
  149. package/dist/index-r7gdhmsp.js +0 -424
  150. package/dist/index-sh6xbav6.js +0 -1219
  151. package/dist/index-z1w83f81.js +0 -4
  152. package/dist/index-zfnddgay.js +0 -2029
  153. package/dist/skill-install-dh0ntqba.js +0 -10
@@ -0,0 +1,277 @@
1
+ import { z } from "zod"
2
+
3
+ import { deepFreezeJson } from "../code/json-snapshot.js"
4
+ import {
5
+ SpatialCameraIdSchema,
6
+ SpatialDigestSchema,
7
+ SpatialSceneV1Schema,
8
+ SpatialTimeUsSchema,
9
+ } from "./contracts.js"
10
+ import {
11
+ compileSpatialDirection,
12
+ SpatialDirectionCompilationSchema,
13
+ spatialDirectionCompilationSha256,
14
+ type SpatialDirectionCompilation,
15
+ } from "./direction-compile.js"
16
+ import { SpatialDirectionSchema, type SpatialDirection } from "./direction.js"
17
+ import { parseSpatialValue, spatialValueSha256 } from "./identity.js"
18
+
19
+ /**
20
+ * Bounded direction galleries for Phase 12.
21
+ *
22
+ * A gallery plan enumerates at most six deterministic variants of one direction
23
+ * along a single axis — performance pacing, camera rig families, lighting and
24
+ * material presets, post-process suggestions, or sequence granularity. Camera
25
+ * and sequence variants re-author the direction and recompile; look variants
26
+ * annotate the compiled look intents. Every candidate embeds its own compiled
27
+ * variant document: source, derived, and selection identities stay separate,
28
+ * and the planner never renders or selects.
29
+ */
30
+
31
+ export const SPATIAL_GALLERY_LIMITS = Object.freeze({
32
+ candidates: 6,
33
+ previewSamples: 8,
34
+ })
35
+
36
+ export const SpatialGalleryAxisSchema = z.enum([
37
+ "performance",
38
+ "camera",
39
+ "lighting",
40
+ "materials",
41
+ "effects",
42
+ "sequence",
43
+ ])
44
+ export type SpatialGalleryAxis = z.infer<typeof SpatialGalleryAxisSchema>
45
+
46
+ export const SpatialGalleryCandidateSchema = z.strictObject({
47
+ candidateId: z.string().min(1).max(96).regex(/^cand_[a-f0-9]{16}$/u),
48
+ label: z.string().min(1).max(128),
49
+ parameter: z.string().min(1).max(64),
50
+ documentKind: z.literal("slopcamera.spatial-direction-compilation"),
51
+ documentSha256: SpatialDigestSchema,
52
+ document: z.unknown(),
53
+ })
54
+ export type SpatialGalleryCandidate = Readonly<z.infer<typeof SpatialGalleryCandidateSchema>>
55
+
56
+ export const SpatialGalleryPlanSchema = z.strictObject({
57
+ kind: z.literal("slopcamera.spatial-gallery-plan"),
58
+ schemaVersion: z.literal(1),
59
+ axis: SpatialGalleryAxisSchema,
60
+ sourceSha256: SpatialDigestSchema,
61
+ sceneSha256: SpatialDigestSchema,
62
+ cameraId: SpatialCameraIdSchema.optional(),
63
+ candidates: z.array(SpatialGalleryCandidateSchema).max(SPATIAL_GALLERY_LIMITS.candidates),
64
+ previewReel: z.strictObject({
65
+ sampleTimesUs: z.array(SpatialTimeUsSchema).max(SPATIAL_GALLERY_LIMITS.previewSamples),
66
+ }).optional(),
67
+ selection: z.strictObject({
68
+ candidateId: z.string().min(1).max(96),
69
+ selectorDigest: SpatialDigestSchema.optional(),
70
+ note: z.string().min(1).max(256).optional(),
71
+ }).optional(),
72
+ })
73
+ export type SpatialGalleryPlan = Readonly<z.infer<typeof SpatialGalleryPlanSchema>>
74
+
75
+ export function spatialGalleryPlanSha256(plan: SpatialGalleryPlan): string {
76
+ return spatialValueSha256(plan)
77
+ }
78
+
79
+ const CAMERA_VARIANT_RIGS = ["tripod", "dolly", "orbit", "rail", "chase", "handheld"] as const
80
+ const PERFORMANCE_VARIANT_SCALE = [1, 0.9, 1.1, 0.75, 1.25, 1.5] as const
81
+ const LIGHTING_VARIANT_PRESETS = ["neutral", "low-key", "high-key", "night", "dawn", "storm"] as const
82
+ const MATERIAL_VARIANT_PALETTES = ["authored", "warm", "cool", "monochrome", "pastel", "noir"] as const
83
+ const SEQUENCE_VARIANT_STRIDE = [1, 2, 3, 4, 5, 6] as const
84
+
85
+ const EFFECTS_VARIANT_STACKS: ReadonlyArray<readonly Record<string, unknown>[]> = [
86
+ [],
87
+ [{ kind: "tone-map", exposure: 1, whitePoint: 4 }],
88
+ [
89
+ { kind: "tone-map", exposure: 1, whitePoint: 4 },
90
+ { kind: "vignette", intensity: 0.25, radius: 0.5 },
91
+ ],
92
+ [
93
+ { kind: "tone-map", exposure: 1, whitePoint: 4 },
94
+ { kind: "grain", intensity: 0.08, seed: 7 },
95
+ ],
96
+ [
97
+ { kind: "tone-map", exposure: 1, whitePoint: 4 },
98
+ { kind: "bloom", threshold: 0.85, intensity: 0.35, radius: 0.5 },
99
+ ],
100
+ [
101
+ { kind: "tone-map", exposure: 0.9, whitePoint: 2 },
102
+ { kind: "vignette", intensity: 0.35, radius: 0.6 },
103
+ { kind: "grain", intensity: 0.1, seed: 11 },
104
+ ],
105
+ ]
106
+
107
+ const candidateId = (axis: SpatialGalleryAxis, parameter: string, sourceSha256: string): string =>
108
+ `cand_${spatialValueSha256({ domain: "slopcamera.gallery-candidate.v1", axis, parameter, sourceSha256 }).slice(0, 16)}`
109
+
110
+ interface GalleryVariant {
111
+ readonly parameter: string
112
+ readonly label: string
113
+ readonly direction?: (direction: SpatialDirection) => SpatialDirection
114
+ readonly document?: (compilation: SpatialDirectionCompilation) => SpatialDirectionCompilation
115
+ }
116
+
117
+ function variantsForAxis(axis: SpatialGalleryAxis, sceneDurationUs: number): readonly GalleryVariant[] {
118
+ switch (axis) {
119
+ case "camera":
120
+ return CAMERA_VARIANT_RIGS.map((rigKind) => ({
121
+ parameter: rigKind,
122
+ label: `All coverage on ${rigKind} rigs`,
123
+ direction: (direction) => deepFreezeJson({
124
+ ...direction,
125
+ coverage: direction.coverage.map((coverage) => ({ ...coverage, rigKind })),
126
+ }),
127
+ }))
128
+ case "performance":
129
+ return PERFORMANCE_VARIANT_SCALE.map((scale) => ({
130
+ parameter: `pacing-${scale}`,
131
+ label: `Action pacing scaled ${scale}×`,
132
+ direction: (direction) => deepFreezeJson({
133
+ ...direction,
134
+ actions: direction.actions.map((action) => ({
135
+ ...action,
136
+ endUs: Math.min(
137
+ action.startUs + Math.max(1, Math.round((action.endUs - action.startUs) * scale)),
138
+ sceneDurationUs,
139
+ ),
140
+ })),
141
+ }),
142
+ }))
143
+ case "sequence":
144
+ return SEQUENCE_VARIANT_STRIDE.map((stride) => ({
145
+ parameter: `stride-${stride}`,
146
+ label: stride === 1 ? "Coverage-aligned shots" : `Merge coverage in groups of ${stride}`,
147
+ direction: (direction) => {
148
+ if (stride <= 1) return direction
149
+ const coverage = []
150
+ for (let index = 0; index < direction.coverage.length; index += stride) {
151
+ const group = direction.coverage.slice(index, index + stride)
152
+ const first = group[0]!
153
+ const last = group[group.length - 1]!
154
+ coverage.push({ ...first, endUs: last.endUs })
155
+ }
156
+ return deepFreezeJson({ ...direction, coverage })
157
+ },
158
+ }))
159
+ case "lighting":
160
+ return LIGHTING_VARIANT_PRESETS.map((preset) => ({
161
+ parameter: preset,
162
+ label: `Lighting preset ${preset}`,
163
+ document: (compilation) => SpatialDirectionCompilationSchema.parse(deepFreezeJson({
164
+ ...compilation,
165
+ proposals: {
166
+ ...compilation.proposals,
167
+ lookIntents: compilation.proposals.lookIntents.map((intent) => ({
168
+ ...intent,
169
+ suggestedLightingPreset: preset,
170
+ })),
171
+ },
172
+ })),
173
+ }))
174
+ case "materials":
175
+ return MATERIAL_VARIANT_PALETTES.map((palette) => ({
176
+ parameter: palette,
177
+ label: `Material palette ${palette}`,
178
+ document: (compilation) => SpatialDirectionCompilationSchema.parse(deepFreezeJson({
179
+ ...compilation,
180
+ proposals: {
181
+ ...compilation.proposals,
182
+ lookIntents: compilation.proposals.lookIntents.map((intent) => ({
183
+ ...intent,
184
+ suggestedMaterialPalette: palette,
185
+ })),
186
+ },
187
+ })),
188
+ }))
189
+ case "effects":
190
+ return EFFECTS_VARIANT_STACKS.map((stack, index) => ({
191
+ parameter: `stack-${index}`,
192
+ label: stack.length === 0
193
+ ? "No post-process stack"
194
+ : `Post stack: ${stack.map((step) => String(step.kind)).join(" → ")}`,
195
+ document: (compilation) => SpatialDirectionCompilationSchema.parse(deepFreezeJson({
196
+ ...compilation,
197
+ proposals: {
198
+ ...compilation.proposals,
199
+ lookIntents: compilation.proposals.lookIntents.map((intent) => ({
200
+ ...intent,
201
+ suggestedPostProcess: stack,
202
+ })),
203
+ },
204
+ })),
205
+ }))
206
+ }
207
+ }
208
+
209
+ const galleryOptionsSchema = z.strictObject({
210
+ direction: z.unknown(),
211
+ scene: z.unknown(),
212
+ axis: SpatialGalleryAxisSchema,
213
+ cameraId: z.string().min(1).max(128).optional(),
214
+ })
215
+
216
+ const compileVariant = (
217
+ direction: SpatialDirection,
218
+ scene: unknown,
219
+ cameraId: string | undefined,
220
+ ): SpatialDirectionCompilation =>
221
+ compileSpatialDirection({ direction, scene, ...(cameraId === undefined ? {} : { cameraId }) })
222
+
223
+ /**
224
+ * Builds a bounded gallery plan. Variants compile independently; candidates
225
+ * whose documents collide keep the first so the plan stays deduplicated and
226
+ * deterministic. `selection` is never populated by the planner.
227
+ */
228
+ export function planSpatialDirectionGallery(input: unknown): SpatialGalleryPlan {
229
+ const options = parseSpatialValue(galleryOptionsSchema, input, "direction gallery")
230
+ const direction = parseSpatialValue(SpatialDirectionSchema, options.direction, "direction")
231
+ const scene = parseSpatialValue(SpatialSceneV1Schema, options.scene, "scene")
232
+ const base = compileSpatialDirection({
233
+ direction, scene, ...(options.cameraId === undefined ? {} : { cameraId: options.cameraId }),
234
+ })
235
+
236
+ const seen = new Set<string>()
237
+ const candidates: SpatialGalleryCandidate[] = []
238
+ for (const variant of variantsForAxis(options.axis, scene.durationUs)) {
239
+ const document = variant.direction !== undefined
240
+ ? compileVariant(variant.direction(direction), scene, options.cameraId)
241
+ : variant.document!(base)
242
+ const documentSha256 = spatialDirectionCompilationSha256(document)
243
+ if (seen.has(documentSha256)) continue
244
+ seen.add(documentSha256)
245
+ candidates.push(deepFreezeJson({
246
+ candidateId: candidateId(options.axis, variant.parameter, base.directionSha256),
247
+ label: variant.label,
248
+ parameter: variant.parameter,
249
+ documentKind: "slopcamera.spatial-direction-compilation",
250
+ documentSha256,
251
+ document,
252
+ }))
253
+ if (candidates.length >= SPATIAL_GALLERY_LIMITS.candidates) break
254
+ }
255
+
256
+ const sampleTimes = new Set<number>()
257
+ for (const beat of direction.beats) {
258
+ sampleTimes.add(Math.min(beat.startUs + Math.floor((beat.endUs - beat.startUs) / 2), scene.durationUs))
259
+ }
260
+ for (const coverage of direction.coverage) {
261
+ sampleTimes.add(Math.min(coverage.startUs, scene.durationUs))
262
+ }
263
+ const previewReel = sampleTimes.size === 0
264
+ ? undefined
265
+ : { sampleTimesUs: [...sampleTimes].sort((a, b) => a - b).slice(0, SPATIAL_GALLERY_LIMITS.previewSamples) }
266
+
267
+ return deepFreezeJson(SpatialGalleryPlanSchema.parse({
268
+ kind: "slopcamera.spatial-gallery-plan",
269
+ schemaVersion: 1,
270
+ axis: options.axis,
271
+ sourceSha256: base.directionSha256,
272
+ sceneSha256: base.sceneSha256,
273
+ ...(base.cameraId === undefined ? {} : { cameraId: base.cameraId }),
274
+ candidates,
275
+ ...(previewReel === undefined ? {} : { previewReel }),
276
+ }))
277
+ }
@@ -0,0 +1,239 @@
1
+ import { z } from "zod"
2
+ import type { JsonValue } from "../code/contracts.js"
3
+ import { deepFreezeJson, createBoundedJsonValueSnapshot } from "../code/json-snapshot.js"
4
+ import {
5
+ SPATIAL_SCENE_LIMITS, SpatialDigestSchema, SpatialEntitySchema, SpatialGeneratorIdSchema,
6
+ SpatialGeneratorSchema,
7
+ type SpatialAssetManifest, type SpatialEntity, type SpatialGenerator, type SpatialSceneV1,
8
+ } from "./contracts.js"
9
+ import {
10
+ generatedSpatialEntityId, parseSpatialScene, parseSpatialValue, SpatialSceneError,
11
+ spatialGeneratorOutputSha256, spatialValueSha256,
12
+ } from "./identity.js"
13
+
14
+ /**
15
+ * Authoring-time generator support. These functions shape and validate the
16
+ * output of an already-executed generator module; importing or running module
17
+ * source is a host effect owned by the CLI layer, never this portable layer.
18
+ */
19
+ export const SPATIAL_GENERATOR_LIMITS = Object.freeze({
20
+ moduleSourceBytes: 1_048_576,
21
+ parametersBytes: 65_536,
22
+ parametersDepth: 16,
23
+ parametersValues: 8_192,
24
+ })
25
+
26
+ /** The module contract: generate(ctx) returns entities without identity plus declared editable keys. */
27
+ export interface SpatialGeneratorModuleContext {
28
+ readonly seed: number
29
+ readonly parameters: JsonValue
30
+ readonly lib: {
31
+ /** Derived identity for a sibling key, for parentId links inside one output. */
32
+ readonly entityId: (key: string) => string
33
+ }
34
+ }
35
+
36
+ const moduleResultSchema = z.strictObject({
37
+ entities: z.array(z.unknown()).max(SPATIAL_SCENE_LIMITS.entities),
38
+ editableKeys: SpatialGeneratorSchema.shape.editableKeys.optional(),
39
+ })
40
+
41
+ export interface ValidatedSpatialGeneratorOutput {
42
+ readonly entities: readonly SpatialEntity[]
43
+ readonly editableKeys: SpatialGenerator["editableKeys"]
44
+ }
45
+
46
+ /** Parameters are bounded plain JSON; modules receive the frozen captured value. */
47
+ export function parseSpatialGeneratorParameters(input: unknown): JsonValue {
48
+ return createBoundedJsonValueSnapshot(input, SPATIAL_GENERATOR_LIMITS.parametersBytes, "generator parameters", {
49
+ maximumDepth: SPATIAL_GENERATOR_LIMITS.parametersDepth,
50
+ maximumValues: SPATIAL_GENERATOR_LIMITS.parametersValues,
51
+ }).value
52
+ }
53
+
54
+ export function spatialGeneratorParametersSha256(parameters: unknown): string {
55
+ return spatialValueSha256({ domain: "slopcamera.generator-parameters.v1", parameters })
56
+ }
57
+
58
+ /** Absent --seed derives a stable uint32 from the exact source bytes. */
59
+ export function deriveSpatialGeneratorSeed(sourceSha256: string): number {
60
+ SpatialDigestSchema.parse(sourceSha256)
61
+ return Number.parseInt(sourceSha256.slice(0, 8), 16)
62
+ }
63
+
64
+ /**
65
+ * Content-addressed attempt identity. Identical inputs reproduce the identical
66
+ * retained record; a changed source, parameters, seed, runtime, or output is a
67
+ * different attempt.
68
+ */
69
+ export function spatialGeneratorAttemptId(options: {
70
+ readonly generatorId: string
71
+ readonly sourceSha256: string
72
+ readonly parametersSha256: string
73
+ readonly runtimeSha256: string
74
+ readonly outputSha256: string
75
+ readonly seed: number
76
+ }): string {
77
+ return `attempt_${spatialValueSha256({ domain: "slopcamera.generator-attempt.v1", ...options }).slice(0, 32)}`
78
+ }
79
+
80
+ function generatedAssetReference(entity: SpatialEntity): string | undefined {
81
+ if (entity.kind === "mesh") {
82
+ if (entity.geometry.kind === "asset") return entity.geometry.assetId
83
+ return entity.material.kind !== "pbr" ? entity.material.map : undefined
84
+ }
85
+ if (entity.kind === "text") return entity.fontAssetId
86
+ return "assetId" in entity ? entity.assetId : undefined
87
+ }
88
+
89
+ /**
90
+ * Stamps generated identity onto keyed entities and validates the result.
91
+ * Modules must not set entityId or origin: the host owns the
92
+ * entityId == generatedSpatialEntityId(generatorId, key) invariant, so a module
93
+ * cannot forge authored provenance or another generator's output. This version
94
+ * rejects asset references; generated output is mesh primitives, lights, and
95
+ * groups only.
96
+ */
97
+ export function validateSpatialGeneratorOutput(generatorId: string, output: unknown): ValidatedSpatialGeneratorOutput {
98
+ SpatialGeneratorIdSchema.parse(generatorId)
99
+ const result = parseSpatialValue(moduleResultSchema, output, "generator output")
100
+ const seen = new Set<string>()
101
+ const entities: SpatialEntity[] = []
102
+ for (const [index, raw] of result.entities.entries()) {
103
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
104
+ throw new SpatialSceneError("invalid-data", `Generated entity ${index} must be a plain object carrying a stable key.`, "generator output")
105
+ }
106
+ const record = raw as Readonly<Record<string, unknown>>
107
+ const key = record.key
108
+ if (typeof key !== "string" || key.length < 1 || key.length > 256) {
109
+ throw new SpatialSceneError("invalid-data", `Generated entity ${index} must carry a string "key" of 1–256 characters.`, "generator output")
110
+ }
111
+ if ("entityId" in record || "origin" in record) {
112
+ throw new SpatialSceneError("invalid-data", `Generated entity "${key}" must not set entityId or origin; the host stamps generated identity.`, "generator output")
113
+ }
114
+ if (seen.has(key)) throw new SpatialSceneError("invalid-data", `Duplicate generator output key "${key}".`, "generator output")
115
+ seen.add(key)
116
+ const { key: _omitted, ...fields } = record
117
+ const entity = parseSpatialValue(SpatialEntitySchema, {
118
+ ...fields,
119
+ entityId: generatedSpatialEntityId(generatorId, key),
120
+ origin: { kind: "generated", generatorId, key },
121
+ }, `generator output ${key}`)
122
+ const assetId = generatedAssetReference(entity)
123
+ if (assetId !== undefined) {
124
+ throw new SpatialSceneError("invalid-data", `Generated entity "${key}" references ${assetId}; generated entities cannot reference assets in this version.`, "generator output")
125
+ }
126
+ entities.push(entity)
127
+ }
128
+ const editableKeys = result.editableKeys ?? []
129
+ const declared = new Set<string>()
130
+ for (const editable of editableKeys) {
131
+ if (declared.has(editable.key)) throw new SpatialSceneError("invalid-data", `Duplicate editable key "${editable.key}".`, "generator editableKeys")
132
+ declared.add(editable.key)
133
+ if (!seen.has(editable.key)) {
134
+ throw new SpatialSceneError("invalid-data", `Editable key "${editable.key}" does not match any produced entity key.`, "generator editableKeys")
135
+ }
136
+ }
137
+ return deepFreezeJson({ entities, editableKeys })
138
+ }
139
+
140
+ /** Builds the retained attempt record; outputSha256 is recomputed from stamped entities. */
141
+ export function buildSpatialGeneratorRecord(options: {
142
+ readonly generatorId: string
143
+ readonly sourceSha256: string
144
+ readonly closureSha256: string
145
+ readonly parametersSha256: string
146
+ readonly seed: number
147
+ readonly runtimeSha256: string
148
+ readonly entities: readonly SpatialEntity[]
149
+ readonly editableKeys: SpatialGenerator["editableKeys"]
150
+ /** Sorted ids of generator-owned assets (generated payloads and derived facts manifests). */
151
+ readonly assets?: readonly string[]
152
+ }): SpatialGenerator {
153
+ const outputSha256 = spatialGeneratorOutputSha256(options.entities)
154
+ const attemptId = spatialGeneratorAttemptId({
155
+ generatorId: options.generatorId,
156
+ sourceSha256: options.sourceSha256,
157
+ parametersSha256: options.parametersSha256,
158
+ runtimeSha256: options.runtimeSha256,
159
+ outputSha256,
160
+ seed: options.seed,
161
+ })
162
+ return parseSpatialValue(SpatialGeneratorSchema, {
163
+ generatorId: options.generatorId,
164
+ sourceSha256: options.sourceSha256,
165
+ closureSha256: options.closureSha256,
166
+ parametersSha256: options.parametersSha256,
167
+ seed: options.seed,
168
+ outputSha256,
169
+ execution: { kind: "attempt", attemptId, runtimeSha256: options.runtimeSha256 },
170
+ editableKeys: options.editableKeys,
171
+ ...(options.assets === undefined ? {} : { assets: [...options.assets].sort() }),
172
+ }, "generator")
173
+ }
174
+
175
+ /** A minimal editable scene: one calibrated default camera, no authored entities. */
176
+ export function createSpatialGeneratorSceneShell(): unknown {
177
+ return {
178
+ kind: "slopcamera.spatial-scene", schemaVersion: 1, sceneId: "scene_generated",
179
+ coordinates: "right-handed-y-up-meters", durationUs: 4_000_000,
180
+ entities: [],
181
+ cameras: [{ cameraId: "camera_main", name: "Main", pose: { position: [0, 3, 8], rotation: [0, 0, 0, 1] },
182
+ projection: { kind: "perspective", width: 960, height: 540, fx: 650, fy: 650, cx: 480, cy: 270, near: 0.1, far: 200 } }],
183
+ assets: [], animations: [], generators: [], overrides: [],
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Applies replace-generator-output semantics to a whole scene: retained output
189
+ * for this generatorId is removed wholesale, the stamped entities and the new
190
+ * generator record are installed, and the merged document is revalidated end to
191
+ * end. Other generators' retained output and all authored content are preserved.
192
+ */
193
+ export function mergeSpatialGeneratorOutput(scene: SpatialSceneV1 | undefined, generator: SpatialGenerator, entities: readonly SpatialEntity[], assets: readonly SpatialAssetManifest[] = []): SpatialSceneV1 {
194
+ const base = scene ?? (parseSpatialScene(createSpatialGeneratorSceneShell()) as SpatialSceneV1)
195
+ const previous = base.generators.find(record => record.generatorId === generator.generatorId)?.assets ?? []
196
+ if ([...assets.map(asset => asset.assetId)].sort().join("") !== [...(generator.assets ?? [])].sort().join("")) {
197
+ throw new SpatialSceneError("conflict", "Generator output must carry exactly the asset manifests its record declares.", "assets")
198
+ }
199
+ for (const asset of assets) {
200
+ if (asset.provenance.source !== "generated" && asset.provenance.source !== "derived") {
201
+ throw new SpatialSceneError("conflict", "Generator-owned assets require generated or derived provenance.", "assets")
202
+ }
203
+ }
204
+ const retired = new Set(previous)
205
+ const keptAssets = base.assets.filter(asset => !retired.has(asset.assetId))
206
+ const keptAssetIds = new Set([...keptAssets.map(asset => asset.assetId), ...assets.map(asset => asset.assetId)])
207
+ const colliding = assets.find(asset => base.assets.some(existing => existing.assetId === asset.assetId) && !retired.has(asset.assetId))
208
+ if (colliding !== undefined) throw new SpatialSceneError("conflict", `Generator asset ${colliding.assetId} collides with an existing asset.`, "assets")
209
+ const retained = new Set(entities.map(entity => entity.entityId))
210
+ const removed = new Set<string>()
211
+ const kept: SpatialEntity[] = []
212
+ for (const entity of base.entities) {
213
+ if (entity.origin.kind === "generated" && entity.origin.generatorId === generator.generatorId) {
214
+ if (!retained.has(entity.entityId)) removed.add(entity.entityId)
215
+ continue
216
+ }
217
+ kept.push(entity)
218
+ }
219
+ for (const override of base.overrides) {
220
+ if (removed.has(override.entityId)) {
221
+ throw new SpatialSceneError("conflict", `Regeneration removed ${override.entityId}; remove its ${override.property} override or restore the produced key.`, "overrides")
222
+ }
223
+ }
224
+ for (const channel of base.animations) {
225
+ if (removed.has(channel.targetId)) {
226
+ throw new SpatialSceneError("conflict", `Regeneration removed ${channel.targetId}; remove channel ${channel.channelId} or restore the produced key.`, "animations")
227
+ }
228
+ }
229
+ const keptIds = new Set(kept.map(entity => entity.entityId))
230
+ for (const entity of entities) {
231
+ if (keptIds.has(entity.entityId)) throw new SpatialSceneError("conflict", `Generator output collides with ${entity.entityId}.`, "entities")
232
+ const assetId = generatedAssetReference(entity)
233
+ if (assetId !== undefined && !keptAssetIds.has(assetId)) {
234
+ throw new SpatialSceneError("conflict", `Generator output references missing asset ${assetId}.`, "entities")
235
+ }
236
+ }
237
+ const generators = [...base.generators.filter(record => record.generatorId !== generator.generatorId), generator]
238
+ return parseSpatialScene({ ...base, assets: [...keptAssets, ...assets], entities: [...kept, ...entities], generators })
239
+ }
@@ -0,0 +1,165 @@
1
+ import { z } from "zod"
2
+ import { createBoundedJsonValueSnapshot, deepFreezeJson } from "../code/json-snapshot.js"
3
+ import { canonicalJson } from "../code/canonical-json.js"
4
+ import { createSha256HexHasher } from "../code/sha256.js"
5
+ import { SpatialDigestSchema } from "./contracts.js"
6
+ import { parseSpatialValue, SpatialSceneError, spatialValueSha256 } from "./identity.js"
7
+ import { SPATIAL_GLB_LIMITS } from "./gltf.js"
8
+
9
+ /**
10
+ * Retained-native adapter contracts for heavy geometry operations (mesh repair,
11
+ * complex CSG beyond the portable hull bound, UV atlasing, decimation,
12
+ * hull decomposition and GLB emission). The portable layer owns only the
13
+ * typed request/receipt documents and their binding rules: every receipt must
14
+ * name its request's content-derived identity and copy its operation, engine
15
+ * and profile, and declared output digests are verified against real bytes
16
+ * before any artifact may be published into a scene. No native execution lives
17
+ * here — hosts run engines; this module also provides a deterministic fake
18
+ * adapter so tests exercise the full contract without native dependencies.
19
+ */
20
+ export const SPATIAL_GEOMETRY_NATIVE_LIMITS = Object.freeze({
21
+ inputs: 8,
22
+ inputBytes: SPATIAL_GLB_LIMITS.bytes,
23
+ outputs: 16,
24
+ outputBytes: 33_554_432,
25
+ parametersBytes: 4_096,
26
+ parametersDepth: 8,
27
+ parametersValues: 256,
28
+ })
29
+
30
+ export const SPATIAL_GEOMETRY_NATIVE_OPERATIONS = [
31
+ "mesh-repair", "complex-csg", "uv-atlas", "decimation", "hull-decomposition", "glb-emission",
32
+ ] as const
33
+ export type SpatialGeometryNativeOperation = (typeof SPATIAL_GEOMETRY_NATIVE_OPERATIONS)[number]
34
+
35
+ const operation = z.enum(SPATIAL_GEOMETRY_NATIVE_OPERATIONS)
36
+ const engine = z.strictObject({
37
+ engine: z.string().min(1).max(64),
38
+ version: z.string().min(1).max(64),
39
+ device: z.string().min(1).max(64).optional(),
40
+ })
41
+ const digestEntry = z.strictObject({ sha256: SpatialDigestSchema, bytes: z.number().int().safe().min(1).max(SPATIAL_GEOMETRY_NATIVE_LIMITS.outputBytes) })
42
+
43
+ export const SpatialGeometryNativeRequestSchema = z.strictObject({
44
+ kind: z.literal("slopcamera.spatial-geometry-native-request"),
45
+ schemaVersion: z.literal(1),
46
+ operation,
47
+ engine,
48
+ /** Closed adapter profile for this operation, e.g. slopcamera.geometry-native-csg-v1. */
49
+ profile: z.string().min(1).max(128),
50
+ /** Sorted digests of the exact input payloads the engine consumed. */
51
+ inputs: z.array(digestEntry).max(SPATIAL_GEOMETRY_NATIVE_LIMITS.inputs),
52
+ /** Bounded plain-JSON operation parameters; captured canonically. */
53
+ parameters: z.unknown().optional(),
54
+ })
55
+ export type SpatialGeometryNativeRequest = Readonly<z.infer<typeof SpatialGeometryNativeRequestSchema>>
56
+
57
+ export const SpatialGeometryNativeReceiptSchema = z.strictObject({
58
+ kind: z.literal("slopcamera.spatial-geometry-native-receipt"),
59
+ schemaVersion: z.literal(1),
60
+ requestId: z.string().regex(/^native_[a-f0-9]{32}$/u),
61
+ requestSha256: SpatialDigestSchema,
62
+ operation,
63
+ engine,
64
+ profile: z.string().min(1).max(128),
65
+ state: z.enum(["succeeded", "failed"]),
66
+ /** Sorted declared output digests; publication verifies real bytes against them. */
67
+ outputs: z.array(digestEntry).max(SPATIAL_GEOMETRY_NATIVE_LIMITS.outputs),
68
+ failure: z.strictObject({ code: z.enum(["validation", "engine", "budget", "custody"]), message: z.string().min(1).max(2_048) }).optional(),
69
+ }).superRefine((receipt, context) => {
70
+ if (receipt.state === "succeeded" && receipt.failure !== undefined) context.addIssue({ code: "custom", path: ["failure"], message: "Successful receipts carry no failure." })
71
+ if (receipt.state === "failed" && receipt.failure === undefined) context.addIssue({ code: "custom", path: ["failure"], message: "Failed receipts require a failure reason." })
72
+ if (receipt.outputs.reduce((sum, output) => sum + output.bytes, 0) > SPATIAL_GEOMETRY_NATIVE_LIMITS.outputBytes) context.addIssue({ code: "custom", path: ["outputs"], message: "Receipt exceeds the output byte bound." })
73
+ })
74
+ export type SpatialGeometryNativeReceipt = Readonly<z.infer<typeof SpatialGeometryNativeReceiptSchema>>
75
+
76
+ function nativeFail(message: string, path = "geometry-native"): never {
77
+ throw new SpatialSceneError("invalid-data", `slopcamera.spatial-geometry-native-v1: ${message}`, path)
78
+ }
79
+
80
+ /** Parses a foreign request: strict schema plus canonical bounded parameter capture. */
81
+ export function parseSpatialGeometryNativeRequest(input: unknown): SpatialGeometryNativeRequest {
82
+ const request = parseSpatialValue(SpatialGeometryNativeRequestSchema, input, "geometry-native request")
83
+ if (request.inputs.length !== new Set(request.inputs.map(item => item.sha256)).size) nativeFail("Request inputs must carry distinct digests.", "request.inputs")
84
+ const parameters = request.parameters === undefined ? undefined
85
+ : createBoundedJsonValueSnapshot(request.parameters, SPATIAL_GEOMETRY_NATIVE_LIMITS.parametersBytes, "geometry-native parameters", {
86
+ maximumDepth: SPATIAL_GEOMETRY_NATIVE_LIMITS.parametersDepth, maximumValues: SPATIAL_GEOMETRY_NATIVE_LIMITS.parametersValues,
87
+ }).value
88
+ return deepFreezeJson({ ...request, ...(parameters === undefined ? {} : { parameters }) })
89
+ }
90
+
91
+ /** Content-derived request identity and digest; identical requests reproduce both. */
92
+ export function spatialGeometryNativeRequestSha256(request: unknown): string {
93
+ return spatialValueSha256({ domain: "slopcamera.spatial-geometry-native-request.v1", request: parseSpatialGeometryNativeRequest(request) })
94
+ }
95
+ export function spatialGeometryNativeRequestId(request: unknown): string {
96
+ return `native_${spatialGeometryNativeRequestSha256(request).slice(0, 32)}`
97
+ }
98
+ export function spatialGeometryNativeReceiptSha256(receipt: unknown): string {
99
+ return spatialValueSha256({ domain: "slopcamera.spatial-geometry-native-receipt.v1", receipt: parseSpatialValue(SpatialGeometryNativeReceiptSchema, receipt, "geometry-native receipt") })
100
+ }
101
+
102
+ /**
103
+ * Validates that a receipt binds its exact request: operation, engine, profile,
104
+ * requestId and requestSha256 must match, mirroring native-admission plan/receipt
105
+ * reconciliation. Any mismatch is a typed rejection.
106
+ */
107
+ export function validateSpatialGeometryNativeReceipt(input: { readonly request: unknown; readonly receipt: unknown }): SpatialGeometryNativeReceipt {
108
+ const request = parseSpatialGeometryNativeRequest(input.request)
109
+ const receipt = parseSpatialValue(SpatialGeometryNativeReceiptSchema, input.receipt, "geometry-native receipt")
110
+ if (receipt.requestSha256 !== spatialGeometryNativeRequestSha256(request) || receipt.requestId !== spatialGeometryNativeRequestId(request)) {
111
+ nativeFail("Receipt identity differs from the exact request it claims.", "receipt.requestSha256")
112
+ }
113
+ if (receipt.operation !== request.operation || receipt.profile !== request.profile || canonicalJson(receipt.engine) !== canonicalJson(request.engine)) {
114
+ nativeFail("Receipt operation, profile, or engine differs from its request.", "receipt.operation")
115
+ }
116
+ return deepFreezeJson(receipt)
117
+ }
118
+
119
+ /**
120
+ * Verifies declared output digests against real retained bytes BEFORE any
121
+ * manifest or facts document may publish them. A mismatch is a typed error.
122
+ */
123
+ export function verifySpatialGeometryNativeOutputs(receipt: unknown, outputs: readonly Uint8Array[]): readonly { sha256: string; bytes: number }[] {
124
+ const parsed = parseSpatialValue(SpatialGeometryNativeReceiptSchema, receipt, "geometry-native receipt")
125
+ if (parsed.state !== "succeeded") nativeFail("Only succeeded receipts publish retained outputs.", "receipt.state")
126
+ const declared = parsed.outputs
127
+ if (outputs.length !== declared.length) nativeFail("Retained output count differs from the receipt declaration.", "receipt.outputs")
128
+ const verified = declared.map((output, index) => {
129
+ const bytes = outputs[index]!
130
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength !== output.bytes) nativeFail(`Native output ${index} byte length differs from its receipt.`, `receipt.outputs.${index}`)
131
+ const hasher = createSha256HexHasher(); hasher.update(bytes); const sha256 = hasher.digestHex()
132
+ if (sha256 !== output.sha256) nativeFail(`Native output ${index} digest mismatch; refusing publication.`, `receipt.outputs.${index}`)
133
+ return { sha256, bytes: output.bytes }
134
+ })
135
+ return deepFreezeJson(verified)
136
+ }
137
+
138
+ /**
139
+ * Deterministic fake adapter for tests and hosts without a native engine: the
140
+ * output stream derives entirely from the request digest, so identical inputs
141
+ * always produce byte-identical artifacts and receipts.
142
+ */
143
+ export function executeSpatialGeometryNativeFake(input: unknown): { readonly receipt: SpatialGeometryNativeReceipt; readonly outputs: readonly Uint8Array[] } {
144
+ const request = parseSpatialGeometryNativeRequest(input)
145
+ const requestSha256 = spatialGeometryNativeRequestSha256(request)
146
+ const count = request.operation === "hull-decomposition" ? 4 : 1
147
+ const outputs: Uint8Array[] = []
148
+ const declared: { sha256: string; bytes: number }[] = []
149
+ for (let index = 0; index < count; index++) {
150
+ const seed = spatialValueSha256({ domain: "slopcamera.spatial-geometry-native-output.v1", requestSha256, index })
151
+ const length = 256 + (Number.parseInt(seed.slice(0, 4), 16) % 256)
152
+ const bytes = new Uint8Array(length)
153
+ for (let offset = 0; offset < length; offset++) bytes[offset] = Number.parseInt(seed.slice((offset % 32) * 2, (offset % 32) * 2 + 2), 16)
154
+ const hasher = createSha256HexHasher(); hasher.update(bytes); const sha256 = hasher.digestHex()
155
+ outputs.push(bytes)
156
+ declared.push({ sha256, bytes: length })
157
+ }
158
+ const receipt = parseSpatialValue(SpatialGeometryNativeReceiptSchema, {
159
+ kind: "slopcamera.spatial-geometry-native-receipt", schemaVersion: 1,
160
+ requestId: spatialGeometryNativeRequestId(request), requestSha256,
161
+ operation: request.operation, engine: request.engine, profile: request.profile,
162
+ state: "succeeded", outputs: declared,
163
+ }, "geometry-native receipt")
164
+ return deepFreezeJson({ receipt, outputs: Object.freeze(outputs) })
165
+ }