@forgeax/engine-import 0.1.20 → 0.1.23
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 +91 -3
- package/dist/__tests__/ies-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/ies-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ies-producer.unit.test.d.ts +2 -0
- package/dist/__tests__/ies-producer.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-lod-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-lod-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-lod-reimport.integration.test.d.ts +2 -0
- package/dist/__tests__/mesh-lod-reimport.integration.test.d.ts.map +1 -0
- package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts +2 -0
- package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-file-snapshot.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-file-snapshot.unit.test.d.ts.map +1 -0
- package/dist/__tests__/source-package-publication-lod.integration.test.d.ts +2 -0
- package/dist/__tests__/source-package-publication-lod.integration.test.d.ts.map +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.mjs +230 -3
- package/dist/browser.mjs.map +1 -1
- package/dist/build-production.d.ts +1 -8
- package/dist/build-production.d.ts.map +1 -1
- package/dist/ies/ies-importer.d.ts +4 -0
- package/dist/ies/ies-importer.d.ts.map +1 -0
- package/dist/ies/parse-lm63.d.ts +13 -0
- package/dist/ies/parse-lm63.d.ts.map +1 -0
- package/dist/ies/resample-type-c.d.ts +4 -0
- package/dist/ies/resample-type-c.d.ts.map +1 -0
- package/dist/import-runner.d.ts.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2550 -1029
- package/dist/index.mjs.map +1 -1
- package/dist/mesh-bin.d.ts.map +1 -1
- package/dist/mesh-bin.mjs +29 -1
- package/dist/mesh-bin.mjs.map +1 -1
- package/dist/mesh-lod.d.ts +60 -0
- package/dist/mesh-lod.d.ts.map +1 -0
- package/dist/parameterized-scriptable-pack.d.ts +68 -0
- package/dist/parameterized-scriptable-pack.d.ts.map +1 -0
- package/dist/scriptable-pack-file-snapshot.d.ts +71 -0
- package/dist/scriptable-pack-file-snapshot.d.ts.map +1 -0
- package/dist/scriptable-pack-host.d.ts +70 -39
- package/dist/scriptable-pack-host.d.ts.map +1 -1
- package/dist/scriptable-pack-output-producers.d.ts +1 -0
- package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
- package/dist/scriptable-pack-staged-snapshot.d.ts +2 -2
- package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -1
- package/dist/scriptable-pack.d.ts +13 -26
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/source-package-errors.d.ts.map +1 -1
- package/dist/source-package-publication.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/__tests__/ies-contract.unit.test.ts +31 -0
- package/src/__tests__/ies-producer.unit.test.ts +104 -0
- package/src/__tests__/mesh-bin.test.ts +69 -0
- package/src/__tests__/mesh-lod-contract.unit.test.ts +92 -0
- package/src/__tests__/mesh-lod-reimport.integration.test.ts +15 -0
- package/src/__tests__/parameterized-scriptable-pack.unit.test.ts +270 -0
- package/src/__tests__/scriptable-pack-file-snapshot.unit.test.ts +177 -0
- package/src/__tests__/scriptable-pack-host.unit.test.ts +196 -4
- package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +7 -1
- package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +43 -12
- package/src/__tests__/source-package-publication-lod.integration.test.ts +29 -0
- package/src/__tests__/source-package-publication.integration.test.ts +26 -1
- package/src/browser.ts +5 -0
- package/src/build-production.ts +498 -141
- package/src/ies/ies-importer.ts +126 -0
- package/src/ies/parse-lm63.ts +98 -0
- package/src/ies/resample-type-c.ts +108 -0
- package/src/import-runner.ts +58 -0
- package/src/index.ts +49 -19
- package/src/mesh-bin.ts +38 -0
- package/src/mesh-lod.ts +265 -0
- package/src/parameterized-scriptable-pack.ts +725 -0
- package/src/scriptable-pack-file-snapshot.ts +346 -0
- package/src/scriptable-pack-host.ts +790 -343
- package/src/scriptable-pack-output-producers.ts +58 -1
- package/src/scriptable-pack-staged-snapshot.ts +12 -11
- package/src/scriptable-pack.ts +16 -485
- package/src/source-package-errors.ts +6 -0
- package/src/source-package-publication.ts +32 -2
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/scriptable-pack-product.contract.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack.integration.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack.integration.test.d.ts.map +0 -1
- package/dist/scriptable-source-package.d.ts +0 -14
- package/dist/scriptable-source-package.d.ts.map +0 -1
- package/src/__tests__/scriptable-pack-product.contract.test.ts +0 -46
- package/src/__tests__/scriptable-pack.integration.test.ts +0 -435
- package/src/scriptable-source-package.ts +0 -88
package/src/scriptable-pack.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
projectScriptablePackSceneComponents,
|
|
7
|
-
type ScriptablePackDefinition,
|
|
8
|
-
type ScriptablePackError,
|
|
9
|
-
type ScriptablePackReadError,
|
|
10
|
-
type ScriptablePackSourceClosureEntry,
|
|
1
|
+
import type { ScriptablePackAssetKind } from '@forgeax/engine-pack';
|
|
2
|
+
import type {
|
|
3
|
+
AssetReader,
|
|
4
|
+
PackAuthoringError,
|
|
5
|
+
ScriptablePackReadError,
|
|
11
6
|
} from '@forgeax/engine-pack/source';
|
|
12
7
|
import type {
|
|
13
8
|
AnimationGraph,
|
|
@@ -15,56 +10,17 @@ import type {
|
|
|
15
10
|
AssetGuid as AssetGuidType,
|
|
16
11
|
AssetPublicationEnvelope,
|
|
17
12
|
FontAsset,
|
|
13
|
+
ImportError,
|
|
18
14
|
ImportedAsset,
|
|
19
15
|
MaterialAsset,
|
|
20
16
|
MeshAsset,
|
|
21
17
|
Result,
|
|
22
18
|
TilesetAsset,
|
|
23
19
|
} from '@forgeax/engine-types';
|
|
20
|
+
import type { ImportAssetProduct, TerminalImportProduct } from './import-product.js';
|
|
24
21
|
|
|
25
22
|
export type { ScriptablePackSourceClosureEntry } from '@forgeax/engine-pack/source';
|
|
26
23
|
|
|
27
|
-
import { AssetError, err, ImportError, ok } from '@forgeax/engine-types';
|
|
28
|
-
import {
|
|
29
|
-
createImportProduct,
|
|
30
|
-
type ImportAssetProduct,
|
|
31
|
-
type TerminalImportProduct,
|
|
32
|
-
} from './import-product.js';
|
|
33
|
-
|
|
34
|
-
function commonPathPrefix(paths: readonly string[]): string {
|
|
35
|
-
const first = paths[0];
|
|
36
|
-
if (first === undefined) return '';
|
|
37
|
-
const firstNormalized = first.replaceAll('\\', '/');
|
|
38
|
-
const firstDirectory = firstNormalized.slice(0, firstNormalized.lastIndexOf('/'));
|
|
39
|
-
const parts = firstDirectory.split('/');
|
|
40
|
-
let length = parts.length;
|
|
41
|
-
for (const path of paths.slice(1)) {
|
|
42
|
-
const normalized = path.replaceAll('\\', '/');
|
|
43
|
-
const candidate = normalized.slice(0, normalized.lastIndexOf('/')).split('/');
|
|
44
|
-
length = Math.min(length, candidate.length);
|
|
45
|
-
for (let index = 0; index < length; index += 1) {
|
|
46
|
-
if (parts[index] !== candidate[index]) {
|
|
47
|
-
length = index;
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return parts.slice(0, length).join('/');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Remove host-specific absolute roots from the source closure fingerprint. */
|
|
56
|
-
function stableSourceClosure(
|
|
57
|
-
closure: readonly ScriptablePackSourceClosureEntry[],
|
|
58
|
-
): readonly ScriptablePackSourceClosureEntry[] {
|
|
59
|
-
const root = commonPathPrefix(closure.map((entry) => entry.path));
|
|
60
|
-
return closure
|
|
61
|
-
.map((entry) => ({
|
|
62
|
-
path: entry.path.replaceAll('\\', '/').slice(root.length).replace(/^\/+/, ''),
|
|
63
|
-
digest: entry.digest,
|
|
64
|
-
}))
|
|
65
|
-
.sort((left, right) => left.path.localeCompare(right.path));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
24
|
export interface ScriptablePackAssetSnapshot {
|
|
69
25
|
readonly asset: Asset;
|
|
70
26
|
readonly generation: number;
|
|
@@ -73,8 +29,7 @@ export interface ScriptablePackAssetSnapshot {
|
|
|
73
29
|
|
|
74
30
|
export interface ScriptablePackStagedOutput {
|
|
75
31
|
readonly guid: AssetGuidType;
|
|
76
|
-
|
|
77
|
-
readonly sourceKey?: string;
|
|
32
|
+
readonly sourceKey: string;
|
|
78
33
|
readonly asset: Asset;
|
|
79
34
|
readonly digest?: string;
|
|
80
35
|
}
|
|
@@ -114,7 +69,7 @@ type AnimationGraphPackPayload = Omit<AnimationGraph, 'nodes'> & {
|
|
|
114
69
|
)[];
|
|
115
70
|
};
|
|
116
71
|
|
|
117
|
-
/** Serialized
|
|
72
|
+
/** Serialized producer payloads; runtime Assets never carry ref indices. */
|
|
118
73
|
export type AssetOutputPayloadByKind = {
|
|
119
74
|
readonly mesh: MeshAsset;
|
|
120
75
|
readonly material: MaterialPackPayload;
|
|
@@ -132,10 +87,10 @@ export type AssetOutputPayloadByKind = {
|
|
|
132
87
|
readonly 'animation-graph': AnimationGraphPackPayload;
|
|
133
88
|
readonly audio: Extract<Asset, { readonly kind: 'audio' }>;
|
|
134
89
|
readonly 'particle-effect': Extract<Asset, { readonly kind: 'particle-effect' }>;
|
|
90
|
+
readonly 'ies-profile': Extract<Asset, { readonly kind: 'ies-profile' }>;
|
|
135
91
|
};
|
|
136
92
|
|
|
137
93
|
export type AssetOutputPayload = AssetOutputPayloadByKind[ScriptablePackAssetKind];
|
|
138
|
-
|
|
139
94
|
export type AssetOutputProduct = ImportAssetProduct<AssetOutputPayload>;
|
|
140
95
|
|
|
141
96
|
export interface AssetOutputProducer {
|
|
@@ -151,10 +106,10 @@ export class AssetOutputProducerRegistry {
|
|
|
151
106
|
|
|
152
107
|
register(producer: AssetOutputProducer): void {
|
|
153
108
|
if (producer.kind.trim().length === 0 || producer.version.trim().length === 0) {
|
|
154
|
-
throw new TypeError('
|
|
109
|
+
throw new TypeError('Pack output producer kind and version must be non-empty');
|
|
155
110
|
}
|
|
156
111
|
if (typeof producer.produce !== 'function') {
|
|
157
|
-
throw new TypeError(`
|
|
112
|
+
throw new TypeError(`Pack output producer ${producer.kind} must expose produce`);
|
|
158
113
|
}
|
|
159
114
|
this.producers.set(producer.kind, producer);
|
|
160
115
|
}
|
|
@@ -183,15 +138,12 @@ export interface ScriptablePackExternalEvidence {
|
|
|
183
138
|
|
|
184
139
|
export interface ScriptablePackBuildProduct {
|
|
185
140
|
readonly product: TerminalImportProduct<unknown>;
|
|
186
|
-
/** Raw private outputs for the current staged build generation, never a runtime publication path. */
|
|
187
141
|
readonly stagedOutputs: readonly ScriptablePackStagedOutput[];
|
|
188
142
|
readonly externalEvidence: readonly ScriptablePackExternalEvidence[];
|
|
189
143
|
readonly inputFingerprint: string;
|
|
190
|
-
/** Engine publication fields projected after ordinary producers run. */
|
|
191
144
|
readonly publication?: AssetPublicationEnvelope;
|
|
192
145
|
}
|
|
193
146
|
|
|
194
|
-
/** Domain-owned structured failure transported without flattening its recovery fields. */
|
|
195
147
|
export interface ScriptablePackDomainError {
|
|
196
148
|
readonly code: string;
|
|
197
149
|
readonly expected: string;
|
|
@@ -199,428 +151,7 @@ export interface ScriptablePackDomainError {
|
|
|
199
151
|
readonly detail?: unknown;
|
|
200
152
|
}
|
|
201
153
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
>;
|
|
206
|
-
|
|
207
|
-
interface ObservedRead {
|
|
208
|
-
readonly guid: string;
|
|
209
|
-
readonly asset: Asset;
|
|
210
|
-
readonly generation: number;
|
|
211
|
-
readonly digest: string;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function privateClone<T>(value: T): T {
|
|
215
|
-
return structuredClone(value);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function record(value: unknown): value is Record<string, unknown> {
|
|
219
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function isAsset(value: unknown): value is Asset {
|
|
223
|
-
return record(value) && typeof value.kind === 'string' && isScriptablePackAssetKind(value.kind);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function isStructuredDomainError(value: unknown): value is ScriptablePackDomainError {
|
|
227
|
-
return (
|
|
228
|
-
value !== null &&
|
|
229
|
-
typeof value === 'object' &&
|
|
230
|
-
typeof (value as { readonly code?: unknown }).code === 'string' &&
|
|
231
|
-
typeof (value as { readonly expected?: unknown }).expected === 'string' &&
|
|
232
|
-
typeof (value as { readonly hint?: unknown }).hint === 'string'
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function remapBuildFailureSource(
|
|
237
|
-
value: unknown,
|
|
238
|
-
sourcePath: string,
|
|
239
|
-
): ScriptablePackError | undefined {
|
|
240
|
-
if (value === null || typeof value !== 'object') return undefined;
|
|
241
|
-
const error = value as { readonly code?: unknown; readonly detail?: unknown };
|
|
242
|
-
if (error.code !== 'pack-source-load-failed') return undefined;
|
|
243
|
-
if (error.detail === null || typeof error.detail !== 'object') return undefined;
|
|
244
|
-
const detail = error.detail as { readonly phase?: unknown } & Record<string, unknown>;
|
|
245
|
-
if (detail.phase !== 'build') return undefined;
|
|
246
|
-
return {
|
|
247
|
-
...(value as ScriptablePackError),
|
|
248
|
-
detail: { ...detail, sourcePath },
|
|
249
|
-
} as ScriptablePackError;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function observedAssetReader(source?: ScriptablePackAssetSnapshotSource): {
|
|
253
|
-
readonly reader: AssetReader;
|
|
254
|
-
readonly reads: ReadonlyMap<string, ObservedRead>;
|
|
255
|
-
} {
|
|
256
|
-
const reads = new Map<string, ObservedRead>();
|
|
257
|
-
const reader: AssetReader = {
|
|
258
|
-
async readByGuid<TAsset extends Asset = Asset>(guid: AssetGuidType) {
|
|
259
|
-
const key = AssetGuid.format(guid);
|
|
260
|
-
const cached = reads.get(key);
|
|
261
|
-
if (cached !== undefined) return ok(privateClone(cached.asset) as TAsset);
|
|
262
|
-
if (source === undefined) {
|
|
263
|
-
return err(
|
|
264
|
-
new AssetError({
|
|
265
|
-
code: 'asset-not-found',
|
|
266
|
-
expected: `a host Asset snapshot source for ScriptablePack content read ${key}`,
|
|
267
|
-
hint: 'configure the standard ScriptablePack asset source or remove the content read',
|
|
268
|
-
}),
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
const result = await source.readByGuid(guid);
|
|
272
|
-
if (!result.ok) return result;
|
|
273
|
-
const fixed = privateClone(result.value.asset);
|
|
274
|
-
reads.set(key, {
|
|
275
|
-
guid: key,
|
|
276
|
-
asset: fixed,
|
|
277
|
-
generation: result.value.generation,
|
|
278
|
-
digest: result.value.digest,
|
|
279
|
-
});
|
|
280
|
-
return ok(privateClone(fixed) as TAsset);
|
|
281
|
-
},
|
|
282
|
-
};
|
|
283
|
-
return { reader, reads };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function stable(value: unknown): string {
|
|
287
|
-
if (value instanceof Uint8Array) return JSON.stringify(Array.from(value));
|
|
288
|
-
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
289
|
-
if (value !== null && typeof value === 'object') {
|
|
290
|
-
const record = value as Record<string, unknown>;
|
|
291
|
-
return `{${Object.keys(record)
|
|
292
|
-
.sort()
|
|
293
|
-
.map((key) => `${JSON.stringify(key)}:${stable(record[key])}`)
|
|
294
|
-
.join(',')}}`;
|
|
295
|
-
}
|
|
296
|
-
return JSON.stringify(value) ?? 'null';
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async function fingerprint(value: unknown): Promise<string> {
|
|
300
|
-
const subtle = globalThis.crypto?.subtle;
|
|
301
|
-
if (subtle === undefined)
|
|
302
|
-
throw new Error('Web Crypto API is required for ScriptablePack fingerprints');
|
|
303
|
-
const digest = await subtle.digest('SHA-256', new TextEncoder().encode(stable(value)));
|
|
304
|
-
const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join(
|
|
305
|
-
'',
|
|
306
|
-
);
|
|
307
|
-
return `sha256:${hex}`;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function outputError(
|
|
311
|
-
definition: ScriptablePackDefinition,
|
|
312
|
-
output: Readonly<Record<string, unknown>>,
|
|
313
|
-
): ScriptablePackError | undefined {
|
|
314
|
-
const declaredKeys = Object.keys(definition.assets);
|
|
315
|
-
const outputKeys = Object.keys(output);
|
|
316
|
-
const outputKeySet = new Set(outputKeys);
|
|
317
|
-
const declaredKeySet = new Set(declaredKeys);
|
|
318
|
-
const missingKeys = declaredKeys.filter((key) => !outputKeySet.has(key));
|
|
319
|
-
const unexpectedSourceKeys = outputKeys.filter((key) => !declaredKeySet.has(key));
|
|
320
|
-
const kindMismatches = declaredKeys.flatMap((sourceKey) => {
|
|
321
|
-
const descriptor = definition.assets[sourceKey];
|
|
322
|
-
const value = output[sourceKey];
|
|
323
|
-
if (descriptor === undefined || value === undefined) return [];
|
|
324
|
-
const actualKind =
|
|
325
|
-
value !== null && typeof value === 'object' && 'kind' in value
|
|
326
|
-
? String((value as { readonly kind: unknown }).kind)
|
|
327
|
-
: typeof value;
|
|
328
|
-
return actualKind === descriptor.kind
|
|
329
|
-
? []
|
|
330
|
-
: [{ sourceKey, expected: descriptor.kind, actual: actualKind }];
|
|
331
|
-
});
|
|
332
|
-
if (
|
|
333
|
-
missingKeys.length === 0 &&
|
|
334
|
-
unexpectedSourceKeys.length === 0 &&
|
|
335
|
-
kindMismatches.length === 0
|
|
336
|
-
) {
|
|
337
|
-
return undefined;
|
|
338
|
-
}
|
|
339
|
-
return {
|
|
340
|
-
code: 'pack-source-output-invalid',
|
|
341
|
-
expected: 'build output keys and kinds to exactly match definition.assets',
|
|
342
|
-
hint: 'inspect sourceKey and GUID topology, then rebuild or cold-cook the ScriptablePack',
|
|
343
|
-
detail: {
|
|
344
|
-
missingGuids: missingKeys.map((key) =>
|
|
345
|
-
AssetGuid.format(definition.assets[key]?.guid as AssetGuidType),
|
|
346
|
-
),
|
|
347
|
-
unexpectedSourceKeys,
|
|
348
|
-
kindMismatches,
|
|
349
|
-
},
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function externalClosureError(
|
|
354
|
-
declared: ReadonlySet<string>,
|
|
355
|
-
referenced: ReadonlySet<string>,
|
|
356
|
-
read: ReadonlySet<string>,
|
|
357
|
-
): ScriptablePackError | undefined {
|
|
358
|
-
const used = new Set([...referenced, ...read]);
|
|
359
|
-
const undeclaredReferencedGuids = [...referenced].filter((guid) => !declared.has(guid));
|
|
360
|
-
const undeclaredReadGuids = [...read].filter((guid) => !declared.has(guid));
|
|
361
|
-
const unusedDeclaredGuids = [...declared].filter((guid) => !used.has(guid));
|
|
362
|
-
if (
|
|
363
|
-
undeclaredReferencedGuids.length === 0 &&
|
|
364
|
-
undeclaredReadGuids.length === 0 &&
|
|
365
|
-
unusedDeclaredGuids.length === 0
|
|
366
|
-
) {
|
|
367
|
-
return undefined;
|
|
368
|
-
}
|
|
369
|
-
return {
|
|
370
|
-
code: 'pack-source-external-closure-mismatch',
|
|
371
|
-
expected: 'externalAssets GUIDs to equal output external refs union AssetReader reads',
|
|
372
|
-
hint: 'inspect refs and AssetReader reads, repair GUID declarations, then rebuild or cold-cook',
|
|
373
|
-
detail: {
|
|
374
|
-
undeclaredReferencedGuids: undeclaredReferencedGuids.sort(),
|
|
375
|
-
undeclaredReadGuids: undeclaredReadGuids.sort(),
|
|
376
|
-
unusedDeclaredGuids: unusedDeclaredGuids.sort(),
|
|
377
|
-
},
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function productContractError(
|
|
382
|
-
sourceKey: string,
|
|
383
|
-
kind: string,
|
|
384
|
-
product: AssetOutputProduct,
|
|
385
|
-
): ScriptablePackError | undefined {
|
|
386
|
-
const mismatches: { sourceKey: string; expected: string; actual: string }[] = [];
|
|
387
|
-
if (product.payload.kind !== kind) {
|
|
388
|
-
mismatches.push({ sourceKey, expected: kind, actual: product.payload.kind });
|
|
389
|
-
}
|
|
390
|
-
for (const reference of product.refs) {
|
|
391
|
-
if (!AssetGuid.parse(reference.guid).ok) {
|
|
392
|
-
mismatches.push({
|
|
393
|
-
sourceKey,
|
|
394
|
-
expected: 'every producer ref to contain a valid Asset GUID',
|
|
395
|
-
actual: reference.guid,
|
|
396
|
-
});
|
|
397
|
-
break;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
for (const [artifactKey, artifact] of Object.entries(product.artifacts)) {
|
|
401
|
-
if (
|
|
402
|
-
artifactKey.length === 0 ||
|
|
403
|
-
artifactKey.startsWith('/') ||
|
|
404
|
-
artifactKey.includes('..') ||
|
|
405
|
-
artifactKey.includes('\\') ||
|
|
406
|
-
artifact.mediaType.trim().length === 0 ||
|
|
407
|
-
!(artifact.bytes instanceof Uint8Array)
|
|
408
|
-
) {
|
|
409
|
-
mismatches.push({
|
|
410
|
-
sourceKey,
|
|
411
|
-
expected: 'asset-local artifacts with safe keys, mediaType, and Uint8Array bytes',
|
|
412
|
-
actual: artifactKey,
|
|
413
|
-
});
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (mismatches.length === 0) return undefined;
|
|
418
|
-
return {
|
|
419
|
-
code: 'pack-source-output-invalid',
|
|
420
|
-
expected: 'producer output refs and asset-local artifacts to satisfy the Pack v2 contract',
|
|
421
|
-
hint: 'inspect sourceKey and artifact provenance, repair refs or bytes, then rebuild or cold-cook',
|
|
422
|
-
detail: { missingGuids: [], unexpectedSourceKeys: [], kindMismatches: mismatches },
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export interface BuildScriptablePackOptions {
|
|
427
|
-
readonly definition: ScriptablePackDefinition;
|
|
428
|
-
readonly sourcePath: string;
|
|
429
|
-
readonly assetSource?: ScriptablePackAssetSnapshotSource;
|
|
430
|
-
readonly outputs: AssetOutputProducerRegistry;
|
|
431
|
-
readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
|
|
432
|
-
readonly authoringContractVersion: string;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Build one external ScriptablePack through the producer-owned Pack bridge.
|
|
437
|
-
* Success preserves payloads, GUID refs, and local artifacts; failure retains
|
|
438
|
-
* `code`, `expected`, `hint`, and `detail` for recovery by the caller.
|
|
439
|
-
*/
|
|
440
|
-
export async function buildScriptablePack(
|
|
441
|
-
options: BuildScriptablePackOptions,
|
|
442
|
-
): Promise<ScriptablePackBuildBridgeResult> {
|
|
443
|
-
const observed = observedAssetReader(options.assetSource);
|
|
444
|
-
let built: Awaited<ReturnType<ScriptablePackDefinition['build']>>;
|
|
445
|
-
try {
|
|
446
|
-
const build = options.definition.build;
|
|
447
|
-
built = await build(observed.reader);
|
|
448
|
-
} catch (error) {
|
|
449
|
-
return err(
|
|
450
|
-
new ImportError({
|
|
451
|
-
code: 'import-internal-error',
|
|
452
|
-
expected: 'ScriptablePack build to return a structured Result without throwing',
|
|
453
|
-
hint: 'fix the build implementation and return a structured failure for expected authoring errors',
|
|
454
|
-
detail: { reason: error instanceof Error ? error.message : String(error) },
|
|
455
|
-
}),
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
if (!built.ok) {
|
|
459
|
-
const remappedBuildFailure = remapBuildFailureSource(built.error, options.sourcePath);
|
|
460
|
-
if (remappedBuildFailure !== undefined) return err(remappedBuildFailure);
|
|
461
|
-
if (
|
|
462
|
-
built.error instanceof ImportError ||
|
|
463
|
-
built.error instanceof AssetError ||
|
|
464
|
-
isStructuredDomainError(built.error)
|
|
465
|
-
) {
|
|
466
|
-
return err(built.error);
|
|
467
|
-
}
|
|
468
|
-
return err(
|
|
469
|
-
new ImportError({
|
|
470
|
-
code: 'import-internal-error',
|
|
471
|
-
expected: 'ScriptablePack build failure to use a structured domain error',
|
|
472
|
-
hint: 'return an error with code, expected, hint, and optional detail fields',
|
|
473
|
-
detail: { reason: String(built.error) },
|
|
474
|
-
}),
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
if (!record(built.value)) {
|
|
478
|
-
return err(
|
|
479
|
-
new ImportError({
|
|
480
|
-
code: 'import-internal-error',
|
|
481
|
-
expected: 'ScriptablePack build output to be an object keyed by declared sourceKey',
|
|
482
|
-
hint: 'return one concrete Asset payload for each declared sourceKey',
|
|
483
|
-
detail: { reason: 'build output is not an object' },
|
|
484
|
-
}),
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
const output = built.value;
|
|
488
|
-
const invalidOutput = outputError(options.definition, output);
|
|
489
|
-
if (invalidOutput !== undefined) return err(invalidOutput);
|
|
490
|
-
|
|
491
|
-
const assets: ImportedAsset<unknown>[] = [];
|
|
492
|
-
for (const sourceKey of Object.keys(options.definition.assets).sort()) {
|
|
493
|
-
const descriptor = options.definition.assets[sourceKey];
|
|
494
|
-
const asset = output[sourceKey];
|
|
495
|
-
if (descriptor === undefined) continue;
|
|
496
|
-
if (!isAsset(asset)) {
|
|
497
|
-
return err(
|
|
498
|
-
new ImportError({
|
|
499
|
-
code: 'import-internal-error',
|
|
500
|
-
expected: `a concrete Asset payload for sourceKey ${sourceKey}`,
|
|
501
|
-
hint: 'return a durable Asset with a kind from SCRIPTABLE_PACK_ASSET_KINDS',
|
|
502
|
-
detail: { reason: 'build output payload is missing or has an unknown kind' },
|
|
503
|
-
}),
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
const producer = options.outputs.get(descriptor.kind);
|
|
507
|
-
if (producer === undefined) {
|
|
508
|
-
return err({
|
|
509
|
-
code: 'pack-source-output-invalid',
|
|
510
|
-
expected: `a domain output producer registered for kind ${descriptor.kind}`,
|
|
511
|
-
hint: 'attach the owning producer capability, inspect registration, then rebuild or cold-cook',
|
|
512
|
-
detail: {
|
|
513
|
-
missingGuids: [AssetGuid.format(descriptor.guid)],
|
|
514
|
-
unexpectedSourceKeys: [],
|
|
515
|
-
kindMismatches: [],
|
|
516
|
-
},
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
const product = await producer.produce({
|
|
520
|
-
guid: AssetGuid.format(descriptor.guid),
|
|
521
|
-
sourceKey,
|
|
522
|
-
asset,
|
|
523
|
-
});
|
|
524
|
-
if (!product.ok) return err(product.error);
|
|
525
|
-
const productError = productContractError(sourceKey, descriptor.kind, product.value);
|
|
526
|
-
if (productError !== undefined) return err(productError);
|
|
527
|
-
assets.push({
|
|
528
|
-
guid: AssetGuid.format(descriptor.guid),
|
|
529
|
-
kind: descriptor.kind,
|
|
530
|
-
...(descriptor.name === undefined ? {} : { name: descriptor.name }),
|
|
531
|
-
payload: product.value.payload,
|
|
532
|
-
refs: product.value.refs,
|
|
533
|
-
artifacts: product.value.artifacts,
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const local = new Set(assets.map((asset) => asset.guid.toLowerCase()));
|
|
538
|
-
const referenced = new Set(
|
|
539
|
-
assets
|
|
540
|
-
.flatMap((asset) => asset.refs.map((reference) => reference.guid.toLowerCase()))
|
|
541
|
-
.filter((guid) => !local.has(guid)),
|
|
542
|
-
);
|
|
543
|
-
const read = new Set([...observed.reads.keys()].map((guid) => guid.toLowerCase()));
|
|
544
|
-
const declared = new Set(
|
|
545
|
-
Object.values(options.definition.externalAssets).map((guid) =>
|
|
546
|
-
AssetGuid.format(guid).toLowerCase(),
|
|
547
|
-
),
|
|
548
|
-
);
|
|
549
|
-
const closureError = externalClosureError(declared, referenced, read);
|
|
550
|
-
if (closureError !== undefined) return err(closureError);
|
|
551
|
-
|
|
552
|
-
const externalEvidence = [...declared].sort().map((guid): ScriptablePackExternalEvidence => {
|
|
553
|
-
const observedRead = observed.reads.get(guid);
|
|
554
|
-
const isReference = referenced.has(guid);
|
|
555
|
-
return {
|
|
556
|
-
guid,
|
|
557
|
-
usage: observedRead === undefined ? 'reference' : isReference ? 'both' : 'content',
|
|
558
|
-
...(observedRead === undefined
|
|
559
|
-
? {}
|
|
560
|
-
: { generation: observedRead.generation, digest: observedRead.digest }),
|
|
561
|
-
};
|
|
562
|
-
});
|
|
563
|
-
// The dependency generation is runtime provenance, not a content identity.
|
|
564
|
-
// It may advance when a fresh AssetRegistry is created even though the
|
|
565
|
-
// referenced bytes are unchanged. Keep it in the receipt for diagnostics,
|
|
566
|
-
// but exclude it from the source fingerprint so an identical source closure
|
|
567
|
-
// and dependency digest produce the same publication fence across Edit and
|
|
568
|
-
// fresh Play worlds.
|
|
569
|
-
const fingerprintEvidence = externalEvidence.map(({ guid, usage, digest }) => ({
|
|
570
|
-
guid,
|
|
571
|
-
usage,
|
|
572
|
-
...(digest === undefined ? {} : { digest }),
|
|
573
|
-
}));
|
|
574
|
-
const inputFingerprint = await fingerprint({
|
|
575
|
-
meta: projectScriptablePackMeta(options.definition, options.sourcePath),
|
|
576
|
-
sceneComponents: projectScriptablePackSceneComponents(options.definition.sceneComponents),
|
|
577
|
-
sourceClosure: stableSourceClosure(options.sourceClosure),
|
|
578
|
-
externalEvidence: fingerprintEvidence,
|
|
579
|
-
authoringContractVersion: options.authoringContractVersion,
|
|
580
|
-
producerVersions: options.outputs.versions(),
|
|
581
|
-
});
|
|
582
|
-
const refs = assets.flatMap((asset) => asset.refs);
|
|
583
|
-
const artifacts = Object.fromEntries(
|
|
584
|
-
assets.flatMap((asset) =>
|
|
585
|
-
Object.entries(asset.artifacts).map(([key, artifact]) => [`${asset.guid}/${key}`, artifact]),
|
|
586
|
-
),
|
|
587
|
-
);
|
|
588
|
-
const product = createImportProduct({
|
|
589
|
-
assets,
|
|
590
|
-
sourceDependencies: options.sourceClosure.map((entry) => entry.path),
|
|
591
|
-
refs,
|
|
592
|
-
artifacts,
|
|
593
|
-
receipts: assets.map((asset) => ({
|
|
594
|
-
guid: asset.guid,
|
|
595
|
-
origin: 'sourceMeta' as const,
|
|
596
|
-
status: 'succeeded' as const,
|
|
597
|
-
inputFingerprint,
|
|
598
|
-
})),
|
|
599
|
-
diagnostics: [],
|
|
600
|
-
sourceRevision: inputFingerprint,
|
|
601
|
-
sourceKey: options.sourcePath,
|
|
602
|
-
});
|
|
603
|
-
if (!product.ok) return err(product.error);
|
|
604
|
-
return ok({
|
|
605
|
-
product: product.value,
|
|
606
|
-
stagedOutputs: await Promise.all(
|
|
607
|
-
Object.keys(options.definition.assets)
|
|
608
|
-
.sort()
|
|
609
|
-
.map(async (sourceKey) => {
|
|
610
|
-
const descriptor = options.definition.assets[sourceKey];
|
|
611
|
-
const asset = output[sourceKey];
|
|
612
|
-
if (descriptor === undefined || !isAsset(asset)) {
|
|
613
|
-
throw new Error(`validated ScriptablePack output ${sourceKey} is not an Asset`);
|
|
614
|
-
}
|
|
615
|
-
return {
|
|
616
|
-
guid: descriptor.guid,
|
|
617
|
-
sourceKey,
|
|
618
|
-
asset: privateClone(asset),
|
|
619
|
-
digest: await fingerprint(asset),
|
|
620
|
-
};
|
|
621
|
-
}),
|
|
622
|
-
),
|
|
623
|
-
externalEvidence,
|
|
624
|
-
inputFingerprint,
|
|
625
|
-
});
|
|
626
|
-
}
|
|
154
|
+
/** The source authoring contract is owned by Pack; this alias keeps reader typing local. */
|
|
155
|
+
export type ScriptablePackAssetReader = AssetReader;
|
|
156
|
+
export type ScriptablePackAuthoringError = PackAuthoringError;
|
|
157
|
+
export type ScriptablePackImportedAsset = ImportedAsset<unknown>;
|
|
@@ -110,6 +110,9 @@ function importFailureCode(error: ImportError): SourcePackageErrorCode {
|
|
|
110
110
|
case 'source-read-failed':
|
|
111
111
|
case 'import-internal-error':
|
|
112
112
|
case 'mesh-material-slot-topology-change':
|
|
113
|
+
case 'mesh-lod-contract-invalid':
|
|
114
|
+
case 'mesh-lod-topology-change':
|
|
115
|
+
case 'mesh-lod-authority-conflict':
|
|
113
116
|
case 'unknown-source-key':
|
|
114
117
|
case 'duplicate-source-key':
|
|
115
118
|
case 'invalid-source-overrides':
|
|
@@ -124,6 +127,9 @@ const IMPORT_ERROR_CODES = new Set([
|
|
|
124
127
|
'import-produced-no-assets',
|
|
125
128
|
'guid-mismatch',
|
|
126
129
|
'mesh-material-slot-topology-change',
|
|
130
|
+
'mesh-lod-contract-invalid',
|
|
131
|
+
'mesh-lod-topology-change',
|
|
132
|
+
'mesh-lod-authority-conflict',
|
|
127
133
|
'import-internal-error',
|
|
128
134
|
'source-validation-failed',
|
|
129
135
|
'unknown-source-key',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { dirname } from 'node:path';
|
|
3
4
|
import {
|
|
@@ -158,7 +159,7 @@ async function commitSourcePackageDdc(
|
|
|
158
159
|
async function persistTransport(transport: ImportPublicationInput['transport']): Promise<boolean> {
|
|
159
160
|
if (transport === undefined) return true;
|
|
160
161
|
const directory = dirname(transport.path);
|
|
161
|
-
const temporary = `${transport.path}.tmp`;
|
|
162
|
+
const temporary = `${transport.path}.${randomUUID()}.tmp`;
|
|
162
163
|
try {
|
|
163
164
|
await mkdir(directory, { recursive: true });
|
|
164
165
|
if (((await stat(directory)).mode & 0o222) === 0) {
|
|
@@ -306,6 +307,35 @@ export async function commitImportPublication(
|
|
|
306
307
|
),
|
|
307
308
|
};
|
|
308
309
|
}
|
|
310
|
+
const transportPersisted = await persistTransport(candidate.input.transport);
|
|
311
|
+
if (!transportPersisted) {
|
|
312
|
+
await restoreImportPublication(candidate);
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
error: {
|
|
316
|
+
code: 'source-package-publication-invalid',
|
|
317
|
+
expected: 'the sidecar and DDC publication to commit atomically',
|
|
318
|
+
hint: 'repair the sidecar destination, then rebuild or cold-cook the source package',
|
|
319
|
+
detail: 'sidecar transport persistence failed; restored the previous DDC/LKG generation',
|
|
320
|
+
diagnostic: sourcePackageError(
|
|
321
|
+
'source-package-publication-invalid',
|
|
322
|
+
{
|
|
323
|
+
sourceMeta: '<import-publication>',
|
|
324
|
+
anchorGuid: candidate.input.guid,
|
|
325
|
+
affectedGuids: candidate.input.publishedGuids,
|
|
326
|
+
producer: 'source-package/import-publication',
|
|
327
|
+
importer: 'import-publication',
|
|
328
|
+
},
|
|
329
|
+
{ stage: 'route-integrity', reason: 'sidecar transport persistence failed' },
|
|
330
|
+
),
|
|
331
|
+
},
|
|
332
|
+
head: await inspectHead(
|
|
333
|
+
candidate.input.root,
|
|
334
|
+
candidate.input.guid,
|
|
335
|
+
candidate.input.desiredKey,
|
|
336
|
+
),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
309
339
|
const projected = projectImportPublication(candidate.input, publication.value, Date.now());
|
|
310
340
|
return {
|
|
311
341
|
ok: true,
|
|
@@ -313,7 +343,7 @@ export async function commitImportPublication(
|
|
|
313
343
|
head: publication.value,
|
|
314
344
|
catalog: projected.catalog,
|
|
315
345
|
revision: projected.revision,
|
|
316
|
-
transportPersisted
|
|
346
|
+
transportPersisted,
|
|
317
347
|
};
|
|
318
348
|
}
|
|
319
349
|
|