@hraness/slopcamera 3.2.7 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +65 -20
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +6 -9
  6. package/apps/desktop/application/default-registry.ts +26 -10
  7. package/apps/desktop/application/operation.ts +14 -5
  8. package/apps/desktop/application/operations/index.ts +4 -1
  9. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  10. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  11. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  12. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  13. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  14. package/apps/desktop/application/registry.ts +2 -2
  15. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  16. package/apps/desktop/application/spatial-assets.ts +160 -31
  17. package/apps/desktop/application/spatial-render.ts +217 -20
  18. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  19. package/apps/desktop/application/spatial-review.ts +361 -0
  20. package/apps/desktop/application/spatial-spz.ts +48 -3
  21. package/apps/desktop/application/spatial-world-import.ts +26 -5
  22. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  23. package/apps/desktop/cli/args.ts +528 -97
  24. package/apps/desktop/cli/capability-manifest.ts +374 -0
  25. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  26. package/apps/desktop/cli/command-host-resources.ts +31 -2
  27. package/apps/desktop/cli/commands.ts +1215 -70
  28. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  29. package/apps/desktop/cli/help.ts +183 -23
  30. package/apps/desktop/cli/html-scene.ts +139 -28
  31. package/apps/desktop/cli/io.ts +2 -1
  32. package/apps/desktop/cli/main.ts +26 -50
  33. package/apps/desktop/cli/menubar.ts +244 -0
  34. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  35. package/apps/desktop/cli/portable-surface.ts +18 -5
  36. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  37. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  38. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  39. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  40. package/apps/desktop/cli/workflow-code.ts +1 -1
  41. package/apps/desktop/code/application-node-planner.ts +0 -47
  42. package/apps/desktop/code/host-source-layout.ts +46 -0
  43. package/apps/desktop/code/public.ts +0 -1
  44. package/apps/desktop/code/runtime-identity.ts +50 -8
  45. package/apps/desktop/code/semantic-builder.ts +24 -43
  46. package/apps/desktop/code/source-bundle.ts +23 -6
  47. package/apps/desktop/code/source-typecheck.ts +131 -43
  48. package/apps/desktop/code/worker-client.ts +14 -2
  49. package/apps/desktop/contracts/cinema.ts +736 -0
  50. package/apps/desktop/contracts/index.ts +1 -0
  51. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  52. package/apps/desktop/contracts/spatial-world.ts +26 -3
  53. package/apps/desktop/core/cinema-plan.ts +1320 -0
  54. package/apps/desktop/core/index.ts +1 -0
  55. package/apps/desktop/core/music-analysis.ts +124 -1
  56. package/apps/desktop/core/storage.ts +75 -0
  57. package/apps/desktop/dist/cli/main.js +737 -291
  58. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  59. package/apps/desktop/html-overlay/index.ts +9 -0
  60. package/apps/desktop/html-overlay/runtime.ts +2 -0
  61. package/apps/desktop/html-overlay/scene.ts +29 -0
  62. package/apps/desktop/html-overlay/spatial.ts +619 -57
  63. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  64. package/apps/desktop/workflows/index.ts +6 -0
  65. package/dist/cli.js +32 -434
  66. package/dist/code/advanced.js +1 -269
  67. package/dist/code/index.js +16 -2239
  68. package/dist/generate.js +1 -23
  69. package/dist/host-resources.js +1 -25
  70. package/dist/index-0bbesn4m.js +35 -0
  71. package/dist/index-296qhpf2.js +3 -0
  72. package/dist/index-3cxf2c58.js +3 -0
  73. package/dist/index-77fjfg9f.js +3 -0
  74. package/dist/index-80a36bc0.js +3 -0
  75. package/dist/index-a3vvs3rf.js +3 -0
  76. package/dist/index-b3geqczn.js +5 -0
  77. package/dist/index-cgpkyjqa.js +3 -0
  78. package/dist/index-f0ptv8dm.js +7 -0
  79. package/dist/index-grk47tgs.js +3 -0
  80. package/dist/index-qry58nj2.js +10 -0
  81. package/dist/index.js +1 -240
  82. package/dist/operations.js +1 -28
  83. package/dist/skill-install-gx7afxsd.js +2 -0
  84. package/dist/vectorize/worker.js +1 -141
  85. package/dist/workflow.js +1 -15
  86. package/docs/README.md +5 -1
  87. package/docs/studio.md +1 -1
  88. package/package.json +11 -24
  89. package/skills/slopcamera/SKILL.md +14 -1
  90. package/skills/slopcamera/references/diagrams.md +5 -14
  91. package/skills/slopcamera/references/directed-scenes.md +22 -2
  92. package/skills/slopcamera/references/gateway-media.md +15 -1
  93. package/skills/slopcamera/references/image-galleries.md +166 -0
  94. package/skills/slopcamera/references/install.md +2 -2
  95. package/skills/slopcamera/references/music-video.md +23 -3
  96. package/skills/slopcamera/references/parametric-design.md +14 -0
  97. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  98. package/skills/slopcamera/references/scene-building.md +32 -0
  99. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  100. package/skills/slopcamera/references/support.md +25 -0
  101. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  102. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  103. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  104. package/src/capability-manifest.ts +361 -0
  105. package/src/cli.ts +250 -102
  106. package/src/code/canonical-json.ts +9 -0
  107. package/src/generate.ts +2 -2
  108. package/src/icon.ts +1111 -0
  109. package/src/image-gallery.ts +1049 -0
  110. package/src/index.ts +11 -23
  111. package/src/mcp/boundary.ts +11 -11
  112. package/src/mcp/index.ts +3 -0
  113. package/src/mcp/server.ts +2 -1
  114. package/src/mcp/tools.ts +1642 -130
  115. package/src/mcp/types.ts +19 -0
  116. package/src/operations.ts +412 -0
  117. package/src/portable-capability-manifest.ts +67 -0
  118. package/src/scene-gallery.ts +184 -0
  119. package/src/spatial-scene/asset-admission.ts +183 -0
  120. package/src/spatial-scene/audit-rendered.ts +692 -0
  121. package/src/spatial-scene/audit.ts +617 -0
  122. package/src/spatial-scene/behavior-audit.ts +252 -0
  123. package/src/spatial-scene/behavior-bake.ts +233 -0
  124. package/src/spatial-scene/behavior-fns.ts +585 -0
  125. package/src/spatial-scene/behavior-gallery.ts +109 -0
  126. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  127. package/src/spatial-scene/behavior-trace.ts +325 -0
  128. package/src/spatial-scene/behavior.ts +618 -0
  129. package/src/spatial-scene/build.ts +632 -0
  130. package/src/spatial-scene/camera-rig.ts +125 -0
  131. package/src/spatial-scene/camera-track.ts +4 -2
  132. package/src/spatial-scene/character.ts +124 -0
  133. package/src/spatial-scene/contracts.ts +75 -7
  134. package/src/spatial-scene/design-templates.ts +351 -0
  135. package/src/spatial-scene/design.ts +327 -0
  136. package/src/spatial-scene/direction-compile.ts +584 -0
  137. package/src/spatial-scene/direction.ts +122 -0
  138. package/src/spatial-scene/effects.ts +149 -0
  139. package/src/spatial-scene/evaluate.ts +109 -29
  140. package/src/spatial-scene/gallery.ts +277 -0
  141. package/src/spatial-scene/generate.ts +239 -0
  142. package/src/spatial-scene/geometry-native.ts +165 -0
  143. package/src/spatial-scene/geometry.ts +1216 -0
  144. package/src/spatial-scene/gltf.ts +600 -77
  145. package/src/spatial-scene/identity.ts +55 -2
  146. package/src/spatial-scene/index.ts +36 -0
  147. package/src/spatial-scene/inspect.ts +35 -6
  148. package/src/spatial-scene/material-lighting.ts +609 -0
  149. package/src/spatial-scene/math.ts +21 -4
  150. package/src/spatial-scene/motion-evidence.ts +88 -0
  151. package/src/spatial-scene/parametric.ts +765 -0
  152. package/src/spatial-scene/particle-preparation.ts +183 -0
  153. package/src/spatial-scene/particle.ts +195 -0
  154. package/src/spatial-scene/patch.ts +109 -15
  155. package/src/spatial-scene/performance.ts +1613 -0
  156. package/src/spatial-scene/probe.ts +137 -0
  157. package/src/spatial-scene/recipe-pack.ts +86 -0
  158. package/src/spatial-scene/render-effects.ts +240 -0
  159. package/src/spatial-scene/review.ts +639 -0
  160. package/src/spatial-scene/simulation-bake.ts +206 -0
  161. package/src/spatial-scene/simulation.ts +187 -0
  162. package/src/spatial-scene/solve.ts +366 -0
  163. package/src/spatial-scene/temporal-audit.ts +354 -0
  164. package/src/support-completion.ts +16 -0
  165. package/src/support.ts +42 -0
  166. package/src/version.ts +1 -1
  167. package/apps/desktop/application/operations/recording/index.ts +0 -5
  168. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  169. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  170. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  171. package/apps/desktop/application/operations/recording/start.ts +0 -169
  172. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  173. package/apps/desktop/capture/protocol.ts +0 -689
  174. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  175. package/apps/desktop/cli/recording-controller.ts +0 -1567
  176. package/apps/desktop/cli/recording-daemon.ts +0 -931
  177. package/dist/index-42zsesc1.js +0 -2257
  178. package/dist/index-7308egqr.js +0 -48
  179. package/dist/index-8txs6fkn.js +0 -712
  180. package/dist/index-mcy8z0br.js +0 -192
  181. package/dist/index-p63wavx0.js +0 -2029
  182. package/dist/index-r7gdhmsp.js +0 -424
  183. package/dist/index-rvghzt7b.js +0 -2013
  184. package/dist/index-sh6xbav6.js +0 -1219
  185. package/dist/index-z1w83f81.js +0 -4
  186. package/dist/index-z7239b4h.js +0 -2363
  187. package/dist/skill-install-dh0ntqba.js +0 -10
  188. package/src/desktop.ts +0 -253
@@ -0,0 +1,618 @@
1
+ import { z } from "zod"
2
+
3
+ import { boundedCanonicalJsonSha256 } from "../code/canonical-json.js"
4
+ import { deepFreezeJson } from "../code/json-snapshot.js"
5
+ import {
6
+ SpatialDigestSchema,
7
+ SpatialSceneV1Schema,
8
+ SpatialTimeUsSchema,
9
+ type SpatialSceneV1,
10
+ } from "./contracts.js"
11
+ import { parseSpatialValue, spatialValueSha256 } from "./identity.js"
12
+
13
+ /**
14
+ * Behavior documents bind an ALGAL organism closure to one scene entity.
15
+ *
16
+ * A `slopcamera.spatial-behavior` document is bounded, inert, content-addressed
17
+ * data: it carries every organism manifest the behavior needs, keyed by the
18
+ * ALGAL `sha256:` digest, and names the entry organism plus deterministic
19
+ * inputs (seed, range, args). Admission parses from `unknown` through strict
20
+ * schemas and a bake-safe organism profile — a closed subset of
21
+ * `morphogen.organism.v1` containing only `input`, `const`, `fn`, `repeat`,
22
+ * `each`, and `organism` cells. `agent`, `classifier`, `gate`, `tool`,
23
+ * `spawn`, `slot`, `store`, `load` cells and every `via` transport field are
24
+ * structural rejections: bakes must replay deterministically from the document
25
+ * alone, with no model calls, effects, transports, or ambient store state.
26
+ *
27
+ * Organism digests are ALGAL digests — `sha256:` + SHA-256 of canonical JSON
28
+ * over `manifestToJson`'s normalized projection — mirrored here so closure
29
+ * keys rehash exactly against manifests authored with ALGAL tooling.
30
+ */
31
+ export const SPATIAL_BEHAVIOR_LIMITS = Object.freeze({
32
+ args: 16,
33
+ carry: 16,
34
+ cells: 64,
35
+ channels: 64,
36
+ constPorts: 32,
37
+ eachItems: 64,
38
+ edges: 256,
39
+ findings: 128,
40
+ interfacePorts: 32,
41
+ organisms: 8,
42
+ rounds: 16,
43
+ })
44
+
45
+ const organismKey = z.string().regex(/^organism:[a-z][a-z0-9-]{0,54}$/u)
46
+ const cellId = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/u)
47
+ const portName = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/u)
48
+ const interfaceName = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/u)
49
+ const organismDigest = z.string().regex(/^sha256:[a-f0-9]{64}$/u)
50
+ const fnRef = z.string().regex(/^[a-z][a-z0-9_.]*\.v[0-9]+$/u).max(64)
51
+
52
+ export const SpatialBehaviorPortTypeSchema = z.strictObject({
53
+ type: z.enum(["text", "json", "choice", "ref"]),
54
+ optional: z.boolean().optional(),
55
+ many: z.boolean().optional(),
56
+ labels: z.array(z.string().min(1).max(64)).max(32).optional(),
57
+ schema: z.unknown().optional(),
58
+ }).superRefine((port, context) => {
59
+ if (port.labels !== undefined && port.type !== "choice") {
60
+ context.addIssue({ code: "custom", message: "Only choice ports declare labels.", path: ["labels"] })
61
+ }
62
+ if (port.schema !== undefined && port.type !== "json") {
63
+ context.addIssue({ code: "custom", message: "Only json ports declare a schema.", path: ["schema"] })
64
+ }
65
+ })
66
+ export type SpatialBehaviorPortType = Readonly<z.infer<typeof SpatialBehaviorPortTypeSchema>>
67
+
68
+ export const SpatialBehaviorPortMapSchema = z.record(
69
+ portName,
70
+ SpatialBehaviorPortTypeSchema,
71
+ ).refine((ports) => Object.keys(ports).length <= SPATIAL_BEHAVIOR_LIMITS.interfacePorts, {
72
+ message: `A port map admits at most ${SPATIAL_BEHAVIOR_LIMITS.interfacePorts} ports.`,
73
+ })
74
+ export type SpatialBehaviorPortMap = Readonly<z.infer<typeof SpatialBehaviorPortMapSchema>>
75
+
76
+ export interface SpatialBehaviorFnSignature {
77
+ readonly inputs: SpatialBehaviorPortMap
78
+ readonly outputs: SpatialBehaviorPortMap
79
+ }
80
+
81
+ const constOutput = z.record(
82
+ portName,
83
+ SpatialBehaviorPortTypeSchema.extend({ value: z.unknown() }),
84
+ ).refine((ports) => Object.keys(ports).length <= SPATIAL_BEHAVIOR_LIMITS.constPorts, {
85
+ message: `A const cell declares at most ${SPATIAL_BEHAVIOR_LIMITS.constPorts} outputs.`,
86
+ })
87
+
88
+ export const SpatialBehaviorCellSchema = z.discriminatedUnion("kind", [
89
+ z.strictObject({ id: cellId, kind: z.literal("input"), outputs: SpatialBehaviorPortMapSchema }),
90
+ z.strictObject({ id: cellId, kind: z.literal("const"), outputs: constOutput }),
91
+ z.strictObject({ id: cellId, kind: z.literal("fn"), fn: fnRef }),
92
+ z.strictObject({
93
+ id: cellId,
94
+ kind: z.literal("repeat"),
95
+ manifest: organismDigest,
96
+ maxRounds: z.number().int().min(1).max(SPATIAL_BEHAVIOR_LIMITS.rounds),
97
+ carry: z.record(interfaceName, interfaceName).refine(
98
+ (carry) => Object.keys(carry).length <= SPATIAL_BEHAVIOR_LIMITS.carry,
99
+ { message: `A repeat cell carries at most ${SPATIAL_BEHAVIOR_LIMITS.carry} ports.` },
100
+ ).optional(),
101
+ until: z.strictObject({
102
+ output: interfaceName,
103
+ equals: z.string().min(1).max(64),
104
+ field: portName.optional(),
105
+ }).optional(),
106
+ }),
107
+ z.strictObject({
108
+ id: cellId,
109
+ kind: z.literal("each"),
110
+ manifest: organismDigest,
111
+ over: interfaceName,
112
+ maxItems: z.number().int().min(1).max(SPATIAL_BEHAVIOR_LIMITS.eachItems),
113
+ }),
114
+ z.strictObject({ id: cellId, kind: z.literal("organism"), manifest: organismDigest }),
115
+ ])
116
+ export type SpatialBehaviorCell = Readonly<z.infer<typeof SpatialBehaviorCellSchema>>
117
+
118
+ export const SpatialBehaviorEdgeSchema = z.strictObject({
119
+ from: z.strictObject({ cell: cellId, port: portName }),
120
+ to: z.strictObject({ cell: cellId, port: portName }),
121
+ guard: z.strictObject({ equals: z.string().min(1).max(64), field: portName.optional() }).optional(),
122
+ on: z.literal("fail").optional(),
123
+ })
124
+ export type SpatialBehaviorEdge = Readonly<z.infer<typeof SpatialBehaviorEdgeSchema>>
125
+
126
+ const behaviorBudgets = z.strictObject({
127
+ maxSteps: z.number().int().min(1).max(1024).optional(),
128
+ maxAgentCalls: z.number().int().min(0).max(64).optional(),
129
+ maxWork: z.number().int().min(1).max(100_000_000).optional(),
130
+ maxContextBytes: z.number().int().min(1).max(262_144).optional(),
131
+ maxOutputBytes: z.number().int().min(1).max(262_144).optional(),
132
+ maxDepth: z.number().int().min(1).max(8).optional(),
133
+ })
134
+
135
+ const interfaceEndpoint = z.strictObject({ cell: cellId, port: portName })
136
+
137
+ export const SpatialBehaviorOrganismSchema = z.strictObject({
138
+ contract: z.literal("morphogen.organism.v1"),
139
+ key: organismKey,
140
+ name: z.string().min(1).max(120),
141
+ note: z.string().max(2000).optional(),
142
+ budgets: behaviorBudgets.optional(),
143
+ interface: z.strictObject({
144
+ inputs: z.record(interfaceName, interfaceEndpoint),
145
+ outputs: z.record(interfaceName, interfaceEndpoint),
146
+ }),
147
+ cells: z.array(SpatialBehaviorCellSchema).min(1).max(SPATIAL_BEHAVIOR_LIMITS.cells),
148
+ edges: z.array(SpatialBehaviorEdgeSchema).max(SPATIAL_BEHAVIOR_LIMITS.edges),
149
+ }).superRefine((manifest, context) => {
150
+ const ids = new Set<string>()
151
+ for (const [index, cell] of manifest.cells.entries()) {
152
+ if (ids.has(cell.id)) {
153
+ context.addIssue({ code: "custom", message: `Duplicate cell id ${cell.id}.`, path: ["cells", index, "id"] })
154
+ }
155
+ ids.add(cell.id)
156
+ // Producer ports cannot be `many`: declared port maps in the bake-safe
157
+ // subset are all producer-side (input/const outputs).
158
+ if (cell.kind === "input" || cell.kind === "const") {
159
+ for (const [name, port] of Object.entries(cell.outputs)) {
160
+ if (port.many === true) {
161
+ context.addIssue({ code: "custom", message: `Port ${name} declares many, which is only valid on consumer ports.`, path: ["cells", index, "outputs", name, "many"] })
162
+ }
163
+ }
164
+ }
165
+ }
166
+ })
167
+ export type SpatialBehaviorOrganism = Readonly<z.infer<typeof SpatialBehaviorOrganismSchema>>
168
+
169
+ export const SpatialBehaviorSchema = z.strictObject({
170
+ kind: z.literal("slopcamera.spatial-behavior"),
171
+ schemaVersion: z.literal(1),
172
+ behaviorId: z.string().regex(/^behavior_[a-zA-Z0-9_-]{1,64}$/u),
173
+ entityId: z.string().min(1).max(128).regex(/^[a-z][a-z0-9_-]*$/u),
174
+ sceneSha256: SpatialDigestSchema,
175
+ seed: z.number().int().min(0).max(4_294_967_295),
176
+ rangeUs: z.strictObject({ startUs: SpatialTimeUsSchema, endUs: SpatialTimeUsSchema })
177
+ .refine((range) => range.endUs > range.startUs, { message: "rangeUs.endUs must exceed rangeUs.startUs." }),
178
+ organisms: z.record(z.string(), SpatialBehaviorOrganismSchema)
179
+ .refine((organisms) => {
180
+ const keys = Object.keys(organisms)
181
+ return keys.length >= 1 && keys.length <= SPATIAL_BEHAVIOR_LIMITS.organisms
182
+ && keys.every((key) => /^sha256:[a-f0-9]{64}$/u.test(key))
183
+ }, { message: `organisms maps 1–${SPATIAL_BEHAVIOR_LIMITS.organisms} sha256:<64-hex> digests to manifests.` }),
184
+ entry: organismDigest,
185
+ channels: z.array(z.string().regex(/^[a-z][a-z0-9_.-]{0,62}$/u)).min(1).max(SPATIAL_BEHAVIOR_LIMITS.channels),
186
+ args: z.record(interfaceName, z.unknown()).refine(
187
+ (args) => Object.keys(args).length <= SPATIAL_BEHAVIOR_LIMITS.args,
188
+ { message: `At most ${SPATIAL_BEHAVIOR_LIMITS.args} entry args.` },
189
+ ).optional(),
190
+ }).superRefine((behavior, context) => {
191
+ const channelSet = new Set(behavior.channels)
192
+ if (channelSet.size !== behavior.channels.length) {
193
+ context.addIssue({ code: "custom", message: "Behavior channels must be unique.", path: ["channels"] })
194
+ }
195
+ })
196
+ export type SpatialBehavior = Readonly<z.infer<typeof SpatialBehaviorSchema>>
197
+
198
+ export function parseSpatialBehavior(input: unknown): SpatialBehavior {
199
+ return deepFreezeJson(parseSpatialValue(SpatialBehaviorSchema, input, "spatial behavior"))
200
+ }
201
+
202
+ export function spatialBehaviorSha256(behavior: SpatialBehavior): string {
203
+ return spatialValueSha256(behavior)
204
+ }
205
+
206
+ // --------------------------------------------------- organism projection ---
207
+ // Mirrors ALGAL's `manifestToJson` for the bake-safe subset so closure digests
208
+ // rehash exactly against manifests authored with ALGAL tooling. Canonical JSON
209
+ // on both sides is recursively sorted-key JSON.stringify, so
210
+ // `sha256:` + canonical-json-sha256(projection) reproduces the ALGAL
211
+ // `sha256:<hex>` digest byte-for-byte. Kept narrow deliberately: a cell kind
212
+ // outside the subset never reaches this function (the schema rejects it).
213
+
214
+ const ALGAL_DEFAULT_BUDGETS = Object.freeze({
215
+ maxSteps: 256,
216
+ maxAgentCalls: 16,
217
+ maxWork: 1_000_000,
218
+ maxContextBytes: 65_536,
219
+ maxOutputBytes: 65_536,
220
+ maxDepth: 4,
221
+ })
222
+
223
+ type JsonObject = Record<string, unknown>
224
+
225
+ function portTypeJson(port: SpatialBehaviorPortType): JsonObject {
226
+ const out: JsonObject = { type: port.type }
227
+ if (port.optional === true) out.optional = true
228
+ if (port.many === true) out.many = true
229
+ if (port.type === "choice" && port.labels !== undefined) out.labels = [...port.labels]
230
+ if (port.type === "json" && port.schema !== undefined) out.schema = port.schema
231
+ return out
232
+ }
233
+
234
+ function cellJson(cell: SpatialBehaviorCell): JsonObject {
235
+ switch (cell.kind) {
236
+ case "input":
237
+ return { id: cell.id, kind: cell.kind, outputs: portMapJson(cell.outputs) }
238
+ case "const": {
239
+ const outputs: JsonObject = {}
240
+ for (const [name, port] of Object.entries(cell.outputs)) {
241
+ const projected = portTypeJson(port)
242
+ projected.value = port.value
243
+ outputs[name] = projected
244
+ }
245
+ return { id: cell.id, kind: cell.kind, outputs }
246
+ }
247
+ case "fn":
248
+ return { id: cell.id, kind: cell.kind, fn: cell.fn }
249
+ case "organism":
250
+ return { id: cell.id, kind: cell.kind, manifest: cell.manifest }
251
+ case "repeat": {
252
+ const out: JsonObject = { id: cell.id, kind: cell.kind, manifest: cell.manifest, maxRounds: cell.maxRounds }
253
+ if (cell.carry !== undefined) out.carry = { ...cell.carry }
254
+ if (cell.until !== undefined) {
255
+ out.until = {
256
+ output: cell.until.output,
257
+ equals: cell.until.equals,
258
+ ...(cell.until.field === undefined ? {} : { field: cell.until.field }),
259
+ }
260
+ }
261
+ return out
262
+ }
263
+ case "each":
264
+ return { id: cell.id, kind: cell.kind, manifest: cell.manifest, over: cell.over, maxItems: cell.maxItems }
265
+ }
266
+ }
267
+
268
+ function portMapJson(ports: SpatialBehaviorPortMap): JsonObject {
269
+ const out: JsonObject = {}
270
+ for (const [name, port] of Object.entries(ports)) out[name] = portTypeJson(port)
271
+ return out
272
+ }
273
+
274
+ /** Normalized organism projection matching ALGAL `manifestToJson` for the subset. */
275
+ export function behaviorOrganismCanonicalValue(manifest: SpatialBehaviorOrganism): JsonObject {
276
+ const budgets = { ...ALGAL_DEFAULT_BUDGETS, ...(manifest.budgets ?? {}) }
277
+ const out: JsonObject = {
278
+ contract: manifest.contract,
279
+ key: manifest.key,
280
+ name: manifest.name,
281
+ budgets: {
282
+ maxAgentCalls: budgets.maxAgentCalls,
283
+ maxContextBytes: budgets.maxContextBytes,
284
+ maxDepth: budgets.maxDepth,
285
+ maxOutputBytes: budgets.maxOutputBytes,
286
+ maxSteps: budgets.maxSteps,
287
+ maxWork: budgets.maxWork,
288
+ },
289
+ cells: manifest.cells.map(cellJson),
290
+ edges: manifest.edges.map((edge) => ({
291
+ from: { cell: edge.from.cell, port: edge.from.port },
292
+ to: { cell: edge.to.cell, port: edge.to.port },
293
+ ...(edge.on === undefined ? {} : { on: edge.on }),
294
+ ...(edge.guard === undefined
295
+ ? {}
296
+ : {
297
+ guard: {
298
+ equals: edge.guard.equals,
299
+ ...(edge.guard.field === undefined ? {} : { field: edge.guard.field }),
300
+ },
301
+ }),
302
+ })),
303
+ }
304
+ if (manifest.note !== undefined) out.note = manifest.note
305
+ const mapEnds = (endpoints: Readonly<Record<string, { cell: string; port: string }>>) => {
306
+ const mapped: JsonObject = {}
307
+ for (const [name, target] of Object.entries(endpoints)) mapped[name] = { cell: target.cell, port: target.port }
308
+ return mapped
309
+ }
310
+ out.interface = { inputs: mapEnds(manifest.interface.inputs), outputs: mapEnds(manifest.interface.outputs) }
311
+ return out
312
+ }
313
+
314
+ /** ALGAL-form digest (`sha256:<hex>`) of one bake-safe organism manifest. */
315
+ export function behaviorOrganismSha256(manifest: SpatialBehaviorOrganism): string {
316
+ return `sha256:${boundedCanonicalJsonSha256(behaviorOrganismCanonicalValue(manifest), {
317
+ maximumBytes: 262_144,
318
+ name: "behavior organism",
319
+ })}`
320
+ }
321
+
322
+ // ------------------------------------------------------------------ check ---
323
+
324
+ export const SpatialBehaviorFindingCodeSchema = z.enum([
325
+ "stale-digest",
326
+ "unresolved-entity",
327
+ "unresolved-entry",
328
+ "unresolved-manifest",
329
+ "unresolved-fn",
330
+ "closure-digest-mismatch",
331
+ "invalid-wiring",
332
+ "undeclared-arg",
333
+ "range-outside-scene",
334
+ "unreachable-organism",
335
+ "cyclic-composition",
336
+ "unfed-input",
337
+ ])
338
+ export type SpatialBehaviorFindingCode = z.infer<typeof SpatialBehaviorFindingCodeSchema>
339
+
340
+ export const SpatialBehaviorCheckFindingSchema = z.strictObject({
341
+ code: SpatialBehaviorFindingCodeSchema,
342
+ severity: z.enum(["error", "warning"]),
343
+ referenceId: z.string().min(1).max(128).optional(),
344
+ detail: z.string().min(1).max(240),
345
+ })
346
+ export type SpatialBehaviorCheckFinding = Readonly<z.infer<typeof SpatialBehaviorCheckFindingSchema>>
347
+
348
+ export const SpatialBehaviorCheckReportSchema = z.strictObject({
349
+ kind: z.literal("slopcamera.spatial-behavior-check"),
350
+ schemaVersion: z.literal(1),
351
+ behaviorSha256: SpatialDigestSchema,
352
+ sceneSha256: SpatialDigestSchema,
353
+ findings: z.array(SpatialBehaviorCheckFindingSchema).max(SPATIAL_BEHAVIOR_LIMITS.findings),
354
+ counts: z.strictObject({
355
+ organisms: z.number().int().min(0),
356
+ cells: z.number().int().min(0),
357
+ edges: z.number().int().min(0),
358
+ fnRefs: z.number().int().min(0),
359
+ errors: z.number().int().min(0),
360
+ warnings: z.number().int().min(0),
361
+ }),
362
+ })
363
+ export type SpatialBehaviorCheckReport = Readonly<z.infer<typeof SpatialBehaviorCheckReportSchema>>
364
+
365
+ const checkOptionsSchema = z.strictObject({
366
+ behavior: z.unknown(),
367
+ scene: z.unknown(),
368
+ })
369
+
370
+ interface ManifestContext {
371
+ readonly manifest: SpatialBehaviorOrganism
372
+ readonly outputPorts: ReadonlyMap<string, ReadonlySet<string>>
373
+ readonly inputPorts: ReadonlyMap<string, ReadonlySet<string>>
374
+ }
375
+
376
+ function resolveEntityId(scene: SpatialSceneV1, referenceId: string): string | undefined {
377
+ const ids = new Set(scene.entities.map((entity) => entity.entityId))
378
+ if (ids.has(referenceId)) return referenceId
379
+ const prefixed = `entity_${referenceId}`
380
+ return ids.has(prefixed) ? prefixed : undefined
381
+ }
382
+
383
+ /**
384
+ * Validates a behavior document against one scene: closure digest integrity,
385
+ * manifest/fn reference resolution, interface and edge wiring against the
386
+ * admitted fn signatures, entry args, and range bounds. Findings are evidence;
387
+ * nothing executes organism graphs.
388
+ */
389
+ export function checkSpatialBehavior(
390
+ input: unknown,
391
+ admittedFns: ReadonlyMap<string, SpatialBehaviorFnSignature>,
392
+ ): SpatialBehaviorCheckReport {
393
+ const options = parseSpatialValue(checkOptionsSchema, input, "behavior check")
394
+ const behavior = parseSpatialBehavior(options.behavior)
395
+ const scene = parseSpatialValue(SpatialSceneV1Schema, options.scene, "scene")
396
+ const sceneSha256 = spatialValueSha256(scene)
397
+ const findings: SpatialBehaviorCheckFinding[] = []
398
+ const finding = (code: SpatialBehaviorFindingCode, severity: "error" | "warning", detail: string, referenceId?: string): void => {
399
+ findings.push(deepFreezeJson(referenceId === undefined ? { code, severity, detail } : { code, severity, detail, referenceId }))
400
+ }
401
+
402
+ if (behavior.sceneSha256 !== sceneSha256) {
403
+ finding("stale-digest", "error", `Behavior sceneSha256 ${behavior.sceneSha256} does not match scene digest ${sceneSha256}; re-author or rebind the behavior.`)
404
+ }
405
+ if (resolveEntityId(scene, behavior.entityId) === undefined) {
406
+ finding("unresolved-entity", "error", `Behavior entity ${behavior.entityId} is not a scene entity.`, behavior.entityId)
407
+ }
408
+ if (behavior.rangeUs.endUs > scene.durationUs) {
409
+ finding("range-outside-scene", "error", `Behavior range ends at ${behavior.rangeUs.endUs}us beyond scene duration ${scene.durationUs}us.`)
410
+ }
411
+ if (behavior.organisms[behavior.entry] === undefined) {
412
+ finding("unresolved-entry", "error", `Entry digest ${behavior.entry.slice(0, 24)}… is not a member of the organisms closure.`, behavior.entry.slice(7, 39))
413
+ }
414
+
415
+ let cellTotal = 0
416
+ let edgeTotal = 0
417
+ let fnRefTotal = 0
418
+ const contexts = new Map<string, ManifestContext>()
419
+
420
+ // Pass 1: digest integrity and manifest-reference resolution.
421
+ for (const [digest, manifest] of Object.entries(behavior.organisms)) {
422
+ const recomputed = behaviorOrganismSha256(manifest)
423
+ if (recomputed !== digest) {
424
+ finding("closure-digest-mismatch", "error", `Organisms key ${digest.slice(0, 24)}… rehashes to ${recomputed.slice(0, 24)}…; the manifest changed after admission.`, manifest.key)
425
+ }
426
+ cellTotal += manifest.cells.length
427
+ edgeTotal += manifest.edges.length
428
+ const outputPorts = new Map<string, ReadonlySet<string>>()
429
+ const inputPorts = new Map<string, ReadonlySet<string>>()
430
+ for (const cell of manifest.cells) {
431
+ switch (cell.kind) {
432
+ case "input":
433
+ case "const":
434
+ outputPorts.set(cell.id, new Set(Object.keys(cell.outputs)))
435
+ break
436
+ case "fn": {
437
+ fnRefTotal += 1
438
+ const signature = admittedFns.get(cell.fn)
439
+ if (signature === undefined) {
440
+ finding("unresolved-fn", "error", `Cell ${cell.id} names fn ${cell.fn}, which the admitted behavior catalog does not contain.`, `${manifest.key}/${cell.id}`)
441
+ } else {
442
+ outputPorts.set(cell.id, new Set(Object.keys(signature.outputs)))
443
+ inputPorts.set(cell.id, new Set(Object.keys(signature.inputs)))
444
+ }
445
+ break
446
+ }
447
+ case "repeat":
448
+ case "each":
449
+ case "organism": {
450
+ const child = behavior.organisms[cell.manifest]
451
+ if (child === undefined) {
452
+ finding("unresolved-manifest", "error", `Cell ${cell.id} references manifest ${cell.manifest.slice(0, 24)}…, absent from the organisms closure.`, `${manifest.key}/${cell.id}`)
453
+ } else {
454
+ outputPorts.set(cell.id, new Set(Object.keys(child.interface.outputs)))
455
+ inputPorts.set(cell.id, new Set(Object.keys(child.interface.inputs)))
456
+ }
457
+ break
458
+ }
459
+ }
460
+ }
461
+ contexts.set(digest, { manifest, outputPorts, inputPorts })
462
+ }
463
+
464
+ // Pass 2: wiring inside each manifest.
465
+ for (const context of contexts.values()) {
466
+ const { manifest, outputPorts, inputPorts } = context
467
+ const cellIds = new Set(manifest.cells.map((cell) => cell.id))
468
+ const ref = (cell: string) => `${manifest.key}/${cell}`
469
+
470
+ for (const [name, endpoint] of Object.entries(manifest.interface.inputs)) {
471
+ const cell = manifest.cells.find((candidate) => candidate.id === endpoint.cell)
472
+ if (cell === undefined) {
473
+ finding("invalid-wiring", "error", `Interface input ${name} references absent cell ${endpoint.cell}.`, manifest.key)
474
+ } else if (cell.kind !== "input") {
475
+ finding("invalid-wiring", "error", `Interface input ${name} references ${cell.kind} cell ${cell.id}; only input cells receive entry args.`, ref(cell.id))
476
+ } else if (!(endpoint.port in cell.outputs)) {
477
+ finding("invalid-wiring", "error", `Interface input ${name} names undeclared port ${endpoint.port} on input cell ${cell.id}.`, ref(cell.id))
478
+ }
479
+ }
480
+ for (const [name, endpoint] of Object.entries(manifest.interface.outputs)) {
481
+ if (!cellIds.has(endpoint.cell)) {
482
+ finding("invalid-wiring", "error", `Interface output ${name} references absent cell ${endpoint.cell}.`, manifest.key)
483
+ } else if (!outputPorts.get(endpoint.cell)?.has(endpoint.port)) {
484
+ finding("invalid-wiring", "error", `Interface output ${name} names undeclared output port ${endpoint.port} on cell ${endpoint.cell}.`, ref(endpoint.cell))
485
+ }
486
+ }
487
+
488
+ for (const cell of manifest.cells) {
489
+ if (cell.kind === "repeat" || cell.kind === "each" || cell.kind === "organism") {
490
+ const child = behavior.organisms[cell.manifest]
491
+ if (child !== undefined) {
492
+ if (cell.kind === "repeat") {
493
+ for (const [outName, inName] of Object.entries(cell.carry ?? {})) {
494
+ if (!(outName in child.interface.outputs)) {
495
+ finding("invalid-wiring", "error", `Repeat ${cell.id} carries undeclared output ${outName} from ${child.key}.`, ref(cell.id))
496
+ }
497
+ if (!(inName in child.interface.inputs)) {
498
+ finding("invalid-wiring", "error", `Repeat ${cell.id} carries into undeclared input ${inName} on ${child.key}.`, ref(cell.id))
499
+ }
500
+ }
501
+ if (cell.until !== undefined && !(cell.until.output in child.interface.outputs)) {
502
+ finding("invalid-wiring", "error", `Repeat ${cell.id} until watches undeclared output ${cell.until.output} on ${child.key}.`, ref(cell.id))
503
+ }
504
+ }
505
+ if (cell.kind === "each" && !(cell.over in child.interface.inputs)) {
506
+ finding("invalid-wiring", "error", `Each ${cell.id} over names undeclared input ${cell.over} on ${child.key}.`, ref(cell.id))
507
+ }
508
+ // Carried inputs are fed by carry values, not edges; `over` gets its
509
+ // own dedicated check below.
510
+ const carried = cell.kind === "repeat" ? new Set(Object.values(cell.carry ?? {})) : new Set<string>()
511
+ for (const input of Object.keys(child.interface.inputs)) {
512
+ if (carried.has(input)) continue
513
+ if (cell.kind === "each" && input === cell.over) continue
514
+ const fed = manifest.edges.some((edge) => edge.to.cell === cell.id && edge.to.port === input)
515
+ if (!fed) {
516
+ finding("unfed-input", "warning", `${cell.kind} cell ${cell.id} leaves interface input ${input} of ${child.key} without a delivering edge; the input cell produces no value.`, ref(cell.id))
517
+ }
518
+ }
519
+ }
520
+ }
521
+ if (cell.kind === "each") {
522
+ const delivered = manifest.edges.some((edge) => edge.to.cell === cell.id && edge.to.port === cell.over)
523
+ if (!delivered) {
524
+ finding("invalid-wiring", "error", `Each ${cell.id} over port ${cell.over} has no delivering edge; the list can never arrive.`, ref(cell.id))
525
+ }
526
+ }
527
+ }
528
+
529
+ for (const [index, edge] of manifest.edges.entries()) {
530
+ if (!cellIds.has(edge.from.cell)) {
531
+ finding("invalid-wiring", "error", `Edge ${index} from references absent cell ${edge.from.cell}.`, manifest.key)
532
+ } else if (!outputPorts.get(edge.from.cell)?.has(edge.from.port)) {
533
+ finding("invalid-wiring", "error", `Edge ${index} names undeclared output port ${edge.from.port} on cell ${edge.from.cell}.`, ref(edge.from.cell))
534
+ }
535
+ if (!cellIds.has(edge.to.cell)) {
536
+ finding("invalid-wiring", "error", `Edge ${index} to references absent cell ${edge.to.cell}.`, manifest.key)
537
+ } else if (!inputPorts.get(edge.to.cell)?.has(edge.to.port)) {
538
+ finding("invalid-wiring", "error", `Edge ${index} names undeclared input port ${edge.to.port} on cell ${edge.to.cell}; input and const cells have no input ports.`, ref(edge.to.cell))
539
+ }
540
+ }
541
+ }
542
+
543
+ // Manifest-reference cycles can never terminate: nested invocation recurses
544
+ // until the runtime depth budget rejects the run. Flag them statically.
545
+ const visiting = new Set<string>()
546
+ const done = new Set<string>()
547
+ const detectCycles = (digest: string): void => {
548
+ if (done.has(digest)) return
549
+ if (visiting.has(digest)) {
550
+ const context = contexts.get(digest)
551
+ finding("cyclic-composition", "error", `Organism ${context?.manifest.key ?? digest.slice(0, 24)} participates in a manifest-reference cycle that cannot terminate within any depth budget.`, context?.manifest.key)
552
+ return
553
+ }
554
+ visiting.add(digest)
555
+ const context = contexts.get(digest)
556
+ if (context !== undefined) {
557
+ for (const cell of context.manifest.cells) {
558
+ if (cell.kind === "repeat" || cell.kind === "each" || cell.kind === "organism") detectCycles(cell.manifest)
559
+ }
560
+ }
561
+ visiting.delete(digest)
562
+ done.add(digest)
563
+ }
564
+ for (const digest of contexts.keys()) detectCycles(digest)
565
+
566
+ // Reachability from the entry manifest through composition cells.
567
+ const reachable = new Set<string>()
568
+ const walk = (digest: string): void => {
569
+ if (reachable.has(digest)) return
570
+ const context = contexts.get(digest)
571
+ if (context === undefined) return
572
+ reachable.add(digest)
573
+ for (const cell of context.manifest.cells) {
574
+ if (cell.kind === "repeat" || cell.kind === "each" || cell.kind === "organism") walk(cell.manifest)
575
+ }
576
+ }
577
+ walk(behavior.entry)
578
+ for (const [entryDigest, context] of contexts) {
579
+ if (!reachable.has(entryDigest)) {
580
+ finding("unreachable-organism", "warning", `Organism ${context.manifest.key} is in the closure but unreachable from the entry manifest.`, context.manifest.key)
581
+ }
582
+ }
583
+
584
+ // Entry args must bind declared interface inputs; unfed inputs warn.
585
+ const entryContext = contexts.get(behavior.entry)
586
+ if (entryContext !== undefined) {
587
+ const argNames = new Set(Object.keys(behavior.args ?? {}))
588
+ for (const name of Object.keys(behavior.args ?? {})) {
589
+ if (!(name in entryContext.manifest.interface.inputs)) {
590
+ finding("undeclared-arg", "error", `Arg ${name} is not a declared interface input of entry organism ${entryContext.manifest.key}.`, `${entryContext.manifest.key}/${name}`)
591
+ }
592
+ }
593
+ for (const name of Object.keys(entryContext.manifest.interface.inputs)) {
594
+ // `seed` is fed by convention: the bake binds behavior.seed to an entry
595
+ // interface input named `seed` when no arg supplies one.
596
+ if (!argNames.has(name) && name !== "seed") {
597
+ finding("unfed-input", "warning", `Entry organism ${entryContext.manifest.key} declares interface input ${name} but no arg binds it; the input cell produces no value.`, `${entryContext.manifest.key}/${name}`)
598
+ }
599
+ }
600
+ }
601
+
602
+ const errors = findings.filter((item) => item.severity === "error").length
603
+ return deepFreezeJson(SpatialBehaviorCheckReportSchema.parse({
604
+ kind: "slopcamera.spatial-behavior-check",
605
+ schemaVersion: 1,
606
+ behaviorSha256: spatialBehaviorSha256(behavior),
607
+ sceneSha256,
608
+ findings,
609
+ counts: {
610
+ organisms: Object.keys(behavior.organisms).length,
611
+ cells: cellTotal,
612
+ edges: edgeTotal,
613
+ fnRefs: fnRefTotal,
614
+ errors,
615
+ warnings: findings.length - errors,
616
+ },
617
+ }))
618
+ }