@forgeax/engine-import 0.1.21 → 0.1.24
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__/scriptable-pack-build.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-build.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 +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2850 -1157
- 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/scriptable-pack-build.d.ts +68 -0
- package/dist/scriptable-pack-build.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 +68 -37
- 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 +15 -27
- 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 +7 -0
- 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__/scriptable-pack-build.unit.test.ts +267 -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 +2 -0
- 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 +167 -1
- package/src/browser.ts +5 -0
- package/src/build-production.ts +491 -140
- 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 +47 -16
- package/src/mesh-bin.ts +38 -0
- package/src/mesh-lod.ts +265 -0
- package/src/scriptable-pack-build.ts +725 -0
- package/src/scriptable-pack-file-snapshot.ts +346 -0
- package/src/scriptable-pack-host.ts +785 -340
- package/src/scriptable-pack-output-producers.ts +55 -0
- package/src/scriptable-pack-staged-snapshot.ts +12 -11
- package/src/scriptable-pack.ts +18 -486
- package/src/source-package-errors.ts +6 -0
- package/src/source-package-publication.ts +273 -11
- 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
|
}
|
|
@@ -181,17 +136,15 @@ export interface ScriptablePackExternalEvidence {
|
|
|
181
136
|
readonly digest?: string;
|
|
182
137
|
}
|
|
183
138
|
|
|
184
|
-
|
|
139
|
+
/** Shared terminal product envelope for every Pack source form. */
|
|
140
|
+
export interface PackBuildProduct {
|
|
185
141
|
readonly product: TerminalImportProduct<unknown>;
|
|
186
|
-
/** Raw private outputs for the current staged build generation, never a runtime publication path. */
|
|
187
142
|
readonly stagedOutputs: readonly ScriptablePackStagedOutput[];
|
|
188
143
|
readonly externalEvidence: readonly ScriptablePackExternalEvidence[];
|
|
189
144
|
readonly inputFingerprint: string;
|
|
190
|
-
/** Engine publication fields projected after ordinary producers run. */
|
|
191
145
|
readonly publication?: AssetPublicationEnvelope;
|
|
192
146
|
}
|
|
193
147
|
|
|
194
|
-
/** Domain-owned structured failure transported without flattening its recovery fields. */
|
|
195
148
|
export interface ScriptablePackDomainError {
|
|
196
149
|
readonly code: string;
|
|
197
150
|
readonly expected: string;
|
|
@@ -199,428 +152,7 @@ export interface ScriptablePackDomainError {
|
|
|
199
152
|
readonly detail?: unknown;
|
|
200
153
|
}
|
|
201
154
|
|
|
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
|
-
}
|
|
155
|
+
/** The source authoring contract is owned by Pack; this alias keeps reader typing local. */
|
|
156
|
+
export type ScriptablePackAssetReader = AssetReader;
|
|
157
|
+
export type ScriptablePackAuthoringError = PackAuthoringError;
|
|
158
|
+
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',
|