@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.
- 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 +271 -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-grk47tgs.js +3 -0
- package/dist/index-gs48afpt.js +7 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index-zhg36tf9.js +3 -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/docs/studio.md +12 -0
- package/package.json +7 -5
- package/skills/slopcamera/SKILL.md +7 -1
- package/skills/slopcamera/references/directed-scenes.md +48 -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/music-video.md +4 -0
- package/skills/slopcamera/references/native-studio.md +19 -0
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/scene-building.md +172 -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 +1649 -132
- 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 +232 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +108 -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 +617 -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,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
|
+
}
|