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