@hraness/slopcamera 3.2.7 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +1 -7
- package/PRIVACY.md +16 -7
- package/README.md +65 -20
- package/apps/desktop/README.md +14 -130
- package/apps/desktop/application/context.ts +6 -9
- package/apps/desktop/application/default-registry.ts +26 -10
- package/apps/desktop/application/operation.ts +14 -5
- package/apps/desktop/application/operations/index.ts +4 -1
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +528 -97
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +31 -2
- package/apps/desktop/cli/commands.ts +1215 -70
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +183 -23
- package/apps/desktop/cli/html-scene.ts +139 -28
- package/apps/desktop/cli/io.ts +2 -1
- package/apps/desktop/cli/main.ts +26 -50
- package/apps/desktop/cli/menubar.ts +244 -0
- package/apps/desktop/cli/music-analysis-service.ts +61 -0
- package/apps/desktop/cli/portable-surface.ts +18 -5
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +268 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/application-node-planner.ts +0 -47
- package/apps/desktop/code/host-source-layout.ts +46 -0
- package/apps/desktop/code/public.ts +0 -1
- package/apps/desktop/code/runtime-identity.ts +50 -8
- package/apps/desktop/code/semantic-builder.ts +24 -43
- package/apps/desktop/code/source-bundle.ts +23 -6
- package/apps/desktop/code/source-typecheck.ts +131 -43
- package/apps/desktop/code/worker-client.ts +14 -2
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/music-analysis.ts +124 -1
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +737 -291
- package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
- package/apps/desktop/html-overlay/index.ts +9 -0
- package/apps/desktop/html-overlay/runtime.ts +2 -0
- package/apps/desktop/html-overlay/scene.ts +29 -0
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +32 -434
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-cgpkyjqa.js +3 -0
- package/dist/index-f0ptv8dm.js +7 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index.js +1 -240
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +1 -1
- package/package.json +11 -24
- package/skills/slopcamera/SKILL.md +14 -1
- package/skills/slopcamera/references/diagrams.md +5 -14
- package/skills/slopcamera/references/directed-scenes.md +22 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +2 -2
- package/skills/slopcamera/references/music-video.md +23 -3
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/scene-building.md +32 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/support.md +25 -0
- package/skills/slopcamera/references/web-media-excerpts.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +250 -102
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -23
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1642 -130
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +233 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +109 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +618 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/support-completion.ts +16 -0
- package/src/support.ts +42 -0
- package/src/version.ts +1 -1
- package/apps/desktop/application/operations/recording/index.ts +0 -5
- package/apps/desktop/application/operations/recording/pause.ts +0 -34
- package/apps/desktop/application/operations/recording/resume.ts +0 -34
- package/apps/desktop/application/operations/recording/shared.ts +0 -199
- package/apps/desktop/application/operations/recording/start.ts +0 -169
- package/apps/desktop/application/operations/recording/stop.ts +0 -34
- package/apps/desktop/capture/protocol.ts +0 -689
- package/apps/desktop/cli/capture-bundle.ts +0 -1117
- package/apps/desktop/cli/recording-controller.ts +0 -1567
- package/apps/desktop/cli/recording-daemon.ts +0 -931
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-mcy8z0br.js +0 -192
- package/dist/index-p63wavx0.js +0 -2029
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-rvghzt7b.js +0 -2013
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-z7239b4h.js +0 -2363
- package/dist/skill-install-dh0ntqba.js +0 -10
- package/src/desktop.ts +0 -253
|
@@ -0,0 +1,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,109 @@
|
|
|
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
|
+
SpatialSceneV1Schema,
|
|
17
|
+
} from "./contracts.js"
|
|
18
|
+
import { parseSpatialValue, spatialValueSha256 } from "./identity.js"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Seeded behavior galleries: one admitted behavior document baked under
|
|
22
|
+
* deterministic seed variants produces content-addressed bake candidates.
|
|
23
|
+
* Behaviors that never consume the document seed collapse to fewer
|
|
24
|
+
* candidates — deduplication keeps the first, so the plan honestly reports
|
|
25
|
+
* how much diversity the organism actually admits. `selection` is never
|
|
26
|
+
* populated by the planner; selection stays an explicit downstream act.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const BEHAVIOR_GALLERY_SEED_STRIDE = [0, 1, 2, 5, 11, 23] as const
|
|
30
|
+
const SEED_MODULUS = 0x1_0000_0000
|
|
31
|
+
|
|
32
|
+
export const SPATIAL_BEHAVIOR_GALLERY_LIMITS = Object.freeze({
|
|
33
|
+
candidates: 6,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const SpatialBehaviorGalleryCandidateSchema = z.strictObject({
|
|
37
|
+
candidateId: z.string().min(1).max(96).regex(/^cand_[a-f0-9]{16}$/u),
|
|
38
|
+
label: z.string().min(1).max(128),
|
|
39
|
+
parameter: z.string().min(1).max(64),
|
|
40
|
+
documentKind: z.literal("slopcamera.spatial-behavior-bake"),
|
|
41
|
+
documentSha256: SpatialDigestSchema,
|
|
42
|
+
document: z.unknown(),
|
|
43
|
+
})
|
|
44
|
+
export type SpatialBehaviorGalleryCandidate = Readonly<z.infer<typeof SpatialBehaviorGalleryCandidateSchema>>
|
|
45
|
+
|
|
46
|
+
export const SpatialBehaviorGalleryPlanSchema = z.strictObject({
|
|
47
|
+
kind: z.literal("slopcamera.spatial-behavior-gallery"),
|
|
48
|
+
schemaVersion: z.literal(1),
|
|
49
|
+
behaviorSha256: SpatialDigestSchema,
|
|
50
|
+
sceneSha256: SpatialDigestSchema,
|
|
51
|
+
entry: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
52
|
+
candidates: z.array(SpatialBehaviorGalleryCandidateSchema).max(SPATIAL_BEHAVIOR_GALLERY_LIMITS.candidates),
|
|
53
|
+
})
|
|
54
|
+
export type SpatialBehaviorGalleryPlan = Readonly<z.infer<typeof SpatialBehaviorGalleryPlanSchema>>
|
|
55
|
+
|
|
56
|
+
export function spatialBehaviorGalleryPlanSha256(plan: SpatialBehaviorGalleryPlan): string {
|
|
57
|
+
return spatialValueSha256(plan)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const galleryOptionsSchema = z.strictObject({
|
|
61
|
+
behavior: z.unknown(),
|
|
62
|
+
scene: z.unknown(),
|
|
63
|
+
channelMap: z.unknown().optional(),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const candidateId = (parameter: string, behaviorSha256: string): string =>
|
|
67
|
+
`cand_${spatialValueSha256({ domain: "slopcamera.behavior-gallery-candidate.v1", parameter, behaviorSha256 }).slice(0, 16)}`
|
|
68
|
+
|
|
69
|
+
export async function planSpatialBehaviorGallery(input: unknown): Promise<SpatialBehaviorGalleryPlan> {
|
|
70
|
+
const options = parseSpatialValue(galleryOptionsSchema, input, "behavior gallery")
|
|
71
|
+
const behavior = parseSpatialBehavior(options.behavior)
|
|
72
|
+
const scene = parseSpatialValue(SpatialSceneV1Schema, options.scene, "scene")
|
|
73
|
+
const channelMap = options.channelMap === undefined ? undefined : parseSpatialBehaviorChannelMap(options.channelMap)
|
|
74
|
+
|
|
75
|
+
const seen = new Set<string>()
|
|
76
|
+
const candidates: SpatialBehaviorGalleryCandidate[] = []
|
|
77
|
+
for (const stride of BEHAVIOR_GALLERY_SEED_STRIDE) {
|
|
78
|
+
const seed = (behavior.seed + stride) % SEED_MODULUS
|
|
79
|
+
const variant: SpatialBehavior = parseSpatialBehavior({ ...behavior, seed })
|
|
80
|
+
const { bake } = await bakeSpatialBehavior({
|
|
81
|
+
behavior: variant,
|
|
82
|
+
scene,
|
|
83
|
+
...(channelMap === undefined ? {} : { channelMap }),
|
|
84
|
+
})
|
|
85
|
+
// Dedup on the emitted trace, not the receipt-bound bake: seed-agnostic
|
|
86
|
+
// organisms emit identical records under every variant and must collapse.
|
|
87
|
+
const documentSha256 = spatialValueSha256(bake)
|
|
88
|
+
if (seen.has(bake.receipt.emittedSha256)) continue
|
|
89
|
+
seen.add(bake.receipt.emittedSha256)
|
|
90
|
+
const parameter = `seed:${seed}`
|
|
91
|
+
candidates.push(deepFreezeJson({
|
|
92
|
+
candidateId: candidateId(parameter, bake.behaviorSha256),
|
|
93
|
+
label: `seed ${seed}`,
|
|
94
|
+
parameter,
|
|
95
|
+
documentKind: "slopcamera.spatial-behavior-bake",
|
|
96
|
+
documentSha256,
|
|
97
|
+
document: SpatialBehaviorBakeSchema.parse(bake),
|
|
98
|
+
}))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return deepFreezeJson(SpatialBehaviorGalleryPlanSchema.parse({
|
|
102
|
+
kind: "slopcamera.spatial-behavior-gallery",
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
behaviorSha256: spatialBehaviorSha256(behavior),
|
|
105
|
+
sceneSha256: spatialValueSha256(scene),
|
|
106
|
+
entry: behavior.entry,
|
|
107
|
+
candidates,
|
|
108
|
+
}))
|
|
109
|
+
}
|