@hraness/slopcamera 3.2.8 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +29 -9
  2. package/apps/desktop/application/context.ts +6 -0
  3. package/apps/desktop/application/default-registry.ts +26 -0
  4. package/apps/desktop/application/operation.ts +14 -1
  5. package/apps/desktop/application/operations/index.ts +4 -0
  6. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  7. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  8. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  9. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  10. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  11. package/apps/desktop/application/registry.ts +2 -2
  12. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  13. package/apps/desktop/application/spatial-assets.ts +160 -31
  14. package/apps/desktop/application/spatial-render.ts +217 -20
  15. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  16. package/apps/desktop/application/spatial-review.ts +361 -0
  17. package/apps/desktop/application/spatial-spz.ts +48 -3
  18. package/apps/desktop/application/spatial-world-import.ts +26 -5
  19. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  20. package/apps/desktop/cli/args.ts +514 -11
  21. package/apps/desktop/cli/capability-manifest.ts +374 -0
  22. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  23. package/apps/desktop/cli/command-host-resources.ts +29 -1
  24. package/apps/desktop/cli/commands.ts +1188 -4
  25. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  26. package/apps/desktop/cli/help.ts +172 -6
  27. package/apps/desktop/cli/main.ts +3 -3
  28. package/apps/desktop/cli/portable-surface.ts +6 -1
  29. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  30. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  31. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  32. package/apps/desktop/cli/spatial-scene-service.ts +271 -4
  33. package/apps/desktop/cli/workflow-code.ts +1 -1
  34. package/apps/desktop/code/semantic-builder.ts +24 -0
  35. package/apps/desktop/contracts/cinema.ts +736 -0
  36. package/apps/desktop/contracts/index.ts +1 -0
  37. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  38. package/apps/desktop/contracts/spatial-world.ts +26 -3
  39. package/apps/desktop/core/cinema-plan.ts +1320 -0
  40. package/apps/desktop/core/index.ts +1 -0
  41. package/apps/desktop/core/storage.ts +75 -0
  42. package/apps/desktop/dist/cli/main.js +725 -273
  43. package/apps/desktop/html-overlay/spatial.ts +619 -57
  44. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  45. package/apps/desktop/workflows/index.ts +6 -0
  46. package/dist/cli.js +25 -429
  47. package/dist/code/advanced.js +1 -269
  48. package/dist/code/index.js +16 -2239
  49. package/dist/generate.js +1 -23
  50. package/dist/host-resources.js +1 -25
  51. package/dist/index-0bbesn4m.js +35 -0
  52. package/dist/index-296qhpf2.js +3 -0
  53. package/dist/index-3cxf2c58.js +3 -0
  54. package/dist/index-77fjfg9f.js +3 -0
  55. package/dist/index-80a36bc0.js +3 -0
  56. package/dist/index-a3vvs3rf.js +3 -0
  57. package/dist/index-b3geqczn.js +5 -0
  58. package/dist/index-grk47tgs.js +3 -0
  59. package/dist/index-gs48afpt.js +7 -0
  60. package/dist/index-qry58nj2.js +10 -0
  61. package/dist/index-zhg36tf9.js +3 -0
  62. package/dist/index.js +1 -226
  63. package/dist/operations.js +1 -28
  64. package/dist/skill-install-gx7afxsd.js +2 -0
  65. package/dist/vectorize/worker.js +1 -141
  66. package/dist/workflow.js +1 -15
  67. package/docs/README.md +5 -1
  68. package/docs/studio.md +12 -0
  69. package/package.json +7 -5
  70. package/skills/slopcamera/SKILL.md +7 -1
  71. package/skills/slopcamera/references/directed-scenes.md +48 -2
  72. package/skills/slopcamera/references/gateway-media.md +15 -1
  73. package/skills/slopcamera/references/image-galleries.md +166 -0
  74. package/skills/slopcamera/references/install.md +1 -1
  75. package/skills/slopcamera/references/music-video.md +4 -0
  76. package/skills/slopcamera/references/native-studio.md +19 -0
  77. package/skills/slopcamera/references/parametric-design.md +14 -0
  78. package/skills/slopcamera/references/scene-building.md +172 -0
  79. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  80. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  81. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  82. package/src/capability-manifest.ts +361 -0
  83. package/src/cli.ts +222 -4
  84. package/src/code/canonical-json.ts +9 -0
  85. package/src/generate.ts +2 -2
  86. package/src/icon.ts +1111 -0
  87. package/src/image-gallery.ts +1049 -0
  88. package/src/index.ts +11 -0
  89. package/src/mcp/boundary.ts +11 -11
  90. package/src/mcp/index.ts +3 -0
  91. package/src/mcp/server.ts +2 -1
  92. package/src/mcp/tools.ts +1649 -132
  93. package/src/mcp/types.ts +19 -0
  94. package/src/operations.ts +412 -0
  95. package/src/portable-capability-manifest.ts +67 -0
  96. package/src/scene-gallery.ts +184 -0
  97. package/src/spatial-scene/asset-admission.ts +183 -0
  98. package/src/spatial-scene/audit-rendered.ts +692 -0
  99. package/src/spatial-scene/audit.ts +617 -0
  100. package/src/spatial-scene/behavior-audit.ts +252 -0
  101. package/src/spatial-scene/behavior-bake.ts +232 -0
  102. package/src/spatial-scene/behavior-fns.ts +585 -0
  103. package/src/spatial-scene/behavior-gallery.ts +108 -0
  104. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  105. package/src/spatial-scene/behavior-trace.ts +325 -0
  106. package/src/spatial-scene/behavior.ts +617 -0
  107. package/src/spatial-scene/build.ts +632 -0
  108. package/src/spatial-scene/camera-rig.ts +125 -0
  109. package/src/spatial-scene/camera-track.ts +4 -2
  110. package/src/spatial-scene/character.ts +124 -0
  111. package/src/spatial-scene/contracts.ts +75 -7
  112. package/src/spatial-scene/design-templates.ts +351 -0
  113. package/src/spatial-scene/design.ts +327 -0
  114. package/src/spatial-scene/direction-compile.ts +584 -0
  115. package/src/spatial-scene/direction.ts +122 -0
  116. package/src/spatial-scene/effects.ts +149 -0
  117. package/src/spatial-scene/evaluate.ts +109 -29
  118. package/src/spatial-scene/gallery.ts +277 -0
  119. package/src/spatial-scene/generate.ts +239 -0
  120. package/src/spatial-scene/geometry-native.ts +165 -0
  121. package/src/spatial-scene/geometry.ts +1216 -0
  122. package/src/spatial-scene/gltf.ts +600 -77
  123. package/src/spatial-scene/identity.ts +55 -2
  124. package/src/spatial-scene/index.ts +36 -0
  125. package/src/spatial-scene/inspect.ts +35 -6
  126. package/src/spatial-scene/material-lighting.ts +609 -0
  127. package/src/spatial-scene/math.ts +21 -4
  128. package/src/spatial-scene/motion-evidence.ts +88 -0
  129. package/src/spatial-scene/parametric.ts +765 -0
  130. package/src/spatial-scene/particle-preparation.ts +183 -0
  131. package/src/spatial-scene/particle.ts +195 -0
  132. package/src/spatial-scene/patch.ts +109 -15
  133. package/src/spatial-scene/performance.ts +1613 -0
  134. package/src/spatial-scene/probe.ts +137 -0
  135. package/src/spatial-scene/recipe-pack.ts +86 -0
  136. package/src/spatial-scene/render-effects.ts +240 -0
  137. package/src/spatial-scene/review.ts +639 -0
  138. package/src/spatial-scene/simulation-bake.ts +206 -0
  139. package/src/spatial-scene/simulation.ts +187 -0
  140. package/src/spatial-scene/solve.ts +366 -0
  141. package/src/spatial-scene/temporal-audit.ts +354 -0
  142. package/src/version.ts +1 -1
  143. package/dist/index-42zsesc1.js +0 -2257
  144. package/dist/index-7131pg1c.js +0 -1820
  145. package/dist/index-7308egqr.js +0 -48
  146. package/dist/index-8txs6fkn.js +0 -712
  147. package/dist/index-fava6pge.js +0 -192
  148. package/dist/index-h1k0fnjq.js +0 -2363
  149. package/dist/index-r7gdhmsp.js +0 -424
  150. package/dist/index-sh6xbav6.js +0 -1219
  151. package/dist/index-z1w83f81.js +0 -4
  152. package/dist/index-zfnddgay.js +0 -2029
  153. package/dist/skill-install-dh0ntqba.js +0 -10
@@ -0,0 +1,370 @@
1
+ import { constants } from "node:fs";
2
+ import { mkdir, open, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { createSha256HexHasher } from "../../../src/code/sha256";
6
+ import {
7
+ buildSpatialGeneratorRecord,
8
+ deriveSpatialGeneratorSeed,
9
+ generatedSpatialEntityId,
10
+ mergeSpatialGeneratorOutput,
11
+ parseSpatialGeneratorParameters,
12
+ parseSpatialScene,
13
+ SPATIAL_GENERATOR_LIMITS,
14
+ spatialGeneratorParametersSha256,
15
+ spatialSceneSha256,
16
+ SpatialGeneratorIdSchema,
17
+ validateSpatialGeneratorOutput,
18
+ } from "../../../src/spatial-scene/index";
19
+ import { canonicalJsonSha256, sha256Hex } from "../core/canonical-json";
20
+ import type { ApplicationContext } from "../application/context";
21
+ import type { SpatialSceneCommand } from "./args";
22
+ import { CliError, errorMessage } from "./errors";
23
+ import { publishSpatialSource, readSpatialJson } from "./spatial-scene-service";
24
+
25
+ /**
26
+ * Capture an explicit local generator file once: a bounded regular file
27
+ * pinned by descriptor stat before and after the read, never a symlink leaf.
28
+ */
29
+ async function readGeneratorModuleBytes(path: string, label: string): Promise<Uint8Array> {
30
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK)
31
+ .catch((error: NodeJS.ErrnoException) => {
32
+ if (error.code === "ELOOP") throw new CliError("invalid-data", `${label} must be a real file, not a symlink.`);
33
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") throw new CliError("not-found", `${label} does not exist.`);
34
+ throw error;
35
+ });
36
+ try {
37
+ const before = await handle.stat({ bigint: true });
38
+ if (!before.isFile() || before.size === 0n || before.size > BigInt(SPATIAL_GENERATOR_LIMITS.moduleSourceBytes)) {
39
+ throw new CliError("invalid-data", `${label} must be a nonempty bounded regular file.`);
40
+ }
41
+ const bytes = new Uint8Array(Number(before.size));
42
+ let offset = 0;
43
+ while (offset < bytes.byteLength) {
44
+ const read = await handle.read(bytes, offset, bytes.byteLength - offset, offset);
45
+ if (read.bytesRead === 0) throw new CliError("conflict", `${label} ended while it was being read.`);
46
+ offset += read.bytesRead;
47
+ }
48
+ const after = await handle.stat({ bigint: true });
49
+ if (after.size !== before.size || after.ino !== before.ino || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) {
50
+ throw new CliError("conflict", `${label} changed while it was being read.`);
51
+ }
52
+ return bytes;
53
+ } finally { await handle.close(); }
54
+ }
55
+
56
+ /**
57
+ * One verified real file in the generator closure. realPath is the canonical
58
+ * dedupe and containment identity and the verified-read target; relativePath
59
+ * is the POSIX manifest key and the staged location below the module root.
60
+ */
61
+ interface GeneratorClosureFile {
62
+ readonly realPath: string;
63
+ readonly relativePath: string;
64
+ readonly bytes: Uint8Array;
65
+ }
66
+
67
+ interface GeneratorModuleClosure {
68
+ readonly files: readonly GeneratorClosureFile[];
69
+ readonly entryRelativePath: string;
70
+ readonly sourceSha256: string;
71
+ readonly closureSha256: string;
72
+ }
73
+
74
+ /**
75
+ * Closure bounds: at most 64 real files whose verified bytes together stay
76
+ * inside the pre-existing per-module ceiling (1 MiB), so hashing, staging,
77
+ * and module loading remain cheap. A single-file module therefore keeps its
78
+ * exact prior acceptance range.
79
+ */
80
+ const GENERATOR_MODULE_CLOSURE_LIMITS = Object.freeze({
81
+ files: 64,
82
+ totalBytes: SPATIAL_GENERATOR_LIMITS.moduleSourceBytes,
83
+ });
84
+
85
+ /** The transpiler loader for a scanned member, or undefined for data (.json) files that carry no imports. */
86
+ function generatorModuleLoader(relativePath: string): "ts" | "js" | undefined {
87
+ if (relativePath.endsWith(".ts")) return "ts";
88
+ if (relativePath.endsWith(".js")) return "js";
89
+ return undefined;
90
+ }
91
+
92
+ function generatorModuleLabel(entry: GeneratorClosureFile, file: GeneratorClosureFile): string {
93
+ return file === entry ? "Generator module" : `Generator dependency "${file.relativePath}"`;
94
+ }
95
+
96
+ function decodeGeneratorModuleSource(file: GeneratorClosureFile, label: string): string {
97
+ try {
98
+ return new TextDecoder("utf-8", { fatal: true }).decode(file.bytes);
99
+ } catch {
100
+ throw new CliError("invalid-data", `${label} must be UTF-8 TypeScript/JavaScript source.`);
101
+ }
102
+ }
103
+
104
+ function sha256BytesHex(bytes: Uint8Array): string {
105
+ const hasher = createSha256HexHasher();
106
+ hasher.update(bytes);
107
+ return hasher.digestHex();
108
+ }
109
+
110
+ /**
111
+ * Source forms the static closure cannot see are rejected in every file:
112
+ * require()/require.resolve(), dynamic import(), and the Bun import.meta
113
+ * facilities that mint module references at runtime.
114
+ */
115
+ function assertClosureVisibleModuleForms(source: string, label: string): void {
116
+ if (/\brequire(?:\s*\.\s*resolve)?\s*\(/u.test(source)) {
117
+ throw new CliError("invalid-data", `${label} must be ESM; require() escapes the retained closure digest.`);
118
+ }
119
+ if (/(?<![\w$.])import\s*\(/u.test(source)) {
120
+ throw new CliError("invalid-data", `${label} uses dynamic import(), which escapes the retained closure digest.`);
121
+ }
122
+ if (/\bimport\s*\.\s*meta\s*\.\s*(?:glob|resolve|require)\b/u.test(source)) {
123
+ throw new CliError("invalid-data", `${label} uses import.meta glob/resolve/require, which escapes the retained closure digest.`);
124
+ }
125
+ }
126
+
127
+ function isRelativeModuleSpecifier(specifier: string): boolean {
128
+ return specifier === "." || specifier === ".." || specifier.startsWith("./") || specifier.startsWith("../");
129
+ }
130
+
131
+ /** Absolute filesystem specifiers, including drive-letter, UNC, and file: URL forms. */
132
+ function isAbsoluteModuleSpecifier(specifier: string): boolean {
133
+ return specifier.startsWith("/") || specifier.startsWith("\\")
134
+ || /^[a-zA-Z]:/u.test(specifier) || specifier.toLowerCase().startsWith("file:");
135
+ }
136
+
137
+ /**
138
+ * Resolution candidates mirroring Bun's file-then-index probing, restricted
139
+ * to the code/data extensions the closure admits; `.js` also probes the
140
+ * TypeScript `.ts` spelling because the runtime resolves that way.
141
+ */
142
+ function generatorModuleSpecifierCandidates(resolved: string, specifier: string): readonly string[] {
143
+ if (resolved.endsWith(".ts") || resolved.endsWith(".json")) return [resolved];
144
+ if (resolved.endsWith(".js")) return [resolved, `${resolved.slice(0, -3)}.ts`];
145
+ if (extname(resolved) !== "") {
146
+ throw new CliError("invalid-data", `Generator import "${specifier}" must target a .ts, .js, or .json file inside the module directory.`);
147
+ }
148
+ if (specifier.endsWith("/") || specifier === "." || specifier === "..") {
149
+ return [join(resolved, "index.ts"), join(resolved, "index.js"), join(resolved, "index.json")];
150
+ }
151
+ return [`${resolved}.ts`, `${resolved}.js`, `${resolved}.json`,
152
+ join(resolved, "index.ts"), join(resolved, "index.js"), join(resolved, "index.json")];
153
+ }
154
+
155
+ async function realpathIfPresent(candidate: string, specifier: string): Promise<string | undefined> {
156
+ try {
157
+ return await realpath(candidate);
158
+ } catch (error) {
159
+ const code = (error as NodeJS.ErrnoException).code;
160
+ if (code === "ENOENT" || code === "ENOTDIR") return undefined;
161
+ if (code === "ELOOP") {
162
+ throw new CliError("invalid-data", `Generator import "${specifier}" resolves through a symlink; generator modules must be a real file tree.`);
163
+ }
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * BFS over the transitive relative-import graph so the retained closureSha256
170
+ * covers the complete real file set. The module root is the entry file's real
171
+ * directory; every resolved file must stay inside it, must be a real
172
+ * (non-symlink) regular file, and gets the same verified read as the entry.
173
+ * Erased `import type`/`export type` entries carry no runtime dependency and
174
+ * are allowed; bare package specifiers (including node:/bun: builtins) resolve
175
+ * ambient dependencies the digests do not cover — a documented prototype gap
176
+ * unchanged from single-file mode.
177
+ */
178
+ async function resolveGeneratorModuleClosure(modulePath: string): Promise<GeneratorModuleClosure> {
179
+ const entryBytes = await readGeneratorModuleBytes(modulePath, "Generator module");
180
+ let entryRealPath: string;
181
+ try {
182
+ entryRealPath = await realpath(modulePath);
183
+ } catch {
184
+ throw new CliError("not-found", "Generator module does not resolve to a real file.");
185
+ }
186
+ const rootReal = dirname(entryRealPath);
187
+ const entry: GeneratorClosureFile = { realPath: entryRealPath, relativePath: basename(entryRealPath), bytes: entryBytes };
188
+ if (generatorModuleLoader(entry.relativePath) === undefined) {
189
+ throw new CliError("invalid-data", "Generator module must be a .ts or .js file.");
190
+ }
191
+ const files: GeneratorClosureFile[] = [entry];
192
+ const seen = new Set([entry.realPath]);
193
+ const queue = [entry];
194
+ let totalBytes = entry.bytes.byteLength;
195
+
196
+ const resolveImport = async (importer: GeneratorClosureFile, specifier: string): Promise<string> => {
197
+ const resolved = resolve(dirname(importer.realPath), specifier);
198
+ for (const candidate of generatorModuleSpecifierCandidates(resolved, specifier)) {
199
+ const real = await realpathIfPresent(candidate, specifier);
200
+ if (real === undefined) continue;
201
+ const contained = relative(rootReal, real);
202
+ if (contained === "" || contained === ".." || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {
203
+ throw new CliError("invalid-data", `Generator import "${specifier}" escapes the generator module directory.`);
204
+ }
205
+ if (real !== candidate && real.toLowerCase() !== candidate.toLowerCase()) {
206
+ throw new CliError("invalid-data", `Generator import "${specifier}" resolves through a symlink; generator modules must be a real file tree.`);
207
+ }
208
+ return real;
209
+ }
210
+ throw new CliError("not-found", `Generator import "${specifier}" does not resolve to a .ts, .js, or .json file below the module directory.`);
211
+ };
212
+
213
+ while (queue.length > 0) {
214
+ const importer = queue.shift()!;
215
+ const loader = generatorModuleLoader(importer.relativePath);
216
+ if (loader === undefined) continue; // data files carry no further imports
217
+ const label = generatorModuleLabel(entry, importer);
218
+ const source = decodeGeneratorModuleSource(importer, label);
219
+ assertClosureVisibleModuleForms(source, label);
220
+ for (const scanned of new Bun.Transpiler({ loader }).scan(source).imports) {
221
+ if (scanned.kind === "dynamic-import") {
222
+ throw new CliError("invalid-data", `${label} uses dynamic import(), which escapes the retained closure digest.`);
223
+ }
224
+ const specifier = scanned.path;
225
+ if (isAbsoluteModuleSpecifier(specifier)) {
226
+ throw new CliError("invalid-data", `Generator import "${specifier}" is absolute; only relative imports inside the module directory are allowed.`);
227
+ }
228
+ if (!isRelativeModuleSpecifier(specifier)) continue; // ambient bare specifier — documented gap
229
+ const real = await resolveImport(importer, specifier);
230
+ if (seen.has(real)) continue;
231
+ const relativePath = relative(rootReal, real).split(sep).join("/");
232
+ const bytes = await readGeneratorModuleBytes(real, `Generator dependency "${relativePath}"`);
233
+ const file: GeneratorClosureFile = { realPath: real, relativePath, bytes };
234
+ files.push(file);
235
+ seen.add(real);
236
+ queue.push(file);
237
+ totalBytes += bytes.byteLength;
238
+ if (files.length > GENERATOR_MODULE_CLOSURE_LIMITS.files) {
239
+ throw new CliError("invalid-data", `Generator module closure exceeds ${GENERATOR_MODULE_CLOSURE_LIMITS.files} files.`);
240
+ }
241
+ if (totalBytes > GENERATOR_MODULE_CLOSURE_LIMITS.totalBytes) {
242
+ throw new CliError("invalid-data", `Generator module closure exceeds ${GENERATOR_MODULE_CLOSURE_LIMITS.totalBytes} total bytes.`);
243
+ }
244
+ }
245
+ }
246
+
247
+ const manifest = files
248
+ .map(file => ({ relativePath: file.relativePath, sha256: sha256BytesHex(file.bytes) }))
249
+ .sort((left, right) => left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0);
250
+ const sourceSha256 = sha256BytesHex(entry.bytes);
251
+ // Single-file modules keep closureSha256 == sourceSha256 exactly, so the
252
+ // retained record is byte-identical to the pre-closure behavior.
253
+ const closureSha256 = files.length === 1 ? sourceSha256 : canonicalJsonSha256(manifest);
254
+ return { files, entryRelativePath: entry.relativePath, sourceSha256, closureSha256 };
255
+ }
256
+
257
+ async function directoryExists(path: string): Promise<boolean> {
258
+ return stat(path).then(result => result.isDirectory()).catch(() => false);
259
+ }
260
+
261
+ /**
262
+ * The closure executes from a private content-addressed tree mirroring the
263
+ * module's relative structure, so the code that runs is exactly the bytes the
264
+ * retained digests describe. Plain-path imports would return stale modules
265
+ * from the in-process registry after any member file changed.
266
+ */
267
+ async function stageGeneratorModuleTree(privateRoot: string, closure: GeneratorModuleClosure): Promise<string> {
268
+ const directory = join(privateRoot, "scene-generator-modules");
269
+ await mkdir(directory, { recursive: true, mode: 0o700 });
270
+ const staged = join(directory, closure.closureSha256);
271
+ const stagedEntry = join(staged, ...closure.entryRelativePath.split("/"));
272
+ if (await directoryExists(staged)) return stagedEntry;
273
+ const temporary = join(directory, `.${closure.closureSha256}.${process.pid}.tmp`);
274
+ await rm(temporary, { recursive: true, force: true });
275
+ try {
276
+ for (const file of closure.files) {
277
+ const target = join(temporary, ...file.relativePath.split("/"));
278
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
279
+ await writeFile(target, file.bytes, { mode: 0o600 });
280
+ }
281
+ await rename(temporary, staged);
282
+ } catch (error) {
283
+ await rm(temporary, { recursive: true, force: true }).catch(() => undefined);
284
+ if (await directoryExists(staged)) return stagedEntry; // a concurrent invocation staged identical content
285
+ throw error;
286
+ }
287
+ return stagedEntry;
288
+ }
289
+
290
+ export type SpatialGenerateCommand = Extract<SpatialSceneCommand, { readonly action: "generate" }>;
291
+
292
+ /**
293
+ * Executes a trusted current-user generator module — the same trust class as an
294
+ * explicitly imported workflow module, with no sandbox claim — then stamps
295
+ * generated identity, validates bounded output, records attempt provenance and
296
+ * merges through replace-generator-output semantics.
297
+ */
298
+ export async function executeSpatialGenerateCommand(
299
+ application: ApplicationContext,
300
+ command: SpatialGenerateCommand,
301
+ ): Promise<unknown> {
302
+ const parsedGeneratorId = SpatialGeneratorIdSchema.safeParse(command.generatorId);
303
+ if (!parsedGeneratorId.success) {
304
+ throw new CliError("invalid-data", "--generator-id must match generator_<alphanumeric> (1–128 characters).");
305
+ }
306
+ const generatorId = parsedGeneratorId.data;
307
+ const modulePath = resolve(application.paths.repositoryRoot, command.module);
308
+ const closure = await resolveGeneratorModuleClosure(modulePath);
309
+ const { sourceSha256, closureSha256 } = closure;
310
+ const stagedPath = await stageGeneratorModuleTree(application.paths.privateRoot, closure);
311
+
312
+ const parameters = command.parameters === undefined
313
+ ? {}
314
+ : parseSpatialGeneratorParameters(
315
+ await readSpatialJson(resolve(application.paths.repositoryRoot, command.parameters), SPATIAL_GENERATOR_LIMITS.parametersBytes));
316
+ const parametersSha256 = spatialGeneratorParametersSha256(parameters);
317
+ const seed = command.seed ?? deriveSpatialGeneratorSeed(sourceSha256);
318
+
319
+ let loaded: unknown;
320
+ try {
321
+ loaded = await import(pathToFileURL(stagedPath).href);
322
+ } catch (error) {
323
+ throw new CliError("invalid-data", `Generator module failed to load: ${errorMessage(error)}`);
324
+ }
325
+ const generate = (loaded as Readonly<Record<string, unknown>>).generate;
326
+ if (typeof generate !== "function") {
327
+ throw new CliError("invalid-data", 'Generator module must export a "generate" function.');
328
+ }
329
+ const context = Object.freeze({
330
+ seed,
331
+ parameters,
332
+ lib: Object.freeze({ entityId: (key: string) => generatedSpatialEntityId(generatorId, key) }),
333
+ });
334
+ let raw: unknown;
335
+ try {
336
+ raw = await (generate as (context: unknown) => unknown)(context);
337
+ } catch (error) {
338
+ throw new CliError("invalid-data", `Generator module failed: ${errorMessage(error)}`);
339
+ }
340
+
341
+ const output = validateSpatialGeneratorOutput(generatorId, raw);
342
+ const runtimeSha256 = sha256Hex(Bun.version);
343
+ const generator = buildSpatialGeneratorRecord({
344
+ generatorId,
345
+ sourceSha256,
346
+ closureSha256,
347
+ parametersSha256,
348
+ seed,
349
+ runtimeSha256,
350
+ entities: output.entities,
351
+ editableKeys: output.editableKeys,
352
+ });
353
+ const into = command.into === undefined
354
+ ? undefined
355
+ : parseSpatialScene(await readSpatialJson(resolve(application.paths.repositoryRoot, command.into)));
356
+ const scene = mergeSpatialGeneratorOutput(into, generator, output.entities);
357
+ const outputPath = resolve(application.paths.repositoryRoot, command.output);
358
+ await publishSpatialSource(outputPath, scene);
359
+ return {
360
+ path: outputPath,
361
+ sceneSha256: spatialSceneSha256(scene),
362
+ generatorId,
363
+ seed,
364
+ entities: output.entities.length,
365
+ outputSha256: generator.outputSha256,
366
+ sourceSha256,
367
+ attemptId: generator.execution.kind === "attempt" ? generator.execution.attemptId : undefined,
368
+ merged: into !== undefined,
369
+ };
370
+ }