@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,585 @@
1
+ import { z } from "zod"
2
+
3
+ import type { JsonValue } from "../code/contracts.js"
4
+ import type { SpatialBehaviorFnSignature, SpatialBehaviorPortMap } from "./behavior.js"
5
+
6
+ /**
7
+ * The closed behavior-fn catalog organisms compose.
8
+ *
9
+ * Every entry is a pure, total-over-its-domain kernel: `(inputs) => outputs`
10
+ * over bounded JSON, with no ambient state, clock, IO, or randomness beyond
11
+ * the seeded draw streams its spec declares. The same function invoked twice
12
+ * on equal inputs returns deeply equal outputs — replay determinism is the
13
+ * contract that lets baked takes and receipts verify bit-for-bit.
14
+ *
15
+ * Conventions shared across the catalog:
16
+ * - `window` — `{ticks: [{tUs, obs?}]}` with strictly increasing integer
17
+ * microsecond times. The host bake loop chunks dense simulation into
18
+ * windows so organisms need few `repeat` rounds.
19
+ * - `emitted` — `[{tUs, channel, value}]` channel records; the uniform
20
+ * vocabulary the take compiler maps onto performance directives.
21
+ * - `state` — opaque bounded JSON threaded by `carry`; kernels validate it
22
+ * before use and reject foreign shapes with a named error.
23
+ */
24
+ export const SPATIAL_BEHAVIOR_FN_LIMITS = Object.freeze({
25
+ drawsPerCall: 256,
26
+ emittedPerCall: 4096,
27
+ fsmStates: 16,
28
+ fsmTransitions: 64,
29
+ gazeTargets: 8,
30
+ layers: 8,
31
+ moodChannels: 8,
32
+ obsFlags: 32,
33
+ pairs: 8,
34
+ phases: 8,
35
+ positionsPerTick: 8,
36
+ ticksPerWindow: 512,
37
+ })
38
+
39
+ export class SpatialBehaviorFnError extends Error {
40
+ readonly code: "invalid-input" | "invalid-state" | "over-bound"
41
+ constructor(code: "invalid-input" | "invalid-state" | "over-bound", detail: string) {
42
+ super(detail)
43
+ this.name = "SpatialBehaviorFnError"
44
+ this.code = code
45
+ }
46
+ }
47
+
48
+ type JsonObject = Record<string, JsonValue>
49
+
50
+ interface SpatialBehaviorEmitted {
51
+ readonly tUs: number
52
+ readonly channel: string
53
+ readonly value: JsonValue
54
+ }
55
+
56
+ export interface SpatialBehaviorFn {
57
+ readonly signature: SpatialBehaviorFnSignature & { readonly cost: number }
58
+ readonly invoke: (inputs: Readonly<Record<string, JsonValue>>) => Record<string, JsonValue>
59
+ }
60
+
61
+ const uint = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER)
62
+ const channelName = z.string().regex(/^[a-z][a-z0-9_.-]{0,62}$/u)
63
+ const kebabName = z.string().regex(/^[a-z][a-z0-9-]{0,62}$/u)
64
+
65
+ function parseSpec<Schema extends z.ZodType>(schema: Schema, input: JsonValue | undefined, fn: string, field: string): z.infer<Schema> {
66
+ const parsed = schema.safeParse(input ?? null)
67
+ if (!parsed.success) {
68
+ const issue = parsed.error.issues[0]
69
+ throw new SpatialBehaviorFnError(
70
+ field === "state" ? "invalid-state" : "invalid-input",
71
+ `${fn}: ${field} invalid — ${issue?.message ?? "schema mismatch"}`,
72
+ )
73
+ }
74
+ return parsed.data
75
+ }
76
+
77
+ function requireField(inputs: Readonly<Record<string, JsonValue>>, name: string, fn: string): JsonValue {
78
+ const value = inputs[name]
79
+ if (value === undefined) throw new SpatialBehaviorFnError("invalid-input", `${fn}: missing required input ${name}`)
80
+ return value
81
+ }
82
+
83
+ // ------------------------------------------------------------- primitives ---
84
+
85
+ /** mulberry32 — deterministic 32-bit seeded draw stream. */
86
+ function rngNext(state: number): { value: number; next: number } {
87
+ const next = (state + 0x6d2b79f5) >>> 0
88
+ let t = next
89
+ t = Math.imul(t ^ (t >>> 15), t | 1)
90
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
91
+ return { value: ((t ^ (t >>> 14)) >>> 0) / 4294967296, next }
92
+ }
93
+
94
+ function drawStream(seed: number, count: number): { draws: number[]; next: number } {
95
+ const draws: number[] = []
96
+ let state = seed >>> 0
97
+ for (let i = 0; i < count; i += 1) {
98
+ const drawn = rngNext(state)
99
+ draws.push(drawn.value)
100
+ state = drawn.next
101
+ }
102
+ return { draws, next: state }
103
+ }
104
+
105
+ const obsSchema = z.strictObject({
106
+ draw: z.number().min(0).max(1).optional(),
107
+ flags: z.record(kebabName, z.boolean()).refine((flags) => Object.keys(flags).length <= SPATIAL_BEHAVIOR_FN_LIMITS.obsFlags, {
108
+ message: `At most ${SPATIAL_BEHAVIOR_FN_LIMITS.obsFlags} obs flags per tick.`,
109
+ }).optional(),
110
+ distances: z.record(kebabName, z.number().min(0)).refine((distances) => Object.keys(distances).length <= SPATIAL_BEHAVIOR_FN_LIMITS.obsFlags, {
111
+ message: `At most ${SPATIAL_BEHAVIOR_FN_LIMITS.obsFlags} obs distances per tick.`,
112
+ }).optional(),
113
+ })
114
+
115
+ const windowSchema = z.strictObject({
116
+ ticks: z.array(z.strictObject({
117
+ tUs: uint,
118
+ obs: obsSchema.optional(),
119
+ positions: z.record(kebabName, z.tuple([z.number(), z.number(), z.number()])).refine(
120
+ (positions) => Object.keys(positions).length <= SPATIAL_BEHAVIOR_FN_LIMITS.positionsPerTick,
121
+ { message: `At most ${SPATIAL_BEHAVIOR_FN_LIMITS.positionsPerTick} positions per tick.` },
122
+ ).optional(),
123
+ })).min(1).max(SPATIAL_BEHAVIOR_FN_LIMITS.ticksPerWindow),
124
+ }).superRefine((window, context) => {
125
+ for (let i = 1; i < window.ticks.length; i += 1) {
126
+ const previous = window.ticks[i - 1]?.tUs ?? 0
127
+ const current = window.ticks[i]?.tUs ?? 0
128
+ if (current <= previous) {
129
+ context.addIssue({ code: "custom", message: "window.ticks must be strictly increasing in tUs.", path: ["ticks", i, "tUs"] })
130
+ }
131
+ }
132
+ })
133
+ const emittedSchema = z.array(z.strictObject({
134
+ tUs: uint,
135
+ channel: channelName,
136
+ value: z.unknown(),
137
+ })).max(SPATIAL_BEHAVIOR_FN_LIMITS.emittedPerCall)
138
+
139
+ function emit(emitted: SpatialBehaviorEmitted[], tUs: number, channel: string, value: JsonValue): void {
140
+ if (emitted.length >= SPATIAL_BEHAVIOR_FN_LIMITS.emittedPerCall) {
141
+ throw new SpatialBehaviorFnError("over-bound", `Emitted records exceed ${SPATIAL_BEHAVIOR_FN_LIMITS.emittedPerCall} per call.`)
142
+ }
143
+ emitted.push({ tUs, channel, value })
144
+ }
145
+
146
+ // ------------------------------------------------------------ rng.seeded ---
147
+
148
+ const rngInputs = z.strictObject({
149
+ seed: uint,
150
+ count: z.number().int().min(1).max(SPATIAL_BEHAVIOR_FN_LIMITS.drawsPerCall),
151
+ stream: uint.optional(),
152
+ })
153
+
154
+ function rngSeeded(inputs: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
155
+ const spec = parseSpec(rngInputs, { seed: inputs.seed, count: inputs.count, stream: inputs.stream } as JsonObject, "rng.seeded.v1", "inputs")
156
+ const salted = (spec.seed ^ Math.imul(spec.stream ?? 0, 0x9e3779b9)) >>> 0
157
+ const { draws, next } = drawStream(salted, spec.count)
158
+ return { draws, next }
159
+ }
160
+
161
+ // ---------------------------------------------------------- behavior.fsm ---
162
+
163
+ const fsmGuardSchema = z.discriminatedUnion("kind", [
164
+ z.strictObject({ kind: z.literal("after"), us: uint }),
165
+ z.strictObject({ kind: z.literal("chance"), threshold: z.number().min(0).max(1) }),
166
+ z.strictObject({ kind: z.literal("flag"), name: kebabName }),
167
+ z.strictObject({ kind: z.literal("clear"), name: kebabName }),
168
+ z.strictObject({ kind: z.literal("always") }),
169
+ ])
170
+
171
+ const fsmMachineSchema = z.strictObject({
172
+ channel: channelName,
173
+ states: z.array(kebabName).min(1).max(SPATIAL_BEHAVIOR_FN_LIMITS.fsmStates),
174
+ transitions: z.array(z.strictObject({
175
+ from: kebabName,
176
+ to: kebabName,
177
+ guard: fsmGuardSchema,
178
+ })).max(SPATIAL_BEHAVIOR_FN_LIMITS.fsmTransitions),
179
+ minDwellUs: uint.optional(),
180
+ }).superRefine((machine, context) => {
181
+ const states = new Set(machine.states)
182
+ if (states.size !== machine.states.length) {
183
+ context.addIssue({ code: "custom", message: "machine.states must be unique.", path: ["states"] })
184
+ }
185
+ for (const [index, transition] of machine.transitions.entries()) {
186
+ if (!states.has(transition.from)) context.addIssue({ code: "custom", message: `Transition from undeclared state ${transition.from}.`, path: ["transitions", index, "from"] })
187
+ if (!states.has(transition.to)) context.addIssue({ code: "custom", message: `Transition to undeclared state ${transition.to}.`, path: ["transitions", index, "to"] })
188
+ }
189
+ })
190
+ const fsmStateSchema = z.strictObject({ name: kebabName, enteredUs: uint })
191
+
192
+ function guardPasses(guard: z.infer<typeof fsmGuardSchema>, elapsedUs: number, obs: { draw?: number | undefined; flags?: Record<string, boolean> | undefined; distances?: Record<string, number> | undefined } | undefined, fn: string): boolean {
193
+ switch (guard.kind) {
194
+ case "after":
195
+ return elapsedUs >= guard.us
196
+ case "chance": {
197
+ if (obs?.draw === undefined) {
198
+ throw new SpatialBehaviorFnError("invalid-input", `${fn}: a chance guard fired with no obs.draw supplied for the tick`)
199
+ }
200
+ return obs.draw < guard.threshold
201
+ }
202
+ case "flag":
203
+ return obs?.flags?.[guard.name] === true
204
+ case "clear":
205
+ return obs?.flags?.[guard.name] !== true
206
+ case "always":
207
+ return true
208
+ }
209
+ }
210
+
211
+ function behaviorFsm(inputs: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
212
+ const machine = parseSpec(fsmMachineSchema, inputs.machine, "behavior.fsm.v1", "machine")
213
+ const state = parseSpec(fsmStateSchema, requireField(inputs, "state", "behavior.fsm.v1"), "behavior.fsm.v1", "state")
214
+ const window = parseSpec(windowSchema, requireField(inputs, "window", "behavior.fsm.v1"), "behavior.fsm.v1", "window")
215
+ if (!machine.states.includes(state.name)) {
216
+ throw new SpatialBehaviorFnError("invalid-state", `behavior.fsm.v1: state ${state.name} is not declared in machine.states`)
217
+ }
218
+ const emitted: SpatialBehaviorEmitted[] = []
219
+ let current: { name: string; enteredUs: number } = { ...state }
220
+ for (const tick of window.ticks) {
221
+ const elapsedUs = Math.max(0, tick.tUs - current.enteredUs)
222
+ if (machine.minDwellUs === undefined || elapsedUs >= machine.minDwellUs) {
223
+ const fired = machine.transitions.find((transition) => transition.from === current.name && guardPasses(transition.guard, elapsedUs, tick.obs, "behavior.fsm.v1"))
224
+ if (fired !== undefined) {
225
+ emit(emitted, tick.tUs, `${machine.channel}.transition`, { from: current.name, to: fired.to, guard: fired.guard.kind })
226
+ current = { name: fired.to, enteredUs: tick.tUs }
227
+ }
228
+ }
229
+ emit(emitted, tick.tUs, machine.channel, current.name)
230
+ }
231
+ return { next: current as JsonObject, emitted: emitted as unknown as JsonValue[] as JsonObject[] }
232
+ }
233
+
234
+ // ---------------------------------------------------- behavior.expression ---
235
+
236
+ const expressionSpecSchema = z.strictObject({
237
+ channel: channelName,
238
+ seed: uint,
239
+ blinkEveryUs: z.number().int().min(100_000).max(60_000_000),
240
+ blinkUs: z.number().int().min(10_000).max(1_000_000),
241
+ saccadeEveryUs: z.number().int().min(50_000).max(60_000_000),
242
+ gazeTargets: z.array(kebabName).min(1).max(SPATIAL_BEHAVIOR_FN_LIMITS.gazeTargets),
243
+ mood: z.record(kebabName, z.number().min(0).max(1)).refine((mood) => Object.keys(mood).length <= SPATIAL_BEHAVIOR_FN_LIMITS.moodChannels, {
244
+ message: `At most ${SPATIAL_BEHAVIOR_FN_LIMITS.moodChannels} mood channels.`,
245
+ }),
246
+ })
247
+
248
+ const expressionStateSchema = z.strictObject({
249
+ rng: uint,
250
+ nextBlinkUs: uint,
251
+ nextSaccadeUs: uint,
252
+ gazeIndex: z.number().int().min(0).max(SPATIAL_BEHAVIOR_FN_LIMITS.gazeTargets - 1),
253
+ })
254
+
255
+ function behaviorExpression(inputs: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
256
+ const spec = parseSpec(expressionSpecSchema, inputs.spec, "behavior.expression.v1", "spec")
257
+ const state = parseSpec(expressionStateSchema, requireField(inputs, "state", "behavior.expression.v1"), "behavior.expression.v1", "state")
258
+ const window = parseSpec(windowSchema, requireField(inputs, "window", "behavior.expression.v1"), "behavior.expression.v1", "window")
259
+ if (state.gazeIndex >= spec.gazeTargets.length) {
260
+ throw new SpatialBehaviorFnError("invalid-state", `behavior.expression.v1: state.gazeIndex ${state.gazeIndex} exceeds gazeTargets`)
261
+ }
262
+ const emitted: SpatialBehaviorEmitted[] = []
263
+ let { rng, nextBlinkUs, nextSaccadeUs, gazeIndex } = state
264
+ for (const tick of window.ticks) {
265
+ if (tick.tUs >= nextSaccadeUs) {
266
+ const draw = rngNext(rng)
267
+ rng = draw.next
268
+ const step = spec.gazeTargets.length === 1 ? 0 : 1 + Math.floor(draw.value * (spec.gazeTargets.length - 1))
269
+ gazeIndex = (gazeIndex + step) % spec.gazeTargets.length
270
+ const jitter = rngNext(rng)
271
+ rng = jitter.next
272
+ nextSaccadeUs = tick.tUs + spec.saccadeEveryUs + Math.floor(jitter.value * spec.saccadeEveryUs * 0.5)
273
+ }
274
+ if (tick.tUs >= nextBlinkUs + spec.blinkUs) {
275
+ const jitter = rngNext(rng)
276
+ rng = jitter.next
277
+ nextBlinkUs = tick.tUs + spec.blinkEveryUs + Math.floor(jitter.value * spec.blinkEveryUs * 0.5)
278
+ }
279
+ const blinking = tick.tUs >= nextBlinkUs && tick.tUs < nextBlinkUs + spec.blinkUs
280
+ emit(emitted, tick.tUs, `${spec.channel}.blink`, blinking ? 1 : 0)
281
+ emit(emitted, tick.tUs, `${spec.channel}.gaze`, spec.gazeTargets[gazeIndex] ?? spec.gazeTargets[0] ?? "center")
282
+ emit(emitted, tick.tUs, `${spec.channel}.mood`, spec.mood as JsonObject)
283
+ }
284
+ return { next: { rng, nextBlinkUs, nextSaccadeUs, gazeIndex } as JsonObject, emitted: emitted as unknown as JsonValue[] as JsonObject[] }
285
+ }
286
+
287
+ // ----------------------------------------------------- behavior.interact ---
288
+
289
+ const interactSpecSchema = z.strictObject({
290
+ channel: channelName,
291
+ phases: z.array(z.strictObject({
292
+ name: kebabName,
293
+ minUs: uint,
294
+ emit: z.array(z.strictObject({ channel: channelName, value: z.unknown() })).max(8).optional(),
295
+ })).min(1).max(SPATIAL_BEHAVIOR_FN_LIMITS.phases),
296
+ loop: z.boolean().optional(),
297
+ })
298
+
299
+ const interactStateSchema = z.strictObject({
300
+ phaseIndex: z.number().int().min(0).max(SPATIAL_BEHAVIOR_FN_LIMITS.phases - 1),
301
+ enteredUs: uint,
302
+ })
303
+
304
+ function behaviorInteract(inputs: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
305
+ const spec = parseSpec(interactSpecSchema, inputs.spec, "behavior.interact.v1", "spec")
306
+ const state = parseSpec(interactStateSchema, requireField(inputs, "state", "behavior.interact.v1"), "behavior.interact.v1", "state")
307
+ const window = parseSpec(windowSchema, requireField(inputs, "window", "behavior.interact.v1"), "behavior.interact.v1", "window")
308
+ if (state.phaseIndex >= spec.phases.length) {
309
+ throw new SpatialBehaviorFnError("invalid-state", `behavior.interact.v1: state.phaseIndex ${state.phaseIndex} exceeds phases`)
310
+ }
311
+ const emitted: SpatialBehaviorEmitted[] = []
312
+ let { phaseIndex, enteredUs } = state
313
+ for (const tick of window.ticks) {
314
+ let advanced = true
315
+ while (advanced) {
316
+ advanced = false
317
+ const phase = spec.phases[phaseIndex]
318
+ if (phase !== undefined && tick.tUs - enteredUs >= phase.minUs) {
319
+ const last = phaseIndex + 1 >= spec.phases.length
320
+ if (!last || spec.loop === true) {
321
+ const nextIndex = last ? 0 : phaseIndex + 1
322
+ const entered = spec.phases[nextIndex]
323
+ emit(emitted, tick.tUs, `${spec.channel}.phase`, { from: phase.name, to: entered?.name ?? phase.name })
324
+ for (const record of entered?.emit ?? []) {
325
+ emit(emitted, tick.tUs, record.channel, record.value as JsonValue)
326
+ }
327
+ phaseIndex = nextIndex
328
+ enteredUs = tick.tUs
329
+ advanced = true
330
+ }
331
+ }
332
+ }
333
+ emit(emitted, tick.tUs, spec.channel, spec.phases[phaseIndex]?.name ?? "done")
334
+ }
335
+ return { next: { phaseIndex, enteredUs } as JsonObject, emitted: emitted as unknown as JsonValue[] as JsonObject[] }
336
+ }
337
+
338
+ // ------------------------------------------------------------ channel.emit ---
339
+
340
+ function channelEmit(inputs: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
341
+ const channel = parseSpec(channelName, inputs.channel, "channel.emit.v1", "channel")
342
+ const window = parseSpec(windowSchema, requireField(inputs, "window", "channel.emit.v1"), "channel.emit.v1", "window")
343
+ const value = inputs.value ?? null
344
+ const emitted: SpatialBehaviorEmitted[] = []
345
+ for (const tick of window.ticks) emit(emitted, tick.tUs, channel, value)
346
+ return { emitted: emitted as unknown as JsonValue[] as JsonObject[] }
347
+ }
348
+
349
+ // ------------------------------------------------------------ scene.sample ---
350
+
351
+ const sampleSpecSchema = z.strictObject({
352
+ pairs: z.array(z.strictObject({
353
+ a: kebabName,
354
+ b: kebabName,
355
+ within: z.number().min(0).max(100),
356
+ })).min(1).max(SPATIAL_BEHAVIOR_FN_LIMITS.pairs),
357
+ })
358
+
359
+ function sceneSample(inputs: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
360
+ const spec = parseSpec(sampleSpecSchema, inputs.spec, "scene.sample.v1", "spec")
361
+ const window = parseSpec(windowSchema, requireField(inputs, "window", "scene.sample.v1"), "scene.sample.v1", "window")
362
+ const ticks = window.ticks.map((tick) => {
363
+ const positions = tick.positions ?? {}
364
+ const flags: Record<string, boolean> = {}
365
+ const distances: Record<string, number> = {}
366
+ for (const pair of spec.pairs) {
367
+ const a = positions[pair.a]
368
+ const b = positions[pair.b]
369
+ if (a === undefined || b === undefined) {
370
+ throw new SpatialBehaviorFnError("invalid-input", `scene.sample.v1: pair ${pair.a}/${pair.b} missing positions at ${tick.tUs}us`)
371
+ }
372
+ const distance = Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2])
373
+ const key = `${pair.a}-${pair.b}`
374
+ distances[`dist-${key}`] = Math.round(distance * 1_000_000) / 1_000_000
375
+ flags[`contact-${key}`] = distance <= pair.within
376
+ }
377
+ const obs = {
378
+ ...(tick.obs ?? {}),
379
+ flags: { ...(tick.obs?.flags ?? {}), ...flags },
380
+ distances: { ...(tick.obs?.distances ?? {}), ...distances },
381
+ }
382
+ return { tUs: tick.tUs, obs }
383
+ })
384
+ return { window: { ticks: ticks } as JsonObject }
385
+ }
386
+
387
+ // --------------------------------------------------------- behavior.combine ---
388
+
389
+ const combineInputs = z.strictObject({
390
+ layers: z.array(z.strictObject({
391
+ priority: z.number().int().min(0).max(1_000),
392
+ emitted: emittedSchema,
393
+ })).min(1).max(SPATIAL_BEHAVIOR_FN_LIMITS.layers),
394
+ })
395
+
396
+ function behaviorCombine(inputs: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
397
+ const spec = parseSpec(combineInputs, { layers: inputs.layers } as JsonObject, "behavior.combine.v1", "layers")
398
+ const winners = new Map<string, { priority: number; order: number; record: { tUs: number; channel: string; value: JsonValue } }>()
399
+ spec.layers.forEach((layer, layerIndex) => {
400
+ for (const record of layer.emitted) {
401
+ const key = `${record.tUs}${record.channel}`
402
+ const held = winners.get(key)
403
+ if (held === undefined || layer.priority > held.priority) {
404
+ winners.set(key, { priority: layer.priority, order: layerIndex, record: record as { tUs: number; channel: string; value: JsonValue } })
405
+ }
406
+ }
407
+ })
408
+ const merged = [...winners.values()]
409
+ .sort((left, right) => left.record.tUs - right.record.tUs
410
+ || (left.record.channel < right.record.channel ? -1 : left.record.channel > right.record.channel ? 1 : 0)
411
+ || left.order - right.order)
412
+ .map((entry) => entry.record)
413
+ if (merged.length > SPATIAL_BEHAVIOR_FN_LIMITS.emittedPerCall) {
414
+ throw new SpatialBehaviorFnError("over-bound", `behavior.combine.v1: merged emissions exceed ${SPATIAL_BEHAVIOR_FN_LIMITS.emittedPerCall}`)
415
+ }
416
+ return { emitted: merged as unknown as JsonValue[] as JsonObject[] }
417
+ }
418
+
419
+ // --------------------------------------------------------- emitted.append ---
420
+
421
+ const appendInputs = z.strictObject({
422
+ prior: emittedSchema,
423
+ fresh: emittedSchema,
424
+ })
425
+
426
+ /**
427
+ * Concatenates two emitted-record arrays into one sorted trace — the
428
+ * carry-accumulation primitive. `repeat` rounds surface only final outputs,
429
+ * so organisms thread `acc` through `carry` and append each round's fresh
430
+ * records here. Ordering is by `(tUs, channel)` with prior records winning
431
+ * ties, keeping duplicates for downstream priority merging.
432
+ */
433
+ function emittedAppend(inputs: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
434
+ const spec = parseSpec(appendInputs, { prior: inputs.prior, fresh: inputs.fresh } as JsonObject, "emitted.append.v1", "prior")
435
+ const merged = [...spec.prior, ...spec.fresh]
436
+ .sort((left, right) => left.tUs - right.tUs || (left.channel < right.channel ? -1 : left.channel > right.channel ? 1 : 0))
437
+ if (merged.length > SPATIAL_BEHAVIOR_FN_LIMITS.emittedPerCall) {
438
+ throw new SpatialBehaviorFnError("over-bound", `emitted.append.v1: accumulated emissions exceed ${SPATIAL_BEHAVIOR_FN_LIMITS.emittedPerCall}`)
439
+ }
440
+ return { emitted: merged as unknown as JsonValue[] as JsonObject[] }
441
+ }
442
+
443
+ // -------------------------------------------------------- emitted.flatten ---
444
+
445
+ const flattenInputs = z.strictObject({
446
+ nested: z.array(emittedSchema).max(SPATIAL_BEHAVIOR_FN_LIMITS.layers),
447
+ })
448
+
449
+ /**
450
+ * Flattens one level of nested emitted arrays — the `each` counterpart of
451
+ * `emitted.append.v1`. An `each` cell collects its child's interface output
452
+ * per item, so a child emitting `emitted[]` surfaces `emitted[][]`; this
453
+ * kernel flattens it into one sorted trace.
454
+ */
455
+ function emittedFlatten(inputs: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
456
+ const spec = parseSpec(flattenInputs, { nested: inputs.nested } as JsonObject, "emitted.flatten.v1", "nested")
457
+ const merged = spec.nested.flat()
458
+ .sort((left, right) => left.tUs - right.tUs || (left.channel < right.channel ? -1 : left.channel > right.channel ? 1 : 0))
459
+ if (merged.length > SPATIAL_BEHAVIOR_FN_LIMITS.emittedPerCall) {
460
+ throw new SpatialBehaviorFnError("over-bound", `emitted.flatten.v1: flattened emissions exceed ${SPATIAL_BEHAVIOR_FN_LIMITS.emittedPerCall}`)
461
+ }
462
+ return { emitted: merged as unknown as JsonValue[] as JsonObject[] }
463
+ }
464
+
465
+ // --------------------------------------------------------- window.advance ---
466
+
467
+ const advanceInputs = z.strictObject({
468
+ window: windowSchema,
469
+ step: uint.min(1).max(SPATIAL_BEHAVIOR_FN_LIMITS.ticksPerWindow),
470
+ })
471
+
472
+ /**
473
+ * Splits a tick window into a `head` chunk of the first `step` ticks and the
474
+ * `rest` tail — the carry-threading primitive for sequential automata. A
475
+ * `repeat` cell feeds identical inputs each round, so a windowed organism
476
+ * carries `rest → window`: each round advances through the timeline instead
477
+ * of reprocessing the same ticks. `done` is `"true"` once `rest` is empty so
478
+ * `until` can terminate the loop on a string compare.
479
+ */
480
+ function windowAdvance(inputs: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
481
+ const spec = parseSpec(advanceInputs, { window: inputs.window, step: inputs.step } as JsonObject, "window.advance.v1", "window")
482
+ const head = spec.window.ticks.slice(0, spec.step)
483
+ const rest = spec.window.ticks.slice(spec.step)
484
+ return {
485
+ head: { ticks: head } as unknown as JsonValue,
486
+ rest: { ticks: rest } as unknown as JsonValue,
487
+ done: (rest.length === 0 ? "true" : "false") as JsonValue,
488
+ }
489
+ }
490
+
491
+ // ---------------------------------------------------------------- catalog ---
492
+
493
+ const ports = (map: Record<string, { type: "text" | "json" | "choice" | "ref"; optional?: boolean; many?: boolean }>): SpatialBehaviorPortMap => map
494
+
495
+ export const SPATIAL_BEHAVIOR_FNS: ReadonlyMap<string, SpatialBehaviorFn> = new Map(Object.entries({
496
+ "rng.seeded.v1": {
497
+ signature: {
498
+ inputs: ports({ seed: { type: "json" }, count: { type: "json" }, stream: { type: "json", optional: true } }),
499
+ outputs: ports({ draws: { type: "json" }, next: { type: "json" } }),
500
+ cost: 50,
501
+ },
502
+ invoke: rngSeeded,
503
+ },
504
+ "behavior.fsm.v1": {
505
+ signature: {
506
+ inputs: ports({ machine: { type: "json" }, state: { type: "json" }, window: { type: "json" } }),
507
+ outputs: ports({ next: { type: "json" }, emitted: { type: "json" } }),
508
+ cost: 200,
509
+ },
510
+ invoke: behaviorFsm,
511
+ },
512
+ "behavior.expression.v1": {
513
+ signature: {
514
+ inputs: ports({ spec: { type: "json" }, state: { type: "json" }, window: { type: "json" } }),
515
+ outputs: ports({ next: { type: "json" }, emitted: { type: "json" } }),
516
+ cost: 200,
517
+ },
518
+ invoke: behaviorExpression,
519
+ },
520
+ "behavior.interact.v1": {
521
+ signature: {
522
+ inputs: ports({ spec: { type: "json" }, state: { type: "json" }, window: { type: "json" } }),
523
+ outputs: ports({ next: { type: "json" }, emitted: { type: "json" } }),
524
+ cost: 200,
525
+ },
526
+ invoke: behaviorInteract,
527
+ },
528
+ "channel.emit.v1": {
529
+ signature: {
530
+ inputs: ports({ channel: { type: "text" }, value: { type: "json", optional: true }, window: { type: "json" } }),
531
+ outputs: ports({ emitted: { type: "json" } }),
532
+ cost: 25,
533
+ },
534
+ invoke: channelEmit,
535
+ },
536
+ "scene.sample.v1": {
537
+ signature: {
538
+ inputs: ports({ spec: { type: "json" }, window: { type: "json" } }),
539
+ outputs: ports({ window: { type: "json" } }),
540
+ cost: 100,
541
+ },
542
+ invoke: sceneSample,
543
+ },
544
+ "behavior.combine.v1": {
545
+ signature: {
546
+ inputs: ports({ layers: { type: "json" } }),
547
+ outputs: ports({ emitted: { type: "json" } }),
548
+ cost: 50,
549
+ },
550
+ invoke: behaviorCombine,
551
+ },
552
+ "emitted.append.v1": {
553
+ signature: {
554
+ inputs: ports({ prior: { type: "json" }, fresh: { type: "json" } }),
555
+ outputs: ports({ emitted: { type: "json" } }),
556
+ cost: 25,
557
+ },
558
+ invoke: emittedAppend,
559
+ },
560
+ "emitted.flatten.v1": {
561
+ signature: {
562
+ inputs: ports({ nested: { type: "json" } }),
563
+ outputs: ports({ emitted: { type: "json" } }),
564
+ cost: 25,
565
+ },
566
+ invoke: emittedFlatten,
567
+ },
568
+ "window.advance.v1": {
569
+ signature: {
570
+ inputs: ports({ window: { type: "json" }, step: { type: "json" } }),
571
+ outputs: ports({ head: { type: "json" }, rest: { type: "json" }, done: { type: "text" } }),
572
+ cost: 10,
573
+ },
574
+ invoke: windowAdvance,
575
+ },
576
+ }))
577
+
578
+ /** Signature-only view of the catalog for `checkSpatialBehavior` admission. */
579
+ export function spatialBehaviorFnSignatures(): Map<string, SpatialBehaviorFnSignature> {
580
+ const signatures = new Map<string, SpatialBehaviorFnSignature>()
581
+ for (const [name, entry] of SPATIAL_BEHAVIOR_FNS) {
582
+ signatures.set(name, { inputs: entry.signature.inputs, outputs: entry.signature.outputs })
583
+ }
584
+ return signatures
585
+ }
@@ -0,0 +1,108 @@
1
+ import { z } from "zod"
2
+
3
+ import { deepFreezeJson } from "../code/json-snapshot.js"
4
+ import {
5
+ parseSpatialBehavior,
6
+ spatialBehaviorSha256,
7
+ type SpatialBehavior,
8
+ } from "./behavior.js"
9
+ import { bakeSpatialBehavior } from "./behavior-bake.js"
10
+ import {
11
+ parseSpatialBehaviorChannelMap,
12
+ SpatialBehaviorBakeSchema,
13
+ } from "./behavior-trace.js"
14
+ import {
15
+ SpatialDigestSchema,
16
+ } from "./contracts.js"
17
+ import { parseSpatialScene, parseSpatialValue, spatialValueSha256 } from "./identity.js"
18
+
19
+ /**
20
+ * Seeded behavior galleries: one admitted behavior document baked under
21
+ * deterministic seed variants produces content-addressed bake candidates.
22
+ * Behaviors that never consume the document seed collapse to fewer
23
+ * candidates — deduplication keeps the first, so the plan honestly reports
24
+ * how much diversity the organism actually admits. `selection` is never
25
+ * populated by the planner; selection stays an explicit downstream act.
26
+ */
27
+
28
+ const BEHAVIOR_GALLERY_SEED_STRIDE = [0, 1, 2, 5, 11, 23] as const
29
+ const SEED_MODULUS = 0x1_0000_0000
30
+
31
+ export const SPATIAL_BEHAVIOR_GALLERY_LIMITS = Object.freeze({
32
+ candidates: 6,
33
+ })
34
+
35
+ export const SpatialBehaviorGalleryCandidateSchema = z.strictObject({
36
+ candidateId: z.string().min(1).max(96).regex(/^cand_[a-f0-9]{16}$/u),
37
+ label: z.string().min(1).max(128),
38
+ parameter: z.string().min(1).max(64),
39
+ documentKind: z.literal("slopcamera.spatial-behavior-bake"),
40
+ documentSha256: SpatialDigestSchema,
41
+ document: z.unknown(),
42
+ })
43
+ export type SpatialBehaviorGalleryCandidate = Readonly<z.infer<typeof SpatialBehaviorGalleryCandidateSchema>>
44
+
45
+ export const SpatialBehaviorGalleryPlanSchema = z.strictObject({
46
+ kind: z.literal("slopcamera.spatial-behavior-gallery"),
47
+ schemaVersion: z.literal(1),
48
+ behaviorSha256: SpatialDigestSchema,
49
+ sceneSha256: SpatialDigestSchema,
50
+ entry: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
51
+ candidates: z.array(SpatialBehaviorGalleryCandidateSchema).max(SPATIAL_BEHAVIOR_GALLERY_LIMITS.candidates),
52
+ })
53
+ export type SpatialBehaviorGalleryPlan = Readonly<z.infer<typeof SpatialBehaviorGalleryPlanSchema>>
54
+
55
+ export function spatialBehaviorGalleryPlanSha256(plan: SpatialBehaviorGalleryPlan): string {
56
+ return spatialValueSha256(plan)
57
+ }
58
+
59
+ const galleryOptionsSchema = z.strictObject({
60
+ behavior: z.unknown(),
61
+ scene: z.unknown(),
62
+ channelMap: z.unknown().optional(),
63
+ })
64
+
65
+ const candidateId = (parameter: string, behaviorSha256: string): string =>
66
+ `cand_${spatialValueSha256({ domain: "slopcamera.behavior-gallery-candidate.v1", parameter, behaviorSha256 }).slice(0, 16)}`
67
+
68
+ export async function planSpatialBehaviorGallery(input: unknown): Promise<SpatialBehaviorGalleryPlan> {
69
+ const options = parseSpatialValue(galleryOptionsSchema, input, "behavior gallery")
70
+ const behavior = parseSpatialBehavior(options.behavior)
71
+ const scene = parseSpatialScene(options.scene)
72
+ const channelMap = options.channelMap === undefined ? undefined : parseSpatialBehaviorChannelMap(options.channelMap)
73
+
74
+ const seen = new Set<string>()
75
+ const candidates: SpatialBehaviorGalleryCandidate[] = []
76
+ for (const stride of BEHAVIOR_GALLERY_SEED_STRIDE) {
77
+ const seed = (behavior.seed + stride) % SEED_MODULUS
78
+ const variant: SpatialBehavior = parseSpatialBehavior({ ...behavior, seed })
79
+ const { bake } = await bakeSpatialBehavior({
80
+ behavior: variant,
81
+ scene,
82
+ ...(channelMap === undefined ? {} : { channelMap }),
83
+ })
84
+ // Dedup on the emitted trace, not the receipt-bound bake: seed-agnostic
85
+ // organisms emit identical records under every variant and must collapse.
86
+ const documentSha256 = spatialValueSha256(bake)
87
+ if (seen.has(bake.receipt.emittedSha256)) continue
88
+ seen.add(bake.receipt.emittedSha256)
89
+ const parameter = `seed:${seed}`
90
+ candidates.push(deepFreezeJson({
91
+ candidateId: candidateId(parameter, bake.behaviorSha256),
92
+ label: `seed ${seed}`,
93
+ parameter,
94
+ documentKind: "slopcamera.spatial-behavior-bake",
95
+ documentSha256,
96
+ document: SpatialBehaviorBakeSchema.parse(bake),
97
+ }))
98
+ }
99
+
100
+ return deepFreezeJson(SpatialBehaviorGalleryPlanSchema.parse({
101
+ kind: "slopcamera.spatial-behavior-gallery",
102
+ schemaVersion: 1,
103
+ behaviorSha256: spatialBehaviorSha256(behavior),
104
+ sceneSha256: spatialValueSha256(scene),
105
+ entry: behavior.entry,
106
+ candidates,
107
+ }))
108
+ }