@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,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,233 @@
|
|
|
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 { SpatialSceneV1Schema } from "./contracts.js"
|
|
31
|
+
import { parseSpatialValue, spatialValueSha256, SpatialSceneError } from "./identity.js"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Deterministic behavior bake: an admitted `slopcamera.spatial-behavior`
|
|
35
|
+
* document runs through the pinned ALGAL runtime under the bake-safe profile
|
|
36
|
+
* — the closed pure fn catalog, a memory store, zero executors, no tools,
|
|
37
|
+
* transports, slots, or effects. The run receipt plus the emitted channel
|
|
38
|
+
* trace and compiled directive proposals form the retained
|
|
39
|
+
* `slopcamera.spatial-behavior-bake` artifact. Organism digests stored into
|
|
40
|
+
* the CAS are recomputed by ALGAL's own `manifestToJson`, so a drift between
|
|
41
|
+
* this mirror and the runtime fails at admission rather than mid-run.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const bakeOptionsSchema = z.strictObject({
|
|
45
|
+
behavior: z.unknown(),
|
|
46
|
+
scene: z.unknown(),
|
|
47
|
+
channelMap: z.unknown().optional(),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
export interface SpatialBehaviorBakeResult {
|
|
51
|
+
readonly bake: SpatialBehaviorBake
|
|
52
|
+
readonly check: SpatialBehaviorCheckReport
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const algalRegistry = (): FnRegistry => {
|
|
56
|
+
const registry: FnRegistry = new Map()
|
|
57
|
+
for (const [name, entry] of SPATIAL_BEHAVIOR_FNS) {
|
|
58
|
+
registry.set(name, {
|
|
59
|
+
signature: {
|
|
60
|
+
inputs: entry.signature.inputs as AlgalPortMap,
|
|
61
|
+
outputs: entry.signature.outputs as AlgalPortMap,
|
|
62
|
+
cost: entry.signature.cost,
|
|
63
|
+
},
|
|
64
|
+
fn: (inputs: Record<string, AlgalJsonValue>) => entry.invoke(inputs) as Record<string, AlgalJsonValue>,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
return registry
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parseEntryManifest = (
|
|
71
|
+
behavior: SpatialBehavior,
|
|
72
|
+
parseOrganismManifest: (input: unknown) => OrganismManifest,
|
|
73
|
+
): OrganismManifest => {
|
|
74
|
+
const organism = behavior.organisms[behavior.entry]
|
|
75
|
+
if (organism === undefined) {
|
|
76
|
+
throw new SpatialSceneError("invalid-data", `unresolved-entry: entry digest ${behavior.entry.slice(0, 24)}… is absent from the organisms closure.`, "behavior-bake")
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return parseOrganismManifest(organism)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new SpatialSceneError("invalid-data", `invalid-manifest: entry organism failed ALGAL manifest parsing: ${error instanceof Error ? error.message : String(error)}`, "behavior-bake")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Maps declared interface-name args to ALGAL's cell-keyed run args. An entry
|
|
87
|
+
* interface input named `seed` binds `behavior.seed` by convention when no
|
|
88
|
+
* arg supplies one — the document-level seed feeds seeded kernels without
|
|
89
|
+
* duplicating it into args, and galleries vary it for candidate diversity.
|
|
90
|
+
*/
|
|
91
|
+
const entryRunArgs = (behavior: SpatialBehavior, manifest: OrganismManifest): Record<string, Record<string, AlgalJsonValue>> => {
|
|
92
|
+
const args: Record<string, Record<string, AlgalJsonValue>> = {}
|
|
93
|
+
for (const [name, value] of Object.entries(behavior.args ?? {})) {
|
|
94
|
+
const endpoint = manifest.interface?.inputs[name]
|
|
95
|
+
if (endpoint === undefined) continue // `undeclared-arg` already an error finding
|
|
96
|
+
;(args[endpoint.cell] ??= {})[endpoint.port] = value as AlgalJsonValue
|
|
97
|
+
}
|
|
98
|
+
const seedEndpoint = manifest.interface?.inputs.seed
|
|
99
|
+
if (seedEndpoint !== undefined && behavior.args?.seed === undefined) {
|
|
100
|
+
(args[seedEndpoint.cell] ??= {})[seedEndpoint.port] = behavior.seed
|
|
101
|
+
}
|
|
102
|
+
return args
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function bakeSpatialBehavior(input: unknown): Promise<SpatialBehaviorBakeResult> {
|
|
106
|
+
const options = parseSpatialValue(bakeOptionsSchema, input, "behavior bake")
|
|
107
|
+
const behavior = parseSpatialBehavior(options.behavior)
|
|
108
|
+
const scene = parseSpatialValue(SpatialSceneV1Schema, options.scene, "scene")
|
|
109
|
+
const sceneSha256 = spatialValueSha256(scene)
|
|
110
|
+
const signatures = spatialBehaviorFnSignatures()
|
|
111
|
+
const check = checkSpatialBehavior({ behavior, scene }, signatures)
|
|
112
|
+
const errors = check.findings.filter((finding) => finding.severity === "error")
|
|
113
|
+
if (errors.length > 0) {
|
|
114
|
+
const codes = [...new Set(errors.map((finding) => finding.code))].join(", ")
|
|
115
|
+
throw new SpatialSceneError("invalid-data", `behavior-check-failed: behavior ${behavior.behaviorId} failed admission with ${errors.length} error finding(s): ${codes}.`, "behavior-bake")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const channelMap: SpatialBehaviorChannelMap | undefined = options.channelMap === undefined
|
|
119
|
+
? undefined
|
|
120
|
+
: parseSpatialBehaviorChannelMap(options.channelMap)
|
|
121
|
+
if (channelMap !== undefined) {
|
|
122
|
+
const declared = new Set(behavior.channels)
|
|
123
|
+
for (const channel of [...Object.keys(channelMap.clips ?? {}), ...(channelMap.trajectories ?? []),
|
|
124
|
+
...(channelMap.attachments ?? []).flatMap((entry) => [entry.attachChannel, entry.releaseChannel])]) {
|
|
125
|
+
if (!declared.has(channel)) {
|
|
126
|
+
throw new SpatialSceneError("invalid-data", `undeclared-channel: channel map binds channel ${channel}, which behavior ${behavior.behaviorId} does not declare.`, "behavior-bake")
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { MemoryStore, parseOrganismManifest, runOrganism } = await import("@hraness/algal")
|
|
132
|
+
|
|
133
|
+
// Populate the CAS closure through ALGAL's own parser and digest: the
|
|
134
|
+
// stored digest must equal the closure key, proving runtime parity.
|
|
135
|
+
const store = new MemoryStore()
|
|
136
|
+
for (const [digest, organism] of Object.entries(behavior.organisms)) {
|
|
137
|
+
let manifest: OrganismManifest
|
|
138
|
+
try {
|
|
139
|
+
manifest = parseOrganismManifest(organism)
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new SpatialSceneError("invalid-data", `invalid-manifest: organism ${organism.key} failed ALGAL manifest parsing: ${error instanceof Error ? error.message : String(error)}`, "behavior-bake")
|
|
142
|
+
}
|
|
143
|
+
const stored = await store.putManifest(manifest)
|
|
144
|
+
if (stored !== digest) {
|
|
145
|
+
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")
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const manifest = parseEntryManifest(behavior, parseOrganismManifest)
|
|
150
|
+
const args = entryRunArgs(behavior, manifest)
|
|
151
|
+
const receipt = await runOrganism({
|
|
152
|
+
manifest,
|
|
153
|
+
args,
|
|
154
|
+
fns: algalRegistry(),
|
|
155
|
+
store,
|
|
156
|
+
executors: [],
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (receipt.manifestDigest !== behavior.entry) {
|
|
160
|
+
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")
|
|
161
|
+
}
|
|
162
|
+
if (receipt.effects.length > 0 || receipt.work.agentCalls > 0) {
|
|
163
|
+
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")
|
|
164
|
+
}
|
|
165
|
+
if (receipt.outcome !== "complete") {
|
|
166
|
+
const failure = receipt.failure
|
|
167
|
+
throw new SpatialSceneError("invalid-data", `behavior-run-failed: ALGAL run ${receipt.outcome}${failure === undefined ? "" : `: ${failure.code} ${failure.message}`}`, "behavior-bake")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Entry interface outputs are the bake surface: every declared output
|
|
171
|
+
// port carries a bounded emitted-record array. Composition internals
|
|
172
|
+
// (carried state, per-round records) stay inside the receipt.
|
|
173
|
+
const declaredChannels = new Set(behavior.channels)
|
|
174
|
+
const emitted: SpatialBehaviorEmitted[] = []
|
|
175
|
+
const outputs = manifest.interface?.outputs ?? {}
|
|
176
|
+
for (const name of Object.keys(outputs).sort()) {
|
|
177
|
+
const endpoint = outputs[name]!
|
|
178
|
+
const record = receipt.cells[endpoint.cell]
|
|
179
|
+
if (record === undefined || record.status !== "committed" || record.outputs === undefined) {
|
|
180
|
+
throw new SpatialSceneError("invalid-data", `emission-missing: interface output ${name} maps to cell ${endpoint.cell}, which did not commit an output.`, "behavior-bake")
|
|
181
|
+
}
|
|
182
|
+
const records = parseSpatialBehaviorEmissions(record.outputs[endpoint.port], `interface output ${name}`)
|
|
183
|
+
for (const emittedRecord of records) {
|
|
184
|
+
if (!declaredChannels.has(emittedRecord.channel)) {
|
|
185
|
+
throw new SpatialSceneError("invalid-data", `undeclared-channel: emission on channel ${emittedRecord.channel}, which behavior ${behavior.behaviorId} does not declare.`, "behavior-bake")
|
|
186
|
+
}
|
|
187
|
+
if (emittedRecord.tUs < behavior.rangeUs.startUs || emittedRecord.tUs > behavior.rangeUs.endUs) {
|
|
188
|
+
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")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
emitted.push(...records)
|
|
192
|
+
}
|
|
193
|
+
if (emitted.length > SPATIAL_BEHAVIOR_TRACE_LIMITS.emitted) {
|
|
194
|
+
throw new SpatialSceneError("invalid-data", `over-bound: run emitted ${emitted.length} records beyond ${SPATIAL_BEHAVIOR_TRACE_LIMITS.emitted}.`, "behavior-bake")
|
|
195
|
+
}
|
|
196
|
+
emitted.sort((a, b) => a.tUs - b.tUs || a.channel.localeCompare(b.channel))
|
|
197
|
+
|
|
198
|
+
const { directives, unresolvedIntents } = compileSpatialBehaviorDirectives({
|
|
199
|
+
emitted,
|
|
200
|
+
...(channelMap === undefined ? {} : { channelMap }),
|
|
201
|
+
rangeUs: behavior.rangeUs,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const behaviorSha256 = spatialBehaviorSha256(behavior)
|
|
205
|
+
const bake = deepFreezeJson(SpatialBehaviorBakeSchema.parse({
|
|
206
|
+
kind: "slopcamera.spatial-behavior-bake",
|
|
207
|
+
schemaVersion: 1,
|
|
208
|
+
behaviorSha256,
|
|
209
|
+
sceneSha256,
|
|
210
|
+
entry: behavior.entry,
|
|
211
|
+
seed: behavior.seed,
|
|
212
|
+
rangeUs: behavior.rangeUs,
|
|
213
|
+
emitted,
|
|
214
|
+
directives,
|
|
215
|
+
unresolvedIntents,
|
|
216
|
+
receipt: {
|
|
217
|
+
kind: "slopcamera.spatial-behavior-bake-receipt",
|
|
218
|
+
schemaVersion: 1,
|
|
219
|
+
behaviorSha256,
|
|
220
|
+
sceneSha256,
|
|
221
|
+
entry: behavior.entry,
|
|
222
|
+
manifestDigest: receipt.manifestDigest,
|
|
223
|
+
runDigest: receipt.digest,
|
|
224
|
+
runtime: { name: receipt.runtime.name, version: receipt.runtime.version },
|
|
225
|
+
fnCatalogSha256: spatialBehaviorFnCatalogSha256(SPATIAL_BEHAVIOR_FNS),
|
|
226
|
+
emittedSha256: spatialValueSha256(emitted),
|
|
227
|
+
...(channelMap === undefined ? {} : { channelMapSha256: spatialValueSha256(channelMap) }),
|
|
228
|
+
outcome: receipt.outcome,
|
|
229
|
+
work: receipt.work,
|
|
230
|
+
},
|
|
231
|
+
}))
|
|
232
|
+
return { bake, check }
|
|
233
|
+
}
|