@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,252 @@
1
+ import { z } from "zod"
2
+
3
+ import { deepFreezeJson } from "../code/json-snapshot.js"
4
+ import { SpatialDigestSchema, SpatialTimeUsSchema } from "./contracts.js"
5
+ import { parseSpatialValue, spatialValueSha256 } from "./identity.js"
6
+ import type { SpatialBehaviorEmitted } from "./behavior-trace.js"
7
+
8
+ /**
9
+ * Behavior-trace audit: analyzes a baked emitted trace for pathological
10
+ * patterns that indicate a degenerate, trivially cyclic, or underspecified
11
+ * behavior. Findings are advisory — they inform gallery review and agent
12
+ * iteration, not automatic selection.
13
+ *
14
+ * Finding kinds:
15
+ * - `state-thrash` — a state channel transitions more than `maxTransitionsPerSecond`
16
+ * times within a one-second window, suggesting an underspecified dwell guard.
17
+ * - `exact-periodicity` — a channel's values repeat with a fixed period across
18
+ * the entire trace, suggesting a degenerate constant or trivial cycle.
19
+ * - `dead-channel` — a channel emits the same value at every tick, contributing
20
+ * no dynamic information to the performance.
21
+ * - `unreachable-state` — for FSM channels where the transition sub-channel
22
+ * is present, any state that appears in transitions as `to` but is never
23
+ * the current value is considered unreachable within this trace window.
24
+ */
25
+
26
+ export const SPATIAL_BEHAVIOR_AUDIT_LIMITS = Object.freeze({
27
+ findings: 128,
28
+ channels: 64,
29
+ })
30
+
31
+ export const SpatialBehaviorAuditFindingKindSchema = z.enum([
32
+ "state-thrash",
33
+ "exact-periodicity",
34
+ "dead-channel",
35
+ "unreachable-state",
36
+ ])
37
+ export type SpatialBehaviorAuditFindingKind = z.infer<typeof SpatialBehaviorAuditFindingKindSchema>
38
+
39
+ export const SpatialBehaviorAuditFindingSchema = z.strictObject({
40
+ kind: SpatialBehaviorAuditFindingKindSchema,
41
+ channel: z.string().min(1).max(64),
42
+ severity: z.enum(["warning", "info"]),
43
+ detail: z.string().min(1).max(240),
44
+ measured: z.number().finite().optional(),
45
+ limit: z.number().finite().optional(),
46
+ })
47
+ export type SpatialBehaviorAuditFinding = Readonly<z.infer<typeof SpatialBehaviorAuditFindingSchema>>
48
+
49
+ export const SpatialBehaviorAuditOptionsSchema = z.strictObject({
50
+ maxTransitionsPerSecond: z.number().int().min(1).max(1000).default(10),
51
+ minDistinctValues: z.number().int().min(2).max(100).default(2),
52
+ maxPeriodicityWindowUs: z.number().int().min(100_000).max(60_000_000).default(5_000_000),
53
+ })
54
+ export type SpatialBehaviorAuditOptions = Readonly<z.infer<typeof SpatialBehaviorAuditOptionsSchema>>
55
+
56
+ export const SpatialBehaviorAuditReportSchema = z.strictObject({
57
+ kind: z.literal("slopcamera.spatial-behavior-audit-report"),
58
+ schemaVersion: z.literal(1),
59
+ behaviorSha256: SpatialDigestSchema,
60
+ emittedSha256: SpatialDigestSchema,
61
+ rangeUs: z.strictObject({ startUs: SpatialTimeUsSchema, endUs: SpatialTimeUsSchema }),
62
+ emittedCount: z.number().int().min(0),
63
+ channelCount: z.number().int().min(0),
64
+ findings: z.array(SpatialBehaviorAuditFindingSchema).max(SPATIAL_BEHAVIOR_AUDIT_LIMITS.findings),
65
+ omittedFindings: z.number().int().min(0),
66
+ })
67
+ export type SpatialBehaviorAuditReport = Readonly<z.infer<typeof SpatialBehaviorAuditReportSchema>>
68
+
69
+ export function spatialBehaviorAuditReportSha256(report: SpatialBehaviorAuditReport): string {
70
+ return spatialValueSha256(report)
71
+ }
72
+
73
+ // -------------------------------------------------------------- internals ---
74
+
75
+ const valueKey = (value: unknown): string => {
76
+ if (typeof value === "string") return value
77
+ if (typeof value === "number") return `n:${value}`
78
+ if (typeof value === "boolean") return `b:${value ? 1 : 0}`
79
+ try { return JSON.stringify(value) } catch { return "\0" }
80
+ }
81
+
82
+ interface ChannelTrace {
83
+ readonly channel: string
84
+ readonly records: readonly SpatialBehaviorEmitted[]
85
+ readonly values: readonly string[]
86
+ readonly distinctValues: Set<string>
87
+ }
88
+
89
+ function groupByChannel(emitted: readonly SpatialBehaviorEmitted[]): Map<string, ChannelTrace> {
90
+ const groups = new Map<string, SpatialBehaviorEmitted[]>()
91
+ for (const record of emitted) {
92
+ let list = groups.get(record.channel)
93
+ if (list === undefined) { list = []; groups.set(record.channel, list) }
94
+ list.push(record)
95
+ }
96
+ const result = new Map<string, ChannelTrace>()
97
+ for (const [channel, records] of groups) {
98
+ const values = records.map((record) => valueKey(record.value))
99
+ result.set(channel, { channel, records, values, distinctValues: new Set(values) })
100
+ }
101
+ return result
102
+ }
103
+
104
+ function checkStateThrash(
105
+ trace: ChannelTrace,
106
+ maxTransitionsPerSecond: number,
107
+ findings: SpatialBehaviorAuditFinding[],
108
+ ): void {
109
+ const { records, values } = trace
110
+ if (records.length < 3) return
111
+ const windowUs = 1_000_000
112
+ let windowStart = 0
113
+ let transitions = 0
114
+ let maxSeen = 0
115
+ for (let i = 1; i < records.length; i += 1) {
116
+ if (values[i] !== values[i - 1]) transitions += 1
117
+ while (windowStart < i && records[i]!.tUs - records[windowStart]!.tUs > windowUs) {
118
+ if (values[windowStart + 1] !== values[windowStart]) transitions -= 1
119
+ windowStart += 1
120
+ }
121
+ maxSeen = Math.max(maxSeen, transitions)
122
+ }
123
+ if (maxSeen > maxTransitionsPerSecond) {
124
+ findings.push({
125
+ kind: "state-thrash", channel: trace.channel, severity: "warning",
126
+ detail: `${maxSeen} transitions/sec in a 1s window exceeds ${maxTransitionsPerSecond}/sec threshold.`,
127
+ measured: maxSeen, limit: maxTransitionsPerSecond,
128
+ })
129
+ }
130
+ }
131
+
132
+ function checkDeadChannel(
133
+ trace: ChannelTrace,
134
+ minDistinct: number,
135
+ findings: SpatialBehaviorAuditFinding[],
136
+ ): void {
137
+ if (trace.records.length < 2) return
138
+ if (trace.distinctValues.size < minDistinct) {
139
+ findings.push({
140
+ kind: "dead-channel", channel: trace.channel, severity: "info",
141
+ detail: `Channel emits ${trace.distinctValues.size} distinct value(s) across ${trace.records.length} records.`,
142
+ measured: trace.distinctValues.size, limit: minDistinct,
143
+ })
144
+ }
145
+ }
146
+
147
+ function checkExactPeriodicity(
148
+ trace: ChannelTrace,
149
+ maxWindowUs: number,
150
+ findings: SpatialBehaviorAuditFinding[],
151
+ ): void {
152
+ const { records, values } = trace
153
+ if (records.length < 6) return
154
+ const rangeUs = records[records.length - 1]!.tUs - records[0]!.tUs
155
+ if (rangeUs <= 0) return
156
+ // Check if the value sequence repeats with period P for P in [2, half-length].
157
+ const halfLen = Math.floor(values.length / 2)
158
+ const maxPeriod = Math.min(halfLen, Math.floor(values.length * maxWindowUs / rangeUs))
159
+ for (let period = 2; period <= maxPeriod; period += 1) {
160
+ let match = true
161
+ for (let i = period; i < values.length; i += 1) {
162
+ if (values[i] !== values[i - period]) { match = false; break }
163
+ }
164
+ if (match) {
165
+ findings.push({
166
+ kind: "exact-periodicity", channel: trace.channel, severity: "info",
167
+ detail: `Values repeat exactly every ${period} records — the behavior may be trivially cyclic.`,
168
+ measured: period,
169
+ })
170
+ return
171
+ }
172
+ }
173
+ }
174
+
175
+ function checkUnreachableStates(
176
+ channels: Map<string, ChannelTrace>,
177
+ findings: SpatialBehaviorAuditFinding[],
178
+ ): void {
179
+ for (const [channel, trace] of channels) {
180
+ const transitionChannel = `${channel}.transition`
181
+ const transitionTrace = channels.get(transitionChannel)
182
+ if (transitionTrace === undefined) continue
183
+
184
+ const visited = new Set<string>()
185
+ for (const value of trace.values) {
186
+ if (typeof value === "string") visited.add(value)
187
+ }
188
+
189
+ const referenced = new Set<string>()
190
+ for (const record of transitionTrace.records) {
191
+ const value = record.value
192
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
193
+ const typed = value as { to?: unknown; from?: unknown }
194
+ if (typeof typed.to === "string") referenced.add(typed.to)
195
+ if (typeof typed.from === "string") referenced.add(typed.from)
196
+ }
197
+ }
198
+
199
+ for (const state of referenced) {
200
+ if (!visited.has(state)) {
201
+ findings.push({
202
+ kind: "unreachable-state", channel, severity: "warning",
203
+ detail: `State "${state}" appears in ${transitionChannel} transitions but never as the current ${channel} value.`,
204
+ })
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ // ---------------------------------------------------------------- public ---
211
+
212
+ const auditInputSchema = z.strictObject({
213
+ emitted: z.array(z.strictObject({
214
+ tUs: SpatialTimeUsSchema,
215
+ channel: z.string().min(1).max(64),
216
+ value: z.unknown(),
217
+ })).max(16_384),
218
+ behaviorSha256: SpatialDigestSchema,
219
+ emittedSha256: SpatialDigestSchema,
220
+ rangeUs: z.strictObject({ startUs: SpatialTimeUsSchema, endUs: SpatialTimeUsSchema }),
221
+ options: SpatialBehaviorAuditOptionsSchema.optional(),
222
+ })
223
+
224
+ export function auditSpatialBehaviorTrace(input: unknown): SpatialBehaviorAuditReport {
225
+ const parsed = parseSpatialValue(auditInputSchema, input, "behavior audit")
226
+ const options = parsed.options ?? SpatialBehaviorAuditOptionsSchema.parse({})
227
+ const channels = groupByChannel(parsed.emitted)
228
+ const findings: SpatialBehaviorAuditFinding[] = []
229
+
230
+ for (const trace of channels.values()) {
231
+ if (findings.length >= SPATIAL_BEHAVIOR_AUDIT_LIMITS.findings) break
232
+ checkStateThrash(trace, options.maxTransitionsPerSecond, findings)
233
+ checkDeadChannel(trace, options.minDistinctValues, findings)
234
+ checkExactPeriodicity(trace, options.maxPeriodicityWindowUs, findings)
235
+ }
236
+ checkUnreachableStates(channels, findings)
237
+
238
+ const bounded = findings.slice(0, SPATIAL_BEHAVIOR_AUDIT_LIMITS.findings)
239
+ const omitted = findings.length - bounded.length
240
+
241
+ return deepFreezeJson(SpatialBehaviorAuditReportSchema.parse({
242
+ kind: "slopcamera.spatial-behavior-audit-report",
243
+ schemaVersion: 1,
244
+ behaviorSha256: parsed.behaviorSha256,
245
+ emittedSha256: parsed.emittedSha256,
246
+ rangeUs: parsed.rangeUs,
247
+ emittedCount: parsed.emitted.length,
248
+ channelCount: channels.size,
249
+ findings: bounded,
250
+ omittedFindings: omitted,
251
+ }))
252
+ }
@@ -0,0 +1,232 @@
1
+ import { z } from "zod"
2
+
3
+ import type {
4
+ FnRegistry,
5
+ JsonValue as AlgalJsonValue,
6
+ OrganismManifest,
7
+ PortMap as AlgalPortMap,
8
+ } from "@hraness/algal"
9
+
10
+ import { deepFreezeJson } from "../code/json-snapshot.js"
11
+ import {
12
+ checkSpatialBehavior,
13
+ parseSpatialBehavior,
14
+ spatialBehaviorSha256,
15
+ type SpatialBehavior,
16
+ type SpatialBehaviorCheckReport,
17
+ } from "./behavior.js"
18
+ import { SPATIAL_BEHAVIOR_FNS, spatialBehaviorFnSignatures } from "./behavior-fns.js"
19
+ import {
20
+ compileSpatialBehaviorDirectives,
21
+ parseSpatialBehaviorChannelMap,
22
+ parseSpatialBehaviorEmissions,
23
+ spatialBehaviorFnCatalogSha256,
24
+ SPATIAL_BEHAVIOR_TRACE_LIMITS,
25
+ SpatialBehaviorBakeSchema,
26
+ type SpatialBehaviorBake,
27
+ type SpatialBehaviorChannelMap,
28
+ type SpatialBehaviorEmitted,
29
+ } from "./behavior-trace.js"
30
+ import { parseSpatialScene, parseSpatialValue, spatialValueSha256, SpatialSceneError } from "./identity.js"
31
+
32
+ /**
33
+ * Deterministic behavior bake: an admitted `slopcamera.spatial-behavior`
34
+ * document runs through the pinned ALGAL runtime under the bake-safe profile
35
+ * — the closed pure fn catalog, a memory store, zero executors, no tools,
36
+ * transports, slots, or effects. The run receipt plus the emitted channel
37
+ * trace and compiled directive proposals form the retained
38
+ * `slopcamera.spatial-behavior-bake` artifact. Organism digests stored into
39
+ * the CAS are recomputed by ALGAL's own `manifestToJson`, so a drift between
40
+ * this mirror and the runtime fails at admission rather than mid-run.
41
+ */
42
+
43
+ const bakeOptionsSchema = z.strictObject({
44
+ behavior: z.unknown(),
45
+ scene: z.unknown(),
46
+ channelMap: z.unknown().optional(),
47
+ })
48
+
49
+ export interface SpatialBehaviorBakeResult {
50
+ readonly bake: SpatialBehaviorBake
51
+ readonly check: SpatialBehaviorCheckReport
52
+ }
53
+
54
+ const algalRegistry = (): FnRegistry => {
55
+ const registry: FnRegistry = new Map()
56
+ for (const [name, entry] of SPATIAL_BEHAVIOR_FNS) {
57
+ registry.set(name, {
58
+ signature: {
59
+ inputs: entry.signature.inputs as AlgalPortMap,
60
+ outputs: entry.signature.outputs as AlgalPortMap,
61
+ cost: entry.signature.cost,
62
+ },
63
+ fn: (inputs: Record<string, AlgalJsonValue>) => entry.invoke(inputs) as Record<string, AlgalJsonValue>,
64
+ })
65
+ }
66
+ return registry
67
+ }
68
+
69
+ const parseEntryManifest = (
70
+ behavior: SpatialBehavior,
71
+ parseOrganismManifest: (input: unknown) => OrganismManifest,
72
+ ): OrganismManifest => {
73
+ const organism = behavior.organisms[behavior.entry]
74
+ if (organism === undefined) {
75
+ throw new SpatialSceneError("invalid-data", `unresolved-entry: entry digest ${behavior.entry.slice(0, 24)}… is absent from the organisms closure.`, "behavior-bake")
76
+ }
77
+ try {
78
+ return parseOrganismManifest(organism)
79
+ } catch (error) {
80
+ throw new SpatialSceneError("invalid-data", `invalid-manifest: entry organism failed ALGAL manifest parsing: ${error instanceof Error ? error.message : String(error)}`, "behavior-bake")
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Maps declared interface-name args to ALGAL's cell-keyed run args. An entry
86
+ * interface input named `seed` binds `behavior.seed` by convention when no
87
+ * arg supplies one — the document-level seed feeds seeded kernels without
88
+ * duplicating it into args, and galleries vary it for candidate diversity.
89
+ */
90
+ const entryRunArgs = (behavior: SpatialBehavior, manifest: OrganismManifest): Record<string, Record<string, AlgalJsonValue>> => {
91
+ const args: Record<string, Record<string, AlgalJsonValue>> = {}
92
+ for (const [name, value] of Object.entries(behavior.args ?? {})) {
93
+ const endpoint = manifest.interface?.inputs[name]
94
+ if (endpoint === undefined) continue // `undeclared-arg` already an error finding
95
+ ;(args[endpoint.cell] ??= {})[endpoint.port] = value as AlgalJsonValue
96
+ }
97
+ const seedEndpoint = manifest.interface?.inputs.seed
98
+ if (seedEndpoint !== undefined && behavior.args?.seed === undefined) {
99
+ (args[seedEndpoint.cell] ??= {})[seedEndpoint.port] = behavior.seed
100
+ }
101
+ return args
102
+ }
103
+
104
+ export async function bakeSpatialBehavior(input: unknown): Promise<SpatialBehaviorBakeResult> {
105
+ const options = parseSpatialValue(bakeOptionsSchema, input, "behavior bake")
106
+ const behavior = parseSpatialBehavior(options.behavior)
107
+ const scene = parseSpatialScene(options.scene)
108
+ const sceneSha256 = spatialValueSha256(scene)
109
+ const signatures = spatialBehaviorFnSignatures()
110
+ const check = checkSpatialBehavior({ behavior, scene }, signatures)
111
+ const errors = check.findings.filter((finding) => finding.severity === "error")
112
+ if (errors.length > 0) {
113
+ const codes = [...new Set(errors.map((finding) => finding.code))].join(", ")
114
+ throw new SpatialSceneError("invalid-data", `behavior-check-failed: behavior ${behavior.behaviorId} failed admission with ${errors.length} error finding(s): ${codes}.`, "behavior-bake")
115
+ }
116
+
117
+ const channelMap: SpatialBehaviorChannelMap | undefined = options.channelMap === undefined
118
+ ? undefined
119
+ : parseSpatialBehaviorChannelMap(options.channelMap)
120
+ if (channelMap !== undefined) {
121
+ const declared = new Set(behavior.channels)
122
+ for (const channel of [...Object.keys(channelMap.clips ?? {}), ...(channelMap.trajectories ?? []),
123
+ ...(channelMap.attachments ?? []).flatMap((entry) => [entry.attachChannel, entry.releaseChannel])]) {
124
+ if (!declared.has(channel)) {
125
+ throw new SpatialSceneError("invalid-data", `undeclared-channel: channel map binds channel ${channel}, which behavior ${behavior.behaviorId} does not declare.`, "behavior-bake")
126
+ }
127
+ }
128
+ }
129
+
130
+ const { MemoryStore, parseOrganismManifest, runOrganism } = await import("@hraness/algal")
131
+
132
+ // Populate the CAS closure through ALGAL's own parser and digest: the
133
+ // stored digest must equal the closure key, proving runtime parity.
134
+ const store = new MemoryStore()
135
+ for (const [digest, organism] of Object.entries(behavior.organisms)) {
136
+ let manifest: OrganismManifest
137
+ try {
138
+ manifest = parseOrganismManifest(organism)
139
+ } catch (error) {
140
+ throw new SpatialSceneError("invalid-data", `invalid-manifest: organism ${organism.key} failed ALGAL manifest parsing: ${error instanceof Error ? error.message : String(error)}`, "behavior-bake")
141
+ }
142
+ const stored = await store.putManifest(manifest)
143
+ if (stored !== digest) {
144
+ throw new SpatialSceneError("invalid-data", `organism-digest-drift: organism ${organism.key} stored at ${stored.slice(0, 24)}… but the closure keys it at ${digest.slice(0, 24)}…; the ALGAL projection drifted from the admitted digest.`, "behavior-bake")
145
+ }
146
+ }
147
+
148
+ const manifest = parseEntryManifest(behavior, parseOrganismManifest)
149
+ const args = entryRunArgs(behavior, manifest)
150
+ const receipt = await runOrganism({
151
+ manifest,
152
+ args,
153
+ fns: algalRegistry(),
154
+ store,
155
+ executors: [],
156
+ })
157
+
158
+ if (receipt.manifestDigest !== behavior.entry) {
159
+ throw new SpatialSceneError("invalid-data", `organism-digest-drift: run manifest digest ${receipt.manifestDigest.slice(0, 24)}… does not match entry ${behavior.entry.slice(0, 24)}….`, "behavior-bake")
160
+ }
161
+ if (receipt.effects.length > 0 || receipt.work.agentCalls > 0) {
162
+ throw new SpatialSceneError("invalid-data", `effect-leak: bake-safe run recorded ${receipt.effects.length} effect(s) and ${receipt.work.agentCalls} agent call(s); the profile admits none.`, "behavior-bake")
163
+ }
164
+ if (receipt.outcome !== "complete") {
165
+ const failure = receipt.failure
166
+ throw new SpatialSceneError("invalid-data", `behavior-run-failed: ALGAL run ${receipt.outcome}${failure === undefined ? "" : `: ${failure.code} ${failure.message}`}`, "behavior-bake")
167
+ }
168
+
169
+ // Entry interface outputs are the bake surface: every declared output
170
+ // port carries a bounded emitted-record array. Composition internals
171
+ // (carried state, per-round records) stay inside the receipt.
172
+ const declaredChannels = new Set(behavior.channels)
173
+ const emitted: SpatialBehaviorEmitted[] = []
174
+ const outputs = manifest.interface?.outputs ?? {}
175
+ for (const name of Object.keys(outputs).sort()) {
176
+ const endpoint = outputs[name]!
177
+ const record = receipt.cells[endpoint.cell]
178
+ if (record === undefined || record.status !== "committed" || record.outputs === undefined) {
179
+ throw new SpatialSceneError("invalid-data", `emission-missing: interface output ${name} maps to cell ${endpoint.cell}, which did not commit an output.`, "behavior-bake")
180
+ }
181
+ const records = parseSpatialBehaviorEmissions(record.outputs[endpoint.port], `interface output ${name}`)
182
+ for (const emittedRecord of records) {
183
+ if (!declaredChannels.has(emittedRecord.channel)) {
184
+ throw new SpatialSceneError("invalid-data", `undeclared-channel: emission on channel ${emittedRecord.channel}, which behavior ${behavior.behaviorId} does not declare.`, "behavior-bake")
185
+ }
186
+ if (emittedRecord.tUs < behavior.rangeUs.startUs || emittedRecord.tUs > behavior.rangeUs.endUs) {
187
+ throw new SpatialSceneError("invalid-data", `emission-outside-range: emission on channel ${emittedRecord.channel} at ${emittedRecord.tUs}us is outside the declared range ${behavior.rangeUs.startUs}–${behavior.rangeUs.endUs}us.`, "behavior-bake")
188
+ }
189
+ }
190
+ emitted.push(...records)
191
+ }
192
+ if (emitted.length > SPATIAL_BEHAVIOR_TRACE_LIMITS.emitted) {
193
+ throw new SpatialSceneError("invalid-data", `over-bound: run emitted ${emitted.length} records beyond ${SPATIAL_BEHAVIOR_TRACE_LIMITS.emitted}.`, "behavior-bake")
194
+ }
195
+ emitted.sort((a, b) => a.tUs - b.tUs || a.channel.localeCompare(b.channel))
196
+
197
+ const { directives, unresolvedIntents } = compileSpatialBehaviorDirectives({
198
+ emitted,
199
+ ...(channelMap === undefined ? {} : { channelMap }),
200
+ rangeUs: behavior.rangeUs,
201
+ })
202
+
203
+ const behaviorSha256 = spatialBehaviorSha256(behavior)
204
+ const bake = deepFreezeJson(SpatialBehaviorBakeSchema.parse({
205
+ kind: "slopcamera.spatial-behavior-bake",
206
+ schemaVersion: 1,
207
+ behaviorSha256,
208
+ sceneSha256,
209
+ entry: behavior.entry,
210
+ seed: behavior.seed,
211
+ rangeUs: behavior.rangeUs,
212
+ emitted,
213
+ directives,
214
+ unresolvedIntents,
215
+ receipt: {
216
+ kind: "slopcamera.spatial-behavior-bake-receipt",
217
+ schemaVersion: 1,
218
+ behaviorSha256,
219
+ sceneSha256,
220
+ entry: behavior.entry,
221
+ manifestDigest: receipt.manifestDigest,
222
+ runDigest: receipt.digest,
223
+ runtime: { name: receipt.runtime.name, version: receipt.runtime.version },
224
+ fnCatalogSha256: spatialBehaviorFnCatalogSha256(SPATIAL_BEHAVIOR_FNS),
225
+ emittedSha256: spatialValueSha256(emitted),
226
+ ...(channelMap === undefined ? {} : { channelMapSha256: spatialValueSha256(channelMap) }),
227
+ outcome: receipt.outcome,
228
+ work: receipt.work,
229
+ },
230
+ }))
231
+ return { bake, check }
232
+ }