@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,692 @@
1
+ import { z } from "zod"
2
+ import { createBoundedJsonValueSnapshot, deepFreezeJson } from "../code/json-snapshot.js"
3
+ import {
4
+ SPATIAL_SCENE_LIMITS, SpatialAssetIdSchema, SpatialCameraIdSchema, SpatialDigestSchema,
5
+ SpatialEntityIdSchema, SpatialPlacementSchema, SpatialSceneIdSchema, SpatialTimeUsSchema,
6
+ type SpatialEntity,
7
+ } from "./contracts.js"
8
+ import { auditSpatialSceneInContext, SPATIAL_AUDIT_LIMITS, SpatialAuditBoundsSchema } from "./audit.js"
9
+ import { createSpatialEvaluationContext, type SpatialEvaluationContext } from "./evaluate.js"
10
+ import {
11
+ parseSpatialValue, sortSpatialBy, SpatialSceneError,
12
+ } from "./identity.js"
13
+ import type { Bounds } from "./math.js"
14
+
15
+ export const SPATIAL_RENDERED_AUDIT_LIMITS = Object.freeze({
16
+ /** Decoded object-ID frames per audit; the geometric audit owns sample cadence. */
17
+ samples: SPATIAL_AUDIT_LIMITS.samples,
18
+ /** Retained findings; any excess is counted in `omittedFindings`. */
19
+ findings: SPATIAL_AUDIT_LIMITS.findings,
20
+ /** Joint bound on entities × frames so reports stay bounded. */
21
+ entitySamples: SPATIAL_AUDIT_LIMITS.entitySamples,
22
+ /** Canonical report byte bound checked before return. */
23
+ reportBytes: SPATIAL_AUDIT_LIMITS.reportBytes,
24
+ /** Selection codes occupy [1, 4096] in the RGB big-endian uint24 pass. */
25
+ selectionIds: 4_096,
26
+ /** A single decoded object-ID frame stays inside the renderer's pixel budget. */
27
+ frameDimension: 8_192,
28
+ framePixels: 33_554_432,
29
+ })
30
+
31
+ const ENTITY_KINDS = ["group", "mesh", "image", "diagram", "video", "text", "light", "splat", "environment"] as const
32
+ const ELIGIBILITY = ["renderable", "proxy-coverage", "view-masked", "no-surface", "unsupported-kind"] as const
33
+ const BOUNDS_UNKNOWN_REASONS = ["requires-asset-decoding", "requires-text-layout", "no-surface"] as const
34
+ const FINDING_KINDS = [
35
+ "never-rendered", "unsupported-kind", "proxy-coverage", "occluded", "unattributed-pixels", "empty-render", "bounds-unknown",
36
+ ] as const
37
+ const SAMPLE_NOTES = ["out-of-range", "other-camera", "unlowered-expected"] as const
38
+
39
+ /**
40
+ * The lowering-evidence representation label for a splat lowered as its
41
+ * bounding-box proxy. The host derives the box from the prepared splat's
42
+ * decoded position bounds, so the emitted pixels are approximate coverage —
43
+ * never splat pixel truth. The string is part of the lowering↔audit contract;
44
+ * frame evidence uses it so this analyzer can tell an honest proxy from a
45
+ * surface that must not claim splat attribution.
46
+ */
47
+ export const SPATIAL_SPLAT_PROXY_REPRESENTATION =
48
+ "splat-bounding-box-proxy;spz-position-bounds;approximate-not-pixel-truth"
49
+
50
+ /**
51
+ * The coverage policy rendered by the host. Mirrored from the overlay batch
52
+ * contract rather than imported so this boundary stays portable: alpha-threshold
53
+ * admits every surface whose effective alpha reaches the threshold; opaque
54
+ * requires exactly opaque materials and textures.
55
+ */
56
+ export const SpatialRenderedAuditCoverageSchema = z.discriminatedUnion("kind", [
57
+ z.strictObject({ kind: z.literal("opaque") }),
58
+ z.strictObject({ kind: z.literal("alpha-threshold"), threshold: z.number().finite().gt(0).max(1) }),
59
+ ])
60
+ export type SpatialRenderedAuditCoverage = z.infer<typeof SpatialRenderedAuditCoverageSchema>
61
+ /** The default audit pass counts a pixel when effective alpha reaches one half. */
62
+ export const SPATIAL_RENDERED_AUDIT_COVERAGE: SpatialRenderedAuditCoverage = Object.freeze({ kind: "alpha-threshold", threshold: 0.5 })
63
+
64
+ const selectionIdSchema = z.number().int().min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.selectionIds)
65
+ const selectionKeyPattern = /^(?:0|[1-9]\d{0,3})$/u
66
+ const pixelCountSchema = z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels)
67
+
68
+ /** One entity-to-selection evidence row from the rendered batch metadata. */
69
+ export const SpatialRenderedAuditObjectSchema = z.strictObject({
70
+ entityId: SpatialEntityIdSchema,
71
+ selectionId: selectionIdSchema,
72
+ representation: z.string().min(1).max(256),
73
+ placement: z.enum(["world", "view"]),
74
+ assetManifestSha256: SpatialDigestSchema.optional(),
75
+ /** Lowered instance count; the object-ID pass attributes every instance to this entity's selection code. */
76
+ instances: z.number().int().min(1).max(SPATIAL_SCENE_LIMITS.entities).optional(),
77
+ })
78
+
79
+ /** One decoded object-ID frame: per-selection pixel counts plus lowering evidence. */
80
+ export const SpatialRenderedAuditFrameSchema = z.strictObject({
81
+ timeUs: SpatialTimeUsSchema,
82
+ width: z.number().int().min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension),
83
+ height: z.number().int().min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension),
84
+ /** Digest of the exact rendered bytes these counts were decoded from. */
85
+ pngSha256: SpatialDigestSchema,
86
+ counts: z.record(z.string().regex(selectionKeyPattern), pixelCountSchema),
87
+ objects: z.array(SpatialRenderedAuditObjectSchema).max(SPATIAL_SCENE_LIMITS.entities),
88
+ }).superRefine((frame, context) => {
89
+ const pixels = frame.width * frame.height
90
+ if (pixels > SPATIAL_RENDERED_AUDIT_LIMITS.framePixels) {
91
+ context.addIssue({ code: "custom", message: "Object-ID frame exceeds the pixel budget." })
92
+ }
93
+ let total = 0
94
+ for (const [key, count] of Object.entries(frame.counts)) {
95
+ const code = Number(key)
96
+ if (!Number.isInteger(code) || code < 1 || code > SPATIAL_RENDERED_AUDIT_LIMITS.selectionIds) {
97
+ context.addIssue({ code: "custom", message: "Object-ID counts must key selection codes in [1,4096]." })
98
+ }
99
+ total += count
100
+ }
101
+ if (total > pixels) {
102
+ context.addIssue({ code: "custom", message: "Object-ID counts exceed the frame pixel count." })
103
+ }
104
+ const entityIds = new Set<string>(), selectionIds = new Set<number>()
105
+ for (const object of frame.objects) {
106
+ if (entityIds.has(object.entityId) || selectionIds.has(object.selectionId)) {
107
+ context.addIssue({ code: "custom", message: "Frame evidence must name each entity and selection code once." })
108
+ }
109
+ entityIds.add(object.entityId); selectionIds.add(object.selectionId)
110
+ }
111
+ })
112
+
113
+ export const SpatialRenderedAuditSampleSchema = z.strictObject({
114
+ timeUs: SpatialTimeUsSchema,
115
+ /** The entity was eligible and effectively visible under the audited camera. */
116
+ expected: z.boolean(),
117
+ /** The renderer lowered the entity into this frame's object-ID pass. */
118
+ lowered: z.boolean(),
119
+ /** At least one frame pixel was attributed to this entity. */
120
+ rendered: z.boolean(),
121
+ pixels: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels),
122
+ framePercent: z.number().finite().min(0).max(100),
123
+ /** Geometric corner-projection estimate in px² when bounds exist. */
124
+ geometricPixels: z.number().finite().min(0).max(1e15).optional(),
125
+ /** rendered pixels ÷ geometricPixels; absent when the estimate is zero. */
126
+ coverageRatio: z.number().finite().min(0).max(1e15).optional(),
127
+ note: z.enum(SAMPLE_NOTES).optional(),
128
+ })
129
+ export const SpatialRenderedAuditEntitySchema = z.strictObject({
130
+ entityId: SpatialEntityIdSchema,
131
+ name: z.string().min(1).max(256),
132
+ kind: z.enum(ENTITY_KINDS),
133
+ placement: SpatialPlacementSchema,
134
+ /** Whether the object-ID pass can attribute pixels to this entity at all. */
135
+ eligibility: z.enum(ELIGIBILITY),
136
+ selectionId: selectionIdSchema,
137
+ enclosure: z.discriminatedUnion("status", [
138
+ z.strictObject({ status: z.literal("bounded") }),
139
+ z.strictObject({ status: z.literal("unknown"), reason: z.enum(BOUNDS_UNKNOWN_REASONS) }),
140
+ ]),
141
+ /** Present on mesh entities only when local-space instances are declared. */
142
+ instances: z.number().int().min(1).max(SPATIAL_SCENE_LIMITS.entities).optional(),
143
+ samples: z.array(SpatialRenderedAuditSampleSchema).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
144
+ totals: z.strictObject({
145
+ expected: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
146
+ lowered: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
147
+ rendered: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
148
+ pixels: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels * SPATIAL_RENDERED_AUDIT_LIMITS.samples),
149
+ maxPixels: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels),
150
+ maxFramePercent: z.number().finite().min(0).max(100),
151
+ }),
152
+ })
153
+ export const SpatialRenderedAuditFrameReportSchema = z.strictObject({
154
+ timeUs: SpatialTimeUsSchema,
155
+ pngSha256: SpatialDigestSchema,
156
+ renderedPixels: pixelCountSchema,
157
+ /** Pixels carrying selection codes absent from the frame's attributable evidence. */
158
+ unattributedPixels: pixelCountSchema,
159
+ loweredEntities: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
160
+ })
161
+ export const SpatialRenderedAuditFindingSchema = z.strictObject({
162
+ severity: z.enum(["info", "warning"]),
163
+ kind: z.enum(FINDING_KINDS),
164
+ entityId: SpatialEntityIdSchema.optional(),
165
+ timeUs: SpatialTimeUsSchema.optional(),
166
+ detail: z.string().min(1).max(1_024),
167
+ })
168
+ export const SpatialRenderedAuditReportSchema = z.strictObject({
169
+ kind: z.literal("slopcamera.spatial-rendered-audit"),
170
+ schemaVersion: z.literal(1),
171
+ sceneId: SpatialSceneIdSchema,
172
+ sceneSha256: SpatialDigestSchema,
173
+ cameraId: SpatialCameraIdSchema,
174
+ durationUs: SpatialTimeUsSchema,
175
+ /** Ascending unique sample times, one decoded frame each. */
176
+ timesUs: z.array(SpatialTimeUsSchema).min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
177
+ mode: z.strictObject({ kind: z.literal("object-id"), coverage: SpatialRenderedAuditCoverageSchema }),
178
+ frame: z.strictObject({
179
+ width: z.number().int().min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension),
180
+ height: z.number().int().min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension),
181
+ pixels: pixelCountSchema,
182
+ }),
183
+ summary: z.strictObject({
184
+ entities: z.strictObject({
185
+ total: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
186
+ renderable: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
187
+ /** Splats lowered as bounding-box proxies — approximate coverage, never splat pixel truth. */
188
+ proxyCoverage: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities).optional(),
189
+ viewMasked: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
190
+ noSurface: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
191
+ unsupported: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
192
+ }),
193
+ /** Authored-visible renderable or view entities that produced zero attributed pixels. */
194
+ entitiesNeverRendered: z.array(SpatialEntityIdSchema).max(SPATIAL_SCENE_LIMITS.entities),
195
+ entitiesUnsupported: z.array(SpatialEntityIdSchema).max(SPATIAL_SCENE_LIMITS.entities),
196
+ entitiesViewMasked: z.array(SpatialEntityIdSchema).max(SPATIAL_SCENE_LIMITS.entities),
197
+ entitiesProxyCoverage: z.array(SpatialEntityIdSchema).max(SPATIAL_SCENE_LIMITS.entities).optional(),
198
+ renderedPixels: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels * SPATIAL_RENDERED_AUDIT_LIMITS.samples),
199
+ unattributedPixels: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels * SPATIAL_RENDERED_AUDIT_LIMITS.samples),
200
+ }),
201
+ entities: z.array(SpatialRenderedAuditEntitySchema).max(SPATIAL_SCENE_LIMITS.entities),
202
+ frames: z.array(SpatialRenderedAuditFrameReportSchema).min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
203
+ findings: z.array(SpatialRenderedAuditFindingSchema).max(SPATIAL_RENDERED_AUDIT_LIMITS.findings),
204
+ omittedFindings: z.number().int().min(0),
205
+ })
206
+
207
+ /**
208
+ * Structural report types mirror the schemas above rather than inferring from
209
+ * them, following `audit.ts`: recursive readonly mappers over the strict-object
210
+ * graph measurably slow every authored-source typecheck that reaches this
211
+ * module through the `./code` surface.
212
+ */
213
+ export interface SpatialRenderedAuditObject {
214
+ readonly entityId: string
215
+ readonly selectionId: number
216
+ readonly representation: string
217
+ readonly placement: "world" | "view"
218
+ readonly assetManifestSha256?: string | undefined
219
+ readonly instances?: number | undefined
220
+ }
221
+ export interface SpatialRenderedAuditFrame {
222
+ readonly timeUs: number
223
+ readonly width: number
224
+ readonly height: number
225
+ readonly pngSha256: string
226
+ readonly counts: Readonly<Record<string, number>>
227
+ readonly objects: readonly SpatialRenderedAuditObject[]
228
+ }
229
+ export interface SpatialRenderedAuditSample {
230
+ readonly timeUs: number
231
+ readonly expected: boolean
232
+ readonly lowered: boolean
233
+ readonly rendered: boolean
234
+ readonly pixels: number
235
+ readonly framePercent: number
236
+ readonly geometricPixels?: number | undefined
237
+ readonly coverageRatio?: number | undefined
238
+ readonly note?: (typeof SAMPLE_NOTES)[number] | undefined
239
+ }
240
+ export interface SpatialRenderedAuditEntity {
241
+ readonly entityId: string
242
+ readonly name: string
243
+ readonly kind: (typeof ENTITY_KINDS)[number]
244
+ readonly placement: SpatialEntity["placement"]
245
+ readonly eligibility: (typeof ELIGIBILITY)[number]
246
+ readonly selectionId: number
247
+ readonly enclosure:
248
+ | { readonly status: "bounded" }
249
+ | { readonly status: "unknown"; readonly reason: (typeof BOUNDS_UNKNOWN_REASONS)[number] }
250
+ readonly instances?: number | undefined
251
+ readonly samples: readonly SpatialRenderedAuditSample[]
252
+ readonly totals: {
253
+ readonly expected: number
254
+ readonly lowered: number
255
+ readonly rendered: number
256
+ readonly pixels: number
257
+ readonly maxPixels: number
258
+ readonly maxFramePercent: number
259
+ }
260
+ }
261
+ export interface SpatialRenderedAuditFinding {
262
+ readonly severity: "info" | "warning"
263
+ readonly kind: (typeof FINDING_KINDS)[number]
264
+ readonly entityId?: string | undefined
265
+ readonly timeUs?: number | undefined
266
+ readonly detail: string
267
+ }
268
+ export interface SpatialRenderedAuditReport {
269
+ readonly kind: "slopcamera.spatial-rendered-audit"
270
+ readonly schemaVersion: 1
271
+ readonly sceneId: string
272
+ readonly sceneSha256: string
273
+ readonly cameraId: string
274
+ readonly durationUs: number
275
+ readonly timesUs: readonly number[]
276
+ readonly mode: { readonly kind: "object-id"; readonly coverage: SpatialRenderedAuditCoverage }
277
+ readonly frame: { readonly width: number; readonly height: number; readonly pixels: number }
278
+ readonly summary: {
279
+ readonly entities: {
280
+ readonly total: number
281
+ readonly renderable: number
282
+ readonly proxyCoverage?: number | undefined
283
+ readonly viewMasked: number
284
+ readonly noSurface: number
285
+ readonly unsupported: number
286
+ }
287
+ readonly entitiesNeverRendered: readonly string[]
288
+ readonly entitiesUnsupported: readonly string[]
289
+ readonly entitiesViewMasked: readonly string[]
290
+ readonly entitiesProxyCoverage?: readonly string[] | undefined
291
+ readonly renderedPixels: number
292
+ readonly unattributedPixels: number
293
+ }
294
+ readonly entities: readonly SpatialRenderedAuditEntity[]
295
+ readonly frames: readonly {
296
+ readonly timeUs: number
297
+ readonly pngSha256: string
298
+ readonly renderedPixels: number
299
+ readonly unattributedPixels: number
300
+ readonly loweredEntities: number
301
+ }[]
302
+ readonly findings: readonly SpatialRenderedAuditFinding[]
303
+ readonly omittedFindings: number
304
+ }
305
+
306
+ export const SpatialRenderedAuditOptionsSchema = z.strictObject({
307
+ cameraId: SpatialCameraIdSchema,
308
+ assetBounds: z.record(SpatialAssetIdSchema, SpatialAuditBoundsSchema).optional(),
309
+ /** Records which coverage policy produced the decoded frames. */
310
+ coverage: SpatialRenderedAuditCoverageSchema.optional(),
311
+ })
312
+ export interface SpatialRenderedAuditOptions {
313
+ readonly cameraId: string
314
+ readonly assetBounds?: Readonly<Record<string, Bounds>>
315
+ readonly coverage?: SpatialRenderedAuditCoverage
316
+ }
317
+
318
+ /**
319
+ * Decodes one binary-validity object-ID frame into per-selection pixel counts.
320
+ * No-hit is exactly [0,0,0,0]; every drawn pixel must carry alpha 255 and a
321
+ * selection code in [1,4096]. Anything else is not a qualified pass output.
322
+ */
323
+ export function decodeObjectIdPixels(rgba: Uint8Array, width: number, height: number): Record<string, number> {
324
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1
325
+ || width > SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension || height > SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension
326
+ || width * height > SPATIAL_RENDERED_AUDIT_LIMITS.framePixels) {
327
+ throw new RangeError("Object-ID frame dimensions exceed their bound.")
328
+ }
329
+ if (rgba.length !== width * height * 4) {
330
+ throw new RangeError("Object-ID pixels must be exactly width × height RGBA8.")
331
+ }
332
+ const counts: Record<string, number> = {}
333
+ for (let index = 0; index < rgba.length; index += 4) {
334
+ const red = rgba[index]!, green = rgba[index + 1]!, blue = rgba[index + 2]!, alpha = rgba[index + 3]!
335
+ if (alpha === 0) {
336
+ if (red !== 0 || green !== 0 || blue !== 0) {
337
+ throw new RangeError("Object-ID no-hit pixels must be exactly [0,0,0,0].")
338
+ }
339
+ continue
340
+ }
341
+ const code = red * 65_536 + green * 256 + blue
342
+ if (alpha !== 255 || code === 0 || code > SPATIAL_RENDERED_AUDIT_LIMITS.selectionIds) {
343
+ throw new RangeError("Object-ID pixels must be alpha-255 selection codes in [1,4096].")
344
+ }
345
+ counts[String(code)] = (counts[String(code)] ?? 0) + 1
346
+ }
347
+ return counts
348
+ }
349
+
350
+ const round3 = (value: number): number => Math.round(value * 1_000) / 1_000
351
+
352
+ function eligibility(entity: SpatialEntity, assetBounds: Readonly<Record<string, Bounds>>): (typeof ELIGIBILITY)[number] {
353
+ if (entity.kind === "splat") {
354
+ // A world-placed splat with supplied bounds lowers its bounding-box proxy
355
+ // in the object-ID pass — approximate coverage, never pixel truth. A view
356
+ // placement or absent bounds keeps the honest unsupported report.
357
+ return entity.placement.kind === "world" && assetBounds[entity.assetId] !== undefined
358
+ ? "proxy-coverage" : "unsupported-kind"
359
+ }
360
+ if (entity.kind === "group" || entity.kind === "light" || entity.kind === "environment") return "no-surface"
361
+ if (entity.placement.kind === "view") return "view-masked"
362
+ return "renderable"
363
+ }
364
+
365
+ /** The manifest-bearing asset an entity's lowered representation binds, if any. */
366
+ function entityAssetId(entity: SpatialEntity): string | undefined {
367
+ switch (entity.kind) {
368
+ case "mesh": return entity.geometry.kind === "asset" ? entity.geometry.assetId : entity.material.kind !== "pbr" ? entity.material.map : undefined
369
+ case "image": case "diagram": case "video": case "environment": return entity.assetId
370
+ case "text": return entity.fontAssetId
371
+ case "splat": return entity.assetId
372
+ default: return undefined
373
+ }
374
+ }
375
+
376
+ type Finding = SpatialRenderedAuditFinding
377
+ const findingOrder = (finding: Finding): string =>
378
+ `${finding.kind}:${finding.entityId ?? ""}:${String(finding.timeUs ?? -1).padStart(12, "0")}`
379
+
380
+ interface FrameDraft {
381
+ readonly timeUs: number
382
+ readonly width: number
383
+ readonly height: number
384
+ readonly pngSha256: string
385
+ /** selectionId → attributed entity pixels for every declared evidence code. */
386
+ readonly attributed: ReadonlyMap<number, number>
387
+ readonly lowered: ReadonlyMap<string, SpatialRenderedAuditObject>
388
+ readonly renderedPixels: number
389
+ readonly unattributedPixels: number
390
+ }
391
+
392
+ /**
393
+ * Reconciles decoded object-ID frames against per-frame lowering evidence and
394
+ * the geometric audit's expectations. Pure and deterministic: the renderer,
395
+ * decoding and file IO all live in the host; this module sees only counts,
396
+ * evidence rows, and the parsed scene. Unsupported kinds are reported, never
397
+ * estimated; a splat with supplied bounds reports `proxy-coverage` — the proxy
398
+ * attribution is real while its drawn shape stays an approximation.
399
+ */
400
+ export function auditSpatialSceneRendered(
401
+ sceneInput: unknown,
402
+ framesInput: unknown,
403
+ options: SpatialRenderedAuditOptions,
404
+ ): SpatialRenderedAuditReport {
405
+ return auditSpatialSceneRenderedInContext(createSpatialEvaluationContext(sceneInput), framesInput, options)
406
+ }
407
+
408
+ /**
409
+ * The rendered audit against a shared evaluation context. The embedded
410
+ * geometric audit consumes the same context, so one parse and index pass
411
+ * covers frame reconciliation and every geometric sample.
412
+ */
413
+ export function auditSpatialSceneRenderedInContext(
414
+ context: SpatialEvaluationContext,
415
+ framesInput: unknown,
416
+ options: SpatialRenderedAuditOptions,
417
+ ): SpatialRenderedAuditReport {
418
+ const scene = context.scene
419
+ const captured = parseSpatialValue(SpatialRenderedAuditOptionsSchema, options, "rendered audit options")
420
+ const coverage = captured.coverage ?? SPATIAL_RENDERED_AUDIT_COVERAGE
421
+ const cameraId = captured.cameraId
422
+ if (!context.camerasById.has(cameraId)) {
423
+ throw new SpatialSceneError("not-found", `Unknown camera ${cameraId}.`, "cameraId")
424
+ }
425
+ const assetIds = new Set(scene.assets.map(asset => asset.assetId))
426
+ for (const assetId of Object.keys(captured.assetBounds ?? {})) {
427
+ if (!assetIds.has(assetId)) throw new SpatialSceneError("invalid-data", `Asset bounds reference unknown ${assetId}.`, "assetBounds")
428
+ }
429
+ const frames = parseSpatialValue(
430
+ z.array(SpatialRenderedAuditFrameSchema).min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
431
+ framesInput, "rendered audit frames",
432
+ )
433
+ const timeSet = new Set(frames.map(frame => frame.timeUs))
434
+ if (timeSet.size !== frames.length) {
435
+ throw new SpatialSceneError("invalid-data", "Rendered audit frames must have unique sample times.", "frames")
436
+ }
437
+ const timesUs = [...timeSet].sort((a, b) => a - b)
438
+ for (const timeUs of timesUs) {
439
+ if (timeUs > scene.durationUs) throw new SpatialSceneError("invalid-data", "Rendered audit sample time exceeds scene duration.", "frames")
440
+ }
441
+ if (scene.entities.length * timesUs.length > SPATIAL_RENDERED_AUDIT_LIMITS.entitySamples) {
442
+ throw new SpatialSceneError("invalid-data", "Rendered audit entity-sample budget exceeded; pass fewer frames.", "frames")
443
+ }
444
+ const first = frames[0]!
445
+ for (const frame of frames) {
446
+ if (frame.width !== first.width || frame.height !== first.height) {
447
+ throw new SpatialSceneError("invalid-data", "Rendered audit frames must share one calibrated dimension.", "frames")
448
+ }
449
+ }
450
+ const framePixels = first.width * first.height
451
+
452
+ // Frame evidence must be internally consistent with the parsed scene: the
453
+ // selection code is the canonical entity index and an asset digest must match
454
+ // the declared manifest closure.
455
+ const entityIndex = new Map(scene.entities.map((entity, index) => [entity.entityId, index + 1]))
456
+ const manifestDigests = context.assetDigests
457
+ const frameDrafts = new Map<number, FrameDraft>()
458
+ for (const frame of frames) {
459
+ const attributed = new Map<number, number>()
460
+ const lowered = new Map<string, SpatialRenderedAuditObject>()
461
+ for (const object of frame.objects) {
462
+ const entity = scene.entities.find(candidate => candidate.entityId === object.entityId)
463
+ if (entity === undefined) {
464
+ throw new SpatialSceneError("invalid-data", `Frame evidence names unknown entity ${object.entityId}.`, "frames")
465
+ }
466
+ if (object.selectionId !== entityIndex.get(object.entityId)!) {
467
+ throw new SpatialSceneError("invalid-data", `Frame evidence selection id differs from the canonical index for ${object.entityId}.`, "frames")
468
+ }
469
+ if (object.assetManifestSha256 !== undefined) {
470
+ const assetId = entityAssetId(entity)
471
+ if (assetId === undefined || manifestDigests[assetId] !== object.assetManifestSha256) {
472
+ throw new SpatialSceneError("invalid-data", `Frame evidence asset digest does not match the declared manifest for ${object.entityId}.`, "frames")
473
+ }
474
+ }
475
+ if (entity.kind === "splat") {
476
+ // A lowered splat is exactly its supplied-bounds box under the normal
477
+ // selection code; any other representation must not claim attribution.
478
+ if (object.representation !== SPATIAL_SPLAT_PROXY_REPRESENTATION || object.placement !== "world"
479
+ || entity.placement.kind !== "world" || captured.assetBounds?.[entity.assetId] === undefined) {
480
+ throw new SpatialSceneError("invalid-data", `Frame evidence may lower splat ${object.entityId} only as its supplied-bounds bounding-box proxy.`, "frames")
481
+ }
482
+ } else if (object.representation === SPATIAL_SPLAT_PROXY_REPRESENTATION) {
483
+ throw new SpatialSceneError("invalid-data", `Frame evidence must not mark non-splat ${object.entityId} as a splat bounding-box proxy.`, "frames")
484
+ }
485
+ const declaredInstances = entity.kind === "mesh" && entity.instances !== undefined ? entity.instances.length : undefined
486
+ if (object.instances !== declaredInstances) {
487
+ throw new SpatialSceneError("invalid-data", `Frame evidence instance count differs from the authored declaration for ${object.entityId}.`, "frames")
488
+ }
489
+ lowered.set(object.entityId, object)
490
+ // The object-ID pass writes the declared selection code for every lowered
491
+ // surface, including view placement; only the depth pass masks view to
492
+ // no-hit. Codes without evidence remain unattributed.
493
+ attributed.set(object.selectionId, 0)
494
+ }
495
+ let renderedPixels = 0, unattributedPixels = 0
496
+ for (const [key, count] of Object.entries(frame.counts)) {
497
+ const code = Number(key)
498
+ if (!attributed.has(code)) { unattributedPixels += count; continue }
499
+ attributed.set(code, count)
500
+ renderedPixels += count
501
+ }
502
+ frameDrafts.set(frame.timeUs, {
503
+ timeUs: frame.timeUs, width: frame.width, height: frame.height, pngSha256: frame.pngSha256,
504
+ attributed, lowered, renderedPixels, unattributedPixels,
505
+ })
506
+ }
507
+
508
+ // The geometric audit supplies effective visibility and corner-projection
509
+ // coverage; the rendered audit compares, never substitutes, its estimate.
510
+ const geometric = auditSpatialSceneInContext(context, {
511
+ cameraId, timesUs,
512
+ ...(captured.assetBounds === undefined ? {} : { assetBounds: captured.assetBounds }),
513
+ })
514
+ const geometricEntities = new Map(geometric.entities.map(entity => [entity.entityId, entity]))
515
+ const geometricSamples = new Map(geometric.entities.map(entity => [
516
+ entity.entityId, new Map(entity.samples.map(sample => [sample.timeUs, sample])),
517
+ ]))
518
+
519
+ const findings: Finding[] = []
520
+ const entities: SpatialRenderedAuditEntity[] = []
521
+ const entitiesNeverRendered: string[] = []
522
+ const entitiesUnsupported: string[] = []
523
+ const entitiesViewMasked: string[] = []
524
+ const entitiesProxyCoverage: string[] = []
525
+ const suppliedAssetBounds = captured.assetBounds ?? {}
526
+ const eligibilityCounts = { renderable: 0, "proxy-coverage": 0, "view-masked": 0, "no-surface": 0, "unsupported-kind": 0 }
527
+ for (const entity of scene.entities) {
528
+ const entityEligibility = eligibility(entity, suppliedAssetBounds)
529
+ eligibilityCounts[entityEligibility]++
530
+ if (entityEligibility === "unsupported-kind") entitiesUnsupported.push(entity.entityId)
531
+ if (entityEligibility === "view-masked") entitiesViewMasked.push(entity.entityId)
532
+ if (entityEligibility === "proxy-coverage") entitiesProxyCoverage.push(entity.entityId)
533
+ const selectionId = entityIndex.get(entity.entityId)!
534
+ const geometricEntity = geometricEntities.get(entity.entityId)!
535
+ const geoSamples = geometricSamples.get(entity.entityId)!
536
+ const samples: SpatialRenderedAuditSample[] = []
537
+ for (const timeUs of timesUs) {
538
+ const frame = frameDrafts.get(timeUs)!
539
+ const geo = geoSamples.get(timeUs)!
540
+ const evidence = frame.lowered.get(entity.entityId)
541
+ const expected = (entityEligibility === "renderable" || entityEligibility === "view-masked" || entityEligibility === "proxy-coverage")
542
+ && geo.visible && geo.note !== "other-camera"
543
+ const lowered = evidence !== undefined
544
+ const pixels = evidence !== undefined ? frame.attributed.get(selectionId) ?? 0 : 0
545
+ const note: (typeof SAMPLE_NOTES)[number] | undefined = expected && !lowered ? "unlowered-expected" : geo.note
546
+ samples.push({
547
+ timeUs, expected, lowered, rendered: pixels > 0, pixels,
548
+ framePercent: round3(pixels * 100 / framePixels),
549
+ ...(geo.frustum === undefined ? {} : { geometricPixels: geo.frustum.pixelFootprint }),
550
+ ...(geo.frustum !== undefined && geo.frustum.pixelFootprint > 0
551
+ ? { coverageRatio: round3(pixels / geo.frustum.pixelFootprint) } : {}),
552
+ ...(note === undefined ? {} : { note }),
553
+ })
554
+ }
555
+ const totals = {
556
+ expected: samples.filter(sample => sample.expected).length,
557
+ lowered: samples.filter(sample => sample.lowered).length,
558
+ rendered: samples.filter(sample => sample.rendered).length,
559
+ pixels: samples.reduce((sum, sample) => sum + sample.pixels, 0),
560
+ maxPixels: samples.reduce((maximum, sample) => Math.max(maximum, sample.pixels), 0),
561
+ maxFramePercent: samples.reduce((maximum, sample) => Math.max(maximum, sample.framePercent), 0),
562
+ }
563
+ entities.push({
564
+ entityId: entity.entityId, name: entity.name, kind: entity.kind, placement: entity.placement,
565
+ eligibility: entityEligibility, selectionId, enclosure: geometricEntity.enclosure,
566
+ ...(entity.kind === "mesh" && entity.instances !== undefined ? { instances: entity.instances.length } : {}),
567
+ samples, totals,
568
+ })
569
+ if (entityEligibility === "unsupported-kind") {
570
+ findings.push({
571
+ severity: "info", kind: "unsupported-kind", entityId: entity.entityId,
572
+ detail: entity.placement.kind === "world"
573
+ ? "The object-ID pass cannot lower this splat's bounding-box proxy without decoded splat-position bounds; retained collider evidence is approximate, never pixel truth."
574
+ : "The object-ID pass cannot lower a view-placed splat; bounding-box proxies require world placement.",
575
+ })
576
+ if (geometricEntity.enclosure.status === "unknown" && geometricEntity.enclosure.reason !== "no-surface") {
577
+ findings.push({
578
+ severity: "info", kind: "bounds-unknown", entityId: entity.entityId,
579
+ detail: "Bounds require decoded asset data; supply assetBounds so the object-ID pass can lower a bounding-box proxy.",
580
+ })
581
+ }
582
+ continue
583
+ }
584
+ if (entityEligibility === "no-surface") continue
585
+ if (entityEligibility === "proxy-coverage") {
586
+ findings.push({
587
+ severity: "info", kind: "proxy-coverage", entityId: entity.entityId,
588
+ detail: "Object-ID coverage counts this entity's bounding-box proxy built from supplied splat-position bounds — approximate coverage, never splat pixel truth.",
589
+ })
590
+ }
591
+ if (entityEligibility === "view-masked"
592
+ && entity.placement.kind === "view" && entity.placement.cameraId === cameraId && totals.lowered > 0) {
593
+ findings.push({
594
+ severity: "info", kind: "unsupported-kind", entityId: entity.entityId,
595
+ detail: "View surfaces draw after world content and write their own selection code; their pixels are attributed to the entity and can occlude world counts.",
596
+ })
597
+ }
598
+ if (entity.visible && totals.rendered === 0) {
599
+ entitiesNeverRendered.push(entity.entityId)
600
+ const covered = samples.filter(sample => sample.expected && (sample.geometricPixels ?? 0) > 0).length
601
+ const maximum = samples.reduce((value, sample) => Math.max(value, sample.geometricPixels ?? 0), 0)
602
+ findings.push({
603
+ severity: totals.expected === 0 ? "info" : "warning",
604
+ kind: "never-rendered", entityId: entity.entityId,
605
+ detail: totals.expected === 0
606
+ ? "Authored visible but effectively invisible at every sampled time; zero rendered pixels is consistent."
607
+ : covered === 0
608
+ ? `Effectively visible at ${String(totals.expected)} sampled time${totals.expected === 1 ? "" : "s"} but its geometric estimate never covered the frame; zero pixels is consistent.`
609
+ : `Zero pixels at every expected sample despite up to ${String(maximum)} px² of geometric coverage; fully occluded, masked, or below the coverage threshold.`,
610
+ })
611
+ } else if (totals.rendered > 0) {
612
+ const occluded = samples.filter(sample => sample.expected && (sample.geometricPixels ?? 0) > 0 && sample.pixels === 0).length
613
+ if (occluded > 0) {
614
+ findings.push({
615
+ severity: "info", kind: "occluded", entityId: entity.entityId,
616
+ detail: `Zero rendered pixels at ${String(occluded)} of ${String(totals.expected)} expected samples despite positive geometric coverage; occluded, masked, or below the coverage threshold.`,
617
+ })
618
+ }
619
+ }
620
+ if (geometricEntity.enclosure.status === "unknown" && totals.expected > 0 && geometricEntity.enclosure.reason !== "no-surface") {
621
+ findings.push({
622
+ severity: "info", kind: "bounds-unknown", entityId: entity.entityId,
623
+ detail: geometricEntity.enclosure.reason === "requires-asset-decoding"
624
+ ? "Bounds require decoded asset data; supply assetBounds for a geometric footprint delta."
625
+ : "Text bounds require font layout; the rendered footprint has no geometric delta.",
626
+ })
627
+ }
628
+ }
629
+
630
+ const frameReports = timesUs.map(timeUs => {
631
+ const frame = frameDrafts.get(timeUs)!
632
+ return {
633
+ timeUs, pngSha256: frame.pngSha256,
634
+ renderedPixels: frame.renderedPixels, unattributedPixels: frame.unattributedPixels,
635
+ loweredEntities: frame.lowered.size,
636
+ }
637
+ })
638
+ const renderedPixels = frameReports.reduce((sum, frame) => sum + frame.renderedPixels, 0)
639
+ const unattributedPixels = frameReports.reduce((sum, frame) => sum + frame.unattributedPixels, 0)
640
+ if (unattributedPixels > 0) {
641
+ const framesWithUnattributed = frameReports.filter(frame => frame.unattributedPixels > 0).length
642
+ findings.push({
643
+ severity: "warning", kind: "unattributed-pixels",
644
+ detail: `${String(unattributedPixels)} pixel${unattributedPixels === 1 ? "" : "s"} across ${String(framesWithUnattributed)} frame${framesWithUnattributed === 1 ? "" : "s"} carried selection codes absent from attributable evidence.`,
645
+ })
646
+ }
647
+ if (renderedPixels === 0) {
648
+ const expectedSamples = entities.reduce((sum, entity) => sum + entity.totals.expected, 0)
649
+ findings.push({
650
+ severity: "warning", kind: "empty-render",
651
+ detail: `No attributed object-ID pixel appeared in any sampled frame (${String(expectedSamples)} expected entity-samples).`,
652
+ })
653
+ }
654
+
655
+ const sortedFindings = sortSpatialBy(findings, findingOrder)
656
+ const retainedFindings = sortedFindings.slice(0, SPATIAL_RENDERED_AUDIT_LIMITS.findings)
657
+ const report = {
658
+ kind: "slopcamera.spatial-rendered-audit" as const, schemaVersion: 1 as const,
659
+ sceneId: scene.sceneId, sceneSha256: context.sceneSha256, cameraId,
660
+ durationUs: scene.durationUs, timesUs,
661
+ mode: { kind: "object-id" as const, coverage },
662
+ frame: { width: first.width, height: first.height, pixels: framePixels },
663
+ summary: {
664
+ entities: {
665
+ total: scene.entities.length,
666
+ renderable: eligibilityCounts.renderable,
667
+ proxyCoverage: eligibilityCounts["proxy-coverage"],
668
+ viewMasked: eligibilityCounts["view-masked"],
669
+ noSurface: eligibilityCounts["no-surface"],
670
+ unsupported: eligibilityCounts["unsupported-kind"],
671
+ },
672
+ entitiesNeverRendered: sortSpatialBy(entitiesNeverRendered, id => id),
673
+ entitiesUnsupported: sortSpatialBy(entitiesUnsupported, id => id),
674
+ entitiesViewMasked: sortSpatialBy(entitiesViewMasked, id => id),
675
+ entitiesProxyCoverage: sortSpatialBy(entitiesProxyCoverage, id => id),
676
+ renderedPixels, unattributedPixels,
677
+ },
678
+ entities, frames: frameReports,
679
+ findings: retainedFindings,
680
+ omittedFindings: sortedFindings.length - retainedFindings.length,
681
+ }
682
+ const parsed: SpatialRenderedAuditReport = SpatialRenderedAuditReportSchema.parse(report)
683
+ try {
684
+ createBoundedJsonValueSnapshot(parsed, SPATIAL_RENDERED_AUDIT_LIMITS.reportBytes, "rendered audit report", {
685
+ maximumDepth: SPATIAL_SCENE_LIMITS.sourceDepth + 8,
686
+ maximumValues: SPATIAL_SCENE_LIMITS.sourceValues + SPATIAL_RENDERED_AUDIT_LIMITS.entitySamples * 16,
687
+ })
688
+ } catch (error) {
689
+ throw new SpatialSceneError("invalid-data", error instanceof Error ? error.message : "Rendered audit report exceeds its bounded size.", "rendered audit")
690
+ }
691
+ return deepFreezeJson(parsed)
692
+ }