@hraness/slopcamera 3.2.7 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +1 -7
- package/PRIVACY.md +16 -7
- package/README.md +65 -20
- package/apps/desktop/README.md +14 -130
- package/apps/desktop/application/context.ts +6 -9
- package/apps/desktop/application/default-registry.ts +26 -10
- package/apps/desktop/application/operation.ts +14 -5
- package/apps/desktop/application/operations/index.ts +4 -1
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +528 -97
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +31 -2
- package/apps/desktop/cli/commands.ts +1215 -70
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +183 -23
- package/apps/desktop/cli/html-scene.ts +139 -28
- package/apps/desktop/cli/io.ts +2 -1
- package/apps/desktop/cli/main.ts +26 -50
- package/apps/desktop/cli/menubar.ts +244 -0
- package/apps/desktop/cli/music-analysis-service.ts +61 -0
- package/apps/desktop/cli/portable-surface.ts +18 -5
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +268 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/application-node-planner.ts +0 -47
- package/apps/desktop/code/host-source-layout.ts +46 -0
- package/apps/desktop/code/public.ts +0 -1
- package/apps/desktop/code/runtime-identity.ts +50 -8
- package/apps/desktop/code/semantic-builder.ts +24 -43
- package/apps/desktop/code/source-bundle.ts +23 -6
- package/apps/desktop/code/source-typecheck.ts +131 -43
- package/apps/desktop/code/worker-client.ts +14 -2
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/music-analysis.ts +124 -1
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +737 -291
- package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
- package/apps/desktop/html-overlay/index.ts +9 -0
- package/apps/desktop/html-overlay/runtime.ts +2 -0
- package/apps/desktop/html-overlay/scene.ts +29 -0
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +32 -434
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-cgpkyjqa.js +3 -0
- package/dist/index-f0ptv8dm.js +7 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index.js +1 -240
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +1 -1
- package/package.json +11 -24
- package/skills/slopcamera/SKILL.md +14 -1
- package/skills/slopcamera/references/diagrams.md +5 -14
- package/skills/slopcamera/references/directed-scenes.md +22 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +2 -2
- package/skills/slopcamera/references/music-video.md +23 -3
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/scene-building.md +32 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/support.md +25 -0
- package/skills/slopcamera/references/web-media-excerpts.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +250 -102
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -23
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1642 -130
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +233 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +109 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +618 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/support-completion.ts +16 -0
- package/src/support.ts +42 -0
- package/src/version.ts +1 -1
- package/apps/desktop/application/operations/recording/index.ts +0 -5
- package/apps/desktop/application/operations/recording/pause.ts +0 -34
- package/apps/desktop/application/operations/recording/resume.ts +0 -34
- package/apps/desktop/application/operations/recording/shared.ts +0 -199
- package/apps/desktop/application/operations/recording/start.ts +0 -169
- package/apps/desktop/application/operations/recording/stop.ts +0 -34
- package/apps/desktop/capture/protocol.ts +0 -689
- package/apps/desktop/cli/capture-bundle.ts +0 -1117
- package/apps/desktop/cli/recording-controller.ts +0 -1567
- package/apps/desktop/cli/recording-daemon.ts +0 -931
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-mcy8z0br.js +0 -192
- package/dist/index-p63wavx0.js +0 -2029
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-rvghzt7b.js +0 -2013
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-z7239b4h.js +0 -2363
- package/dist/skill-install-dh0ntqba.js +0 -10
- package/src/desktop.ts +0 -253
|
@@ -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
|
+
}
|