@forgeax/engine-pack 0.1.3 → 0.1.4
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 +51 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
- package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
- package/dist/__tests__/package-finalizer.contract.test.d.ts +2 -0
- package/dist/__tests__/package-finalizer.contract.test.d.ts.map +1 -0
- package/dist/__tests__/runtime.browser.test.d.ts +2 -0
- package/dist/__tests__/runtime.browser.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-inventory.contract.test.d.ts +2 -0
- package/dist/__tests__/scanner-inventory.contract.test.d.ts.map +1 -0
- package/dist/artifact-path.mjs +53 -0
- package/dist/artifact-path.mjs.map +1 -0
- package/dist/atlas/shelf-pack.d.ts.map +1 -1
- package/dist/build.d.ts +17 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.mjs +10168 -0
- package/dist/build.mjs.map +1 -0
- package/dist/builtin.d.ts +20 -0
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.mjs +141 -0
- package/dist/builtin.mjs.map +1 -0
- package/dist/catalog-builder.d.ts +43 -0
- package/dist/catalog-builder.d.ts.map +1 -0
- package/dist/catalog-delta.d.ts +11 -0
- package/dist/catalog-delta.d.ts.map +1 -0
- package/dist/catalog-projection.d.ts +91 -0
- package/dist/catalog-projection.d.ts.map +1 -0
- package/dist/cli-asset.mjs +231 -484
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/index.mjs +83 -1
- package/dist/index.mjs.map +1 -1
- package/dist/material-cook.d.ts +2 -0
- package/dist/material-cook.d.ts.map +1 -0
- package/dist/material-cook.mjs +193 -0
- package/dist/material-cook.mjs.map +1 -0
- package/dist/mesh-bin-contract.mjs +96 -0
- package/dist/mesh-bin-contract.mjs.map +1 -0
- package/dist/native-cooker-registry.d.ts +19 -0
- package/dist/native-cooker-registry.d.ts.map +1 -1
- package/dist/native-cooker.mjs +37 -0
- package/dist/native-cooker.mjs.map +1 -1
- package/dist/package-finalizer.d.ts +121 -0
- package/dist/package-finalizer.d.ts.map +1 -0
- package/dist/resolve-asset-source.d.ts.map +1 -1
- package/dist/resolve-asset-source.mjs +16 -6
- package/dist/resolve-asset-source.mjs.map +1 -1
- package/dist/runtime-publication.d.ts +44 -0
- package/dist/runtime-publication.d.ts.map +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.mjs +7261 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/scanner.d.ts +87 -54
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.mjs +258 -454
- package/dist/scanner.mjs.map +1 -1
- package/dist/scriptable-pack-node.d.ts +3 -8
- package/dist/scriptable-pack-node.d.ts.map +1 -1
- package/dist/scriptable-pack-node.mjs +93 -9
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack-worker.mjs +1 -0
- package/dist/scriptable-pack-worker.mjs.map +1 -1
- package/dist/scriptable-pack.d.ts +33 -0
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/scriptable-pack.mjs +91 -2
- package/dist/scriptable-pack.mjs.map +1 -1
- package/dist/topology.d.ts +1 -1
- package/dist/topology.d.ts.map +1 -1
- package/package.json +27 -2
- package/src/__tests__/mesh-bin-consumer-surface.unit.test.ts +2 -1
- package/src/__tests__/owner-chain.integration.test.ts +78 -0
- package/src/__tests__/package-finalizer.contract.test.ts +105 -0
- package/src/__tests__/runtime.browser.test.ts +15 -0
- package/src/__tests__/scanner-inventory.contract.test.ts +167 -0
- package/src/__tests__/scanner-inventory.test.ts +16 -12
- package/src/__tests__/scriptable-pack.unit.test.ts +42 -0
- package/src/atlas/shelf-pack.ts +20 -24
- package/src/build.ts +137 -0
- package/src/builtin.ts +38 -0
- package/src/catalog-builder.ts +313 -0
- package/src/catalog-delta.ts +189 -0
- package/src/catalog-projection.ts +344 -0
- package/src/cli-asset.ts +1 -5
- package/src/index.ts +2 -2
- package/src/material-cook.ts +13 -0
- package/src/native-cooker-registry.ts +72 -0
- package/src/package-finalizer.ts +394 -0
- package/src/resolve-asset-source.ts +2 -7
- package/src/runtime-publication.ts +190 -0
- package/src/runtime.ts +23 -0
- package/src/scanner.ts +289 -290
- package/src/scriptable-pack-node.ts +27 -23
- package/src/scriptable-pack-worker.ts +2 -0
- package/src/scriptable-pack.ts +123 -1
- package/src/topology.ts +2 -1
package/src/scanner.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
3
|
import { basename, join } from 'node:path';
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
CatalogDiagnostic,
|
|
6
|
+
PackErrorCode,
|
|
7
|
+
ProviderProvenance,
|
|
8
|
+
ResourceRevision,
|
|
9
|
+
SourceOverrideDescriptor,
|
|
10
|
+
} from '@forgeax/engine-types';
|
|
4
11
|
import { PACK_ERROR_HINTS } from '@forgeax/engine-types';
|
|
5
12
|
import { loadAssetConfig } from './config.js';
|
|
6
13
|
import { PackError } from './errors.js';
|
|
@@ -8,13 +15,16 @@ import { isValidAssetGuidString } from './guid.js';
|
|
|
8
15
|
import { validateProducerContract, validateProducerOutputs } from './producer-contract.js';
|
|
9
16
|
import { resolveAssetSource } from './resolve-asset-source.js';
|
|
10
17
|
import { validateMeta, validatePack } from './schema-compiled.js';
|
|
11
|
-
import { projectScriptablePackMeta, type ScriptablePackMetaJson } from './scriptable-pack.js';
|
|
12
18
|
import {
|
|
13
|
-
|
|
19
|
+
projectScriptablePackMeta,
|
|
20
|
+
type ScriptablePackDefinition,
|
|
21
|
+
type ScriptablePackMetaJson,
|
|
22
|
+
type ScriptablePackSourceClosureEntry,
|
|
23
|
+
} from './scriptable-pack.js';
|
|
24
|
+
import {
|
|
14
25
|
inventoryScriptablePackSource,
|
|
15
|
-
type LoadScriptablePackOptions,
|
|
16
26
|
loadScriptablePack,
|
|
17
|
-
type
|
|
27
|
+
type ScriptablePackModuleExecutor,
|
|
18
28
|
} from './scriptable-pack-node.js';
|
|
19
29
|
|
|
20
30
|
// Minimal Result<T, E> — structurally compatible with @forgeax/engine-rhi Result
|
|
@@ -34,58 +44,117 @@ function packErr<E>(error: E): ScanResult<never, E> {
|
|
|
34
44
|
/** Host-owned source paths that should not enter the Pack catalog. */
|
|
35
45
|
export interface ScanOptions {
|
|
36
46
|
readonly ignorePath?: (path: string) => boolean;
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
readonly scriptablePack?: ScriptablePackScanOptions;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One bounded executor policy shared by ScriptablePack inventory and production. */
|
|
51
|
+
export interface ScriptablePackScanOptions {
|
|
52
|
+
readonly timeoutMs?: number;
|
|
53
|
+
readonly buildTimeoutMs?: number;
|
|
54
|
+
readonly executor?: ScriptablePackModuleExecutor;
|
|
55
|
+
/** Path-only scans must release the isolated loader before returning. */
|
|
56
|
+
readonly metadataOnly?: boolean;
|
|
39
57
|
}
|
|
40
58
|
|
|
41
|
-
|
|
59
|
+
/** Stable default policy shared by inventory and production owners. */
|
|
60
|
+
export const STANDARD_SCRIPTABLE_PACK_SCAN_OPTIONS = Object.freeze(
|
|
61
|
+
{},
|
|
62
|
+
) satisfies ScriptablePackScanOptions;
|
|
42
63
|
|
|
43
|
-
export interface
|
|
64
|
+
export interface InventoryDeclaration {
|
|
44
65
|
readonly guid: string;
|
|
66
|
+
readonly kind: string;
|
|
67
|
+
readonly sourcePath: string;
|
|
68
|
+
readonly sourceRevision: string;
|
|
45
69
|
readonly sourceKey?: string;
|
|
46
70
|
readonly sourceIndex?: number;
|
|
47
|
-
readonly kind: string;
|
|
48
|
-
readonly name?: string;
|
|
49
|
-
readonly execution?: 'direct' | 'cooked';
|
|
50
|
-
readonly refs: readonly string[];
|
|
51
71
|
}
|
|
52
72
|
|
|
53
|
-
export interface
|
|
54
|
-
readonly
|
|
73
|
+
export interface ScanInventory {
|
|
74
|
+
readonly paths: readonly string[];
|
|
75
|
+
readonly inventory: readonly InventoryDeclaration[];
|
|
76
|
+
/** Complete parsed source declarations captured by the validated scan pass. */
|
|
77
|
+
readonly declarations: ReadonlyMap<string, ScanSourceDeclaration>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ScriptablePackInventoryDeclaration {
|
|
81
|
+
readonly sourcePath: string;
|
|
82
|
+
readonly sourceRevision: string;
|
|
83
|
+
readonly meta: ScriptablePackMetaJson;
|
|
84
|
+
readonly definition: Readonly<ScriptablePackDefinition>;
|
|
85
|
+
readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface MetaInventoryDocument {
|
|
89
|
+
readonly schemaVersion: string | number;
|
|
90
|
+
readonly kind: 'external-asset-package';
|
|
55
91
|
readonly packageId?: string;
|
|
56
|
-
readonly importer?: string;
|
|
57
|
-
readonly source?: string;
|
|
58
92
|
readonly name?: string;
|
|
59
|
-
readonly
|
|
60
|
-
readonly
|
|
93
|
+
readonly provenance?: ProviderProvenance;
|
|
94
|
+
readonly revision?: ResourceRevision;
|
|
95
|
+
readonly diagnostics?: readonly CatalogDiagnostic[];
|
|
96
|
+
readonly importer: string;
|
|
97
|
+
readonly source?: string;
|
|
98
|
+
readonly importSettings: Readonly<Record<string, unknown>>;
|
|
99
|
+
readonly sourceOverrides?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
100
|
+
readonly sourceOverrideDescriptors?: readonly SourceOverrideDescriptor[];
|
|
101
|
+
readonly paramSchema?: readonly Readonly<Record<string, unknown>>[];
|
|
102
|
+
readonly subAssets: readonly MetaInventorySubAsset[];
|
|
61
103
|
}
|
|
62
104
|
|
|
63
|
-
export interface
|
|
64
|
-
readonly
|
|
65
|
-
readonly
|
|
105
|
+
export interface MetaInventorySubAsset {
|
|
106
|
+
readonly guid: string;
|
|
107
|
+
readonly sourceIndex: number;
|
|
108
|
+
readonly sourceKey?: string;
|
|
109
|
+
readonly name?: string;
|
|
110
|
+
readonly kind: string;
|
|
66
111
|
}
|
|
67
112
|
|
|
68
|
-
export interface
|
|
69
|
-
readonly
|
|
70
|
-
readonly
|
|
71
|
-
readonly producer: AssetInventoryProducerKind;
|
|
113
|
+
export interface PackInventoryDocument {
|
|
114
|
+
readonly schemaVersion: '1.0.0' | '2.0.0';
|
|
115
|
+
readonly kind: 'internal-text-package';
|
|
72
116
|
readonly packageId?: string;
|
|
73
|
-
readonly
|
|
74
|
-
readonly
|
|
75
|
-
readonly
|
|
76
|
-
readonly
|
|
77
|
-
readonly diagnostics: readonly AssetInventoryDiagnostic[];
|
|
117
|
+
readonly provenance?: ProviderProvenance;
|
|
118
|
+
readonly revision?: ResourceRevision;
|
|
119
|
+
readonly diagnostics?: readonly CatalogDiagnostic[];
|
|
120
|
+
readonly assets: readonly PackInventoryAsset[];
|
|
78
121
|
}
|
|
79
122
|
|
|
80
|
-
export interface
|
|
81
|
-
readonly
|
|
82
|
-
readonly
|
|
83
|
-
|
|
84
|
-
readonly
|
|
85
|
-
readonly
|
|
86
|
-
|
|
87
|
-
readonly
|
|
88
|
-
readonly
|
|
123
|
+
export interface PackInventoryAsset {
|
|
124
|
+
readonly guid: string;
|
|
125
|
+
readonly kind: string;
|
|
126
|
+
readonly name?: string;
|
|
127
|
+
readonly execution?: 'direct' | 'cooked';
|
|
128
|
+
readonly sourceKey?: string;
|
|
129
|
+
readonly sourceIndex?: number;
|
|
130
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
131
|
+
readonly refs: readonly string[];
|
|
132
|
+
readonly artifacts?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type ScanSourceDeclaration =
|
|
136
|
+
| {
|
|
137
|
+
readonly format: 'meta.json';
|
|
138
|
+
readonly sourcePath: string;
|
|
139
|
+
readonly sourceRevision: string;
|
|
140
|
+
readonly value: MetaInventoryDocument;
|
|
141
|
+
}
|
|
142
|
+
| {
|
|
143
|
+
readonly format: 'pack.json';
|
|
144
|
+
readonly sourcePath: string;
|
|
145
|
+
readonly sourceRevision: string;
|
|
146
|
+
readonly sourceText: string;
|
|
147
|
+
readonly value: PackInventoryDocument;
|
|
148
|
+
}
|
|
149
|
+
| ({
|
|
150
|
+
readonly format: 'pack.ts';
|
|
151
|
+
readonly sourcePath: string;
|
|
152
|
+
readonly sourceRevision: string;
|
|
153
|
+
readonly value: ScriptablePackMetaJson;
|
|
154
|
+
} & ScriptablePackInventoryDeclaration);
|
|
155
|
+
|
|
156
|
+
interface ScanCapture {
|
|
157
|
+
readonly declarations: Map<string, ScanSourceDeclaration>;
|
|
89
158
|
}
|
|
90
159
|
|
|
91
160
|
/**
|
|
@@ -111,6 +180,45 @@ const BLACKLIST = new Set([
|
|
|
111
180
|
|
|
112
181
|
export const SCANNER_BLACKLIST: ReadonlySet<string> = BLACKLIST;
|
|
113
182
|
|
|
183
|
+
type MalformedFileCode = Extract<PackErrorCode, 'pack-malformed-pack' | 'pack-malformed-meta'>;
|
|
184
|
+
|
|
185
|
+
type JsonValidator = {
|
|
186
|
+
(value: unknown): boolean;
|
|
187
|
+
errors?: readonly { readonly instancePath?: string; readonly message?: string }[] | null;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
async function readValidatedJson(
|
|
191
|
+
path: string,
|
|
192
|
+
code: MalformedFileCode,
|
|
193
|
+
validate: JsonValidator,
|
|
194
|
+
): Promise<ScanResult<{ readonly raw: string; readonly parsed: unknown }, PackError>> {
|
|
195
|
+
let raw: string;
|
|
196
|
+
let parsed: unknown;
|
|
197
|
+
try {
|
|
198
|
+
raw = await readFile(path, 'utf-8');
|
|
199
|
+
parsed = JSON.parse(raw);
|
|
200
|
+
} catch {
|
|
201
|
+
return packErr(
|
|
202
|
+
makePackError(code, {
|
|
203
|
+
path,
|
|
204
|
+
ajvErrors: [{ instancePath: '', message: 'JSON parse failed' }],
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (!validate(parsed)) {
|
|
209
|
+
return packErr(
|
|
210
|
+
makePackError(code, {
|
|
211
|
+
path,
|
|
212
|
+
ajvErrors: (validate.errors ?? []).map((error) => ({
|
|
213
|
+
instancePath: error.instancePath ?? '',
|
|
214
|
+
message: error.message ?? 'unknown ajv error',
|
|
215
|
+
})),
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return ok({ raw, parsed });
|
|
220
|
+
}
|
|
221
|
+
|
|
114
222
|
function makePackError(
|
|
115
223
|
code: PackErrorCode,
|
|
116
224
|
detail: ConstructorParameters<typeof PackError>[0]['detail'],
|
|
@@ -153,7 +261,7 @@ function* extractMountSourceGuids(asset: {
|
|
|
153
261
|
/**
|
|
154
262
|
* Scan one or more root directories for `.meta.json`, `.pack.json`, and `.pack.ts` files.
|
|
155
263
|
* Runs a 7-step fail-fast validation chain (w17 + M7-T01):
|
|
156
|
-
* Step 1 - collect all .meta.json + .pack.json
|
|
264
|
+
* Step 1 - collect all .meta.json + .pack.json paths (blacklist skipped)
|
|
157
265
|
* Step 2 - schema validation (ajv strict)
|
|
158
266
|
* Step 3 - GUID string format validation
|
|
159
267
|
* Step 4 - GUID collision detection
|
|
@@ -161,17 +269,18 @@ function* extractMountSourceGuids(asset: {
|
|
|
161
269
|
* Step 6 - cyclic reference detection (hand-written DFS)
|
|
162
270
|
* Step 7 - complete pack and source closure validation
|
|
163
271
|
*
|
|
164
|
-
* Returns
|
|
272
|
+
* Returns `Ok(paths)` or `Err(PackError)` on the first violation.
|
|
165
273
|
*
|
|
166
274
|
* NOTE: source files without a .meta.json are logged but not fatal (requirements §5).
|
|
167
275
|
*/
|
|
168
|
-
|
|
276
|
+
async function scanValidated(
|
|
169
277
|
roots: readonly string[],
|
|
170
278
|
opts: ScanOptions = {},
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
//
|
|
279
|
+
capture?: ScanCapture,
|
|
280
|
+
): Promise<ScanResult<string[], PackError>> {
|
|
281
|
+
// Step 1: collect all authored package declarations. ScriptablePack runtime
|
|
282
|
+
// validation belongs to its trusted module loader; scanner only inventories
|
|
283
|
+
// the source path so CLI/Vite share one discovery set.
|
|
175
284
|
const rawPaths: string[] = [];
|
|
176
285
|
const explicitRootSet = new Set(roots);
|
|
177
286
|
|
|
@@ -240,36 +349,11 @@ export async function scanInventory(
|
|
|
240
349
|
// kind stays in the path/detail evidence; identity is the normalized GUID.
|
|
241
350
|
const guidToPath = new Map<string, string>();
|
|
242
351
|
const packRefs = new Map<string, string[]>(); // guid -> refs[]
|
|
243
|
-
const scriptablePackMeta = new Map<string, ScriptablePackMetaJson>();
|
|
244
|
-
const metaByGuid = new Map<string, string>();
|
|
245
|
-
const declarations: AssetInventoryDeclaration[] = [];
|
|
246
352
|
|
|
247
353
|
for (const packPath of packPaths) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
parsed = JSON.parse(raw);
|
|
252
|
-
} catch {
|
|
253
|
-
return packErr(
|
|
254
|
-
makePackError('pack-malformed-pack', {
|
|
255
|
-
path: packPath,
|
|
256
|
-
ajvErrors: [{ instancePath: '', message: 'JSON parse failed' }],
|
|
257
|
-
}),
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const valid = validatePack(parsed);
|
|
262
|
-
if (!valid) {
|
|
263
|
-
return packErr(
|
|
264
|
-
makePackError('pack-malformed-pack', {
|
|
265
|
-
path: packPath,
|
|
266
|
-
ajvErrors: (validatePack.errors ?? []).map((e) => ({
|
|
267
|
-
instancePath: e.instancePath,
|
|
268
|
-
message: e.message ?? 'unknown ajv error',
|
|
269
|
-
})),
|
|
270
|
-
}),
|
|
271
|
-
);
|
|
272
|
-
}
|
|
354
|
+
const loaded = await readValidatedJson(packPath, 'pack-malformed-pack', validatePack);
|
|
355
|
+
if (!loaded.ok) return loaded;
|
|
356
|
+
const { raw, parsed } = loaded.value;
|
|
273
357
|
|
|
274
358
|
const packageContract = validateProducerContract(parsed);
|
|
275
359
|
if (!packageContract.ok) {
|
|
@@ -282,20 +366,7 @@ export async function scanInventory(
|
|
|
282
366
|
}
|
|
283
367
|
|
|
284
368
|
// Step 3: validate GUIDs in pack
|
|
285
|
-
const packObj = parsed as
|
|
286
|
-
packageId?: string;
|
|
287
|
-
assets: {
|
|
288
|
-
guid: string;
|
|
289
|
-
kind: string;
|
|
290
|
-
execution?: 'direct' | 'cooked';
|
|
291
|
-
name?: string;
|
|
292
|
-
payload: unknown;
|
|
293
|
-
refs: string[];
|
|
294
|
-
artifacts?: Readonly<Record<string, unknown>>;
|
|
295
|
-
sourceKey?: string;
|
|
296
|
-
sourceIndex?: number;
|
|
297
|
-
}[];
|
|
298
|
-
};
|
|
369
|
+
const packObj = parsed as unknown as PackInventoryDocument;
|
|
299
370
|
for (const asset of packObj.assets) {
|
|
300
371
|
if (
|
|
301
372
|
asset.kind === 'particle-effect' &&
|
|
@@ -332,7 +403,12 @@ export async function scanInventory(
|
|
|
332
403
|
}
|
|
333
404
|
}
|
|
334
405
|
const topologyContract = validateProducerOutputs(
|
|
335
|
-
producerAssets
|
|
406
|
+
producerAssets.map((asset, sourceIndex) => ({
|
|
407
|
+
guid: asset.guid,
|
|
408
|
+
kind: asset.kind,
|
|
409
|
+
sourceIndex: asset.sourceIndex ?? sourceIndex,
|
|
410
|
+
...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
|
|
411
|
+
})),
|
|
336
412
|
);
|
|
337
413
|
if (!topologyContract.ok) {
|
|
338
414
|
return packErr(
|
|
@@ -376,12 +452,6 @@ export async function scanInventory(
|
|
|
376
452
|
}
|
|
377
453
|
guidToPath.set(normalizedGuid, packPath);
|
|
378
454
|
|
|
379
|
-
// Accumulate refs for cycle detection
|
|
380
|
-
const existingRefs = packRefs.get(normalizedGuid) ?? [];
|
|
381
|
-
for (const ref of asset.refs) {
|
|
382
|
-
existingRefs.push(ref.toLowerCase());
|
|
383
|
-
}
|
|
384
|
-
|
|
385
455
|
// feat-20260608-scene-nesting-ecs-fication M1 / w14 (D-1):
|
|
386
456
|
// mount-payload-extract — for scene assets, redundantly inject the
|
|
387
457
|
// mount.source -> resolved GUID edge into the cycle graph alongside
|
|
@@ -393,65 +463,26 @@ export async function scanInventory(
|
|
|
393
463
|
// refs[]. The `kind: 'mount-asset'` tag on the resulting
|
|
394
464
|
// pack-cyclic-reference detail is set by the cycle producer below
|
|
395
465
|
// (R10).
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
466
|
+
packRefs.set(normalizedGuid, [
|
|
467
|
+
...asset.refs.map((ref) => ref.toLowerCase()),
|
|
468
|
+
...extractMountSourceGuids(asset),
|
|
469
|
+
]);
|
|
400
470
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
|
|
404
|
-
...(asset.sourceIndex === undefined ? {} : { sourceIndex: asset.sourceIndex }),
|
|
405
|
-
kind: asset.kind,
|
|
406
|
-
...(asset.name === undefined ? {} : { name: asset.name }),
|
|
407
|
-
...(asset.execution === undefined ? {} : { execution: asset.execution }),
|
|
408
|
-
refs: asset.refs,
|
|
409
|
-
}));
|
|
410
|
-
declarations.push({
|
|
471
|
+
capture?.declarations.set(packPath, {
|
|
472
|
+
format: 'pack.json',
|
|
411
473
|
sourcePath: packPath,
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
projectedMeta: {
|
|
416
|
-
schemaVersion: '2.0.0',
|
|
417
|
-
...(packObj.packageId === undefined ? {} : { packageId: packObj.packageId }),
|
|
418
|
-
outputs,
|
|
419
|
-
externalGuids: outputs.flatMap((output) => output.refs),
|
|
420
|
-
},
|
|
421
|
-
outputs,
|
|
422
|
-
externalGuids: outputs.flatMap((output) => output.refs),
|
|
423
|
-
diagnostics: [],
|
|
474
|
+
sourceRevision: `sha256:${createHash('sha256').update(raw).digest('hex')}`,
|
|
475
|
+
sourceText: raw,
|
|
476
|
+
value: packObj,
|
|
424
477
|
});
|
|
425
478
|
}
|
|
426
479
|
|
|
427
480
|
// Step 2 + 3 + 5: parse + schema validate + GUID format validate + orphan check for meta files
|
|
428
481
|
const { paths: assetPaths } = loadAssetConfig(process.cwd());
|
|
429
482
|
for (const metaPath of metaPaths) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
parsed = JSON.parse(raw);
|
|
434
|
-
} catch {
|
|
435
|
-
return packErr(
|
|
436
|
-
makePackError('pack-malformed-meta', {
|
|
437
|
-
path: metaPath,
|
|
438
|
-
ajvErrors: [{ instancePath: '', message: 'JSON parse failed' }],
|
|
439
|
-
}),
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const valid = validateMeta(parsed);
|
|
444
|
-
if (!valid) {
|
|
445
|
-
return packErr(
|
|
446
|
-
makePackError('pack-malformed-meta', {
|
|
447
|
-
path: metaPath,
|
|
448
|
-
ajvErrors: (validateMeta.errors ?? []).map((e) => ({
|
|
449
|
-
instancePath: e.instancePath,
|
|
450
|
-
message: e.message ?? 'unknown ajv error',
|
|
451
|
-
})),
|
|
452
|
-
}),
|
|
453
|
-
);
|
|
454
|
-
}
|
|
483
|
+
const loaded = await readValidatedJson(metaPath, 'pack-malformed-meta', validateMeta);
|
|
484
|
+
if (!loaded.ok) return loaded;
|
|
485
|
+
const { raw, parsed } = loaded.value;
|
|
455
486
|
|
|
456
487
|
const metaContract = validateProducerContract(parsed);
|
|
457
488
|
if (!metaContract.ok) {
|
|
@@ -464,23 +495,13 @@ export async function scanInventory(
|
|
|
464
495
|
}
|
|
465
496
|
|
|
466
497
|
// Step 3: validate GUIDs in meta subAssets
|
|
467
|
-
const metaObj = parsed as
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
};
|
|
475
|
-
subAssets: {
|
|
476
|
-
guid: string;
|
|
477
|
-
sourceIndex: number;
|
|
478
|
-
sourceKey?: string;
|
|
479
|
-
kind: string;
|
|
480
|
-
name?: string;
|
|
481
|
-
}[];
|
|
482
|
-
sourceOverrides?: unknown;
|
|
483
|
-
};
|
|
498
|
+
const metaObj = parsed as unknown as MetaInventoryDocument;
|
|
499
|
+
capture?.declarations.set(metaPath, {
|
|
500
|
+
format: 'meta.json',
|
|
501
|
+
sourcePath: metaPath,
|
|
502
|
+
sourceRevision: `sha256:${createHash('sha256').update(raw).digest('hex')}`,
|
|
503
|
+
value: metaObj,
|
|
504
|
+
});
|
|
484
505
|
for (const sub of metaObj.subAssets) {
|
|
485
506
|
if (!isValidAssetGuidString(sub.guid)) {
|
|
486
507
|
return packErr(
|
|
@@ -501,13 +522,10 @@ export async function scanInventory(
|
|
|
501
522
|
);
|
|
502
523
|
}
|
|
503
524
|
guidToPath.set(normalizedGuid, metaPath);
|
|
504
|
-
metaByGuid.set(normalizedGuid, metaPath);
|
|
505
525
|
}
|
|
506
526
|
const producerSubAssets = metaObj.subAssets.length > 1 ? metaObj.subAssets : [];
|
|
507
527
|
if (producerSubAssets.length > 0) {
|
|
508
|
-
const topologyContract = validateProducerOutputs(
|
|
509
|
-
producerSubAssets as unknown as readonly ImportedOutputDeclaration[],
|
|
510
|
-
);
|
|
528
|
+
const topologyContract = validateProducerOutputs(producerSubAssets);
|
|
511
529
|
if (!topologyContract.ok) {
|
|
512
530
|
return packErr(
|
|
513
531
|
makePackError('pack-malformed-meta', {
|
|
@@ -536,122 +554,86 @@ export async function scanInventory(
|
|
|
536
554
|
}),
|
|
537
555
|
);
|
|
538
556
|
}
|
|
539
|
-
const outputs = metaObj.subAssets.map((sub) => ({
|
|
540
|
-
guid: sub.guid,
|
|
541
|
-
...(sub.sourceKey === undefined ? {} : { sourceKey: sub.sourceKey }),
|
|
542
|
-
sourceIndex: sub.sourceIndex,
|
|
543
|
-
kind: sub.kind,
|
|
544
|
-
...(sub.name === undefined ? {} : { name: sub.name }),
|
|
545
|
-
refs: [],
|
|
546
|
-
}));
|
|
547
|
-
const externalGuids = (metaObj.importSettings?.externalAssets ?? []).flatMap((entry) =>
|
|
548
|
-
typeof entry.guid === 'string' ? [entry.guid] : [],
|
|
549
|
-
);
|
|
550
|
-
declarations.push({
|
|
551
|
-
sourcePath: metaPath,
|
|
552
|
-
canonicalSourcePath: metaPath.replace(/\\/g, '/'),
|
|
553
|
-
producer: 'source-meta',
|
|
554
|
-
...(metaObj.packageId === undefined ? {} : { packageId: metaObj.packageId }),
|
|
555
|
-
projectedMeta: {
|
|
556
|
-
...(metaObj.packageId === undefined ? {} : { packageId: metaObj.packageId }),
|
|
557
|
-
...(metaObj.importer === undefined ? {} : { importer: metaObj.importer }),
|
|
558
|
-
...(metaObj.source === undefined ? {} : { source: metaObj.source }),
|
|
559
|
-
...(metaObj.name === undefined ? {} : { name: metaObj.name }),
|
|
560
|
-
outputs,
|
|
561
|
-
externalGuids,
|
|
562
|
-
},
|
|
563
|
-
outputs,
|
|
564
|
-
externalGuids,
|
|
565
|
-
diagnostics: [],
|
|
566
|
-
});
|
|
567
557
|
}
|
|
568
558
|
|
|
569
559
|
// ScriptablePack identity enters the same topology and collision authority as JSON declarations.
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
560
|
+
for (const sourcePath of scriptablePaths) {
|
|
561
|
+
let source: string;
|
|
562
|
+
try {
|
|
563
|
+
source = await readFile(sourcePath, 'utf8');
|
|
564
|
+
} catch {
|
|
565
|
+
return packErr(
|
|
566
|
+
makePackError('pack-malformed-meta', {
|
|
567
|
+
path: sourcePath,
|
|
568
|
+
ajvErrors: [{ instancePath: '', message: 'ScriptablePack source read failed' }],
|
|
569
|
+
}),
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
const loaded = await loadScriptablePack(sourcePath, {
|
|
573
|
+
...(opts.scriptablePack ?? {}),
|
|
574
|
+
...(capture === undefined ? { metadataOnly: true } : {}),
|
|
575
|
+
});
|
|
576
|
+
if (!loaded.ok) {
|
|
577
|
+
const diagnostic =
|
|
578
|
+
'diagnostic' in loaded.error.detail ? loaded.error.detail.diagnostic : undefined;
|
|
579
|
+
return packErr(
|
|
580
|
+
makePackError('pack-malformed-meta', {
|
|
581
|
+
path: sourcePath,
|
|
582
|
+
ajvErrors: [
|
|
583
|
+
{
|
|
584
|
+
instancePath: '',
|
|
585
|
+
message:
|
|
586
|
+
diagnostic === undefined
|
|
587
|
+
? loaded.error.code
|
|
588
|
+
: `${loaded.error.code}: ${diagnostic}`,
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
}),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
let sourceClosure: readonly ScriptablePackSourceClosureEntry[];
|
|
595
|
+
try {
|
|
596
|
+
sourceClosure = await inventoryScriptablePackSource(sourcePath, source);
|
|
597
|
+
} catch {
|
|
598
|
+
return packErr(
|
|
599
|
+
makePackError('pack-malformed-meta', {
|
|
600
|
+
path: sourcePath,
|
|
601
|
+
ajvErrors: [{ instancePath: '', message: 'ScriptablePack source closure read failed' }],
|
|
602
|
+
}),
|
|
605
603
|
);
|
|
606
|
-
|
|
604
|
+
}
|
|
605
|
+
const meta = projectScriptablePackMeta(loaded.value, sourcePath);
|
|
606
|
+
const topology = validateProducerOutputs(meta.subAssets);
|
|
607
|
+
if (!topology.ok) {
|
|
608
|
+
return packErr(
|
|
609
|
+
makePackError('pack-malformed-meta', {
|
|
610
|
+
path: sourcePath,
|
|
611
|
+
ajvErrors: [{ instancePath: '/subAssets', message: topology.error.code }],
|
|
612
|
+
}),
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
const declaration = {
|
|
616
|
+
sourcePath,
|
|
617
|
+
sourceRevision: `sha256:${createHash('sha256').update(source).digest('hex')}`,
|
|
618
|
+
meta,
|
|
619
|
+
definition: loaded.value,
|
|
620
|
+
sourceClosure,
|
|
621
|
+
} satisfies ScriptablePackInventoryDeclaration;
|
|
622
|
+
capture?.declarations.set(sourcePath, {
|
|
623
|
+
format: 'pack.ts',
|
|
624
|
+
...declaration,
|
|
625
|
+
value: meta,
|
|
626
|
+
});
|
|
627
|
+
for (const sub of meta.subAssets) {
|
|
628
|
+
const guid = sub.guid.toLowerCase();
|
|
629
|
+
const existing = guidToPath.get(guid);
|
|
630
|
+
if (existing !== undefined) {
|
|
607
631
|
return packErr(
|
|
608
|
-
makePackError('pack-
|
|
609
|
-
path: sourcePath,
|
|
610
|
-
ajvErrors: [{ instancePath: '/subAssets', message: topology.error.code }],
|
|
611
|
-
}),
|
|
632
|
+
makePackError('pack-guid-collision', { paths: [existing, sourcePath], guid }),
|
|
612
633
|
);
|
|
613
634
|
}
|
|
614
|
-
|
|
615
|
-
const guid = sub.guid.toLowerCase();
|
|
616
|
-
const existing = guidToPath.get(guid);
|
|
617
|
-
if (existing !== undefined) {
|
|
618
|
-
return packErr(
|
|
619
|
-
makePackError('pack-guid-collision', { paths: [existing, sourcePath], guid }),
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
guidToPath.set(guid, sourcePath);
|
|
623
|
-
}
|
|
624
|
-
const outputs = meta.subAssets.map((sub) => ({
|
|
625
|
-
guid: sub.guid,
|
|
626
|
-
sourceKey: sub.sourceKey,
|
|
627
|
-
sourceIndex: sub.sourceIndex,
|
|
628
|
-
kind: sub.kind,
|
|
629
|
-
...(sub.name === undefined ? {} : { name: sub.name }),
|
|
630
|
-
refs: [],
|
|
631
|
-
}));
|
|
632
|
-
const externalGuids = meta.importSettings.externalAssets.map((entry) => entry.guid);
|
|
633
|
-
declarations.push({
|
|
634
|
-
sourcePath,
|
|
635
|
-
canonicalSourcePath: sourcePath.replace(/\\/g, '/'),
|
|
636
|
-
producer: 'scriptable-pack',
|
|
637
|
-
packageId: meta.packageId,
|
|
638
|
-
projectedMeta: {
|
|
639
|
-
schemaVersion: meta.schemaVersion,
|
|
640
|
-
packageId: meta.packageId,
|
|
641
|
-
importer: meta.importer,
|
|
642
|
-
source: meta.source,
|
|
643
|
-
...(meta.name === undefined ? {} : { name: meta.name }),
|
|
644
|
-
outputs,
|
|
645
|
-
externalGuids,
|
|
646
|
-
},
|
|
647
|
-
outputs,
|
|
648
|
-
externalGuids,
|
|
649
|
-
sourceClosure,
|
|
650
|
-
diagnostics: [],
|
|
651
|
-
});
|
|
635
|
+
guidToPath.set(guid, sourcePath);
|
|
652
636
|
}
|
|
653
|
-
} finally {
|
|
654
|
-
await scriptablePackExecutorPool?.dispose();
|
|
655
637
|
}
|
|
656
638
|
|
|
657
639
|
// Step 6: cyclic reference detection via hand-written DFS (no graphlib dep)
|
|
@@ -693,24 +675,41 @@ export async function scanInventory(
|
|
|
693
675
|
}
|
|
694
676
|
}
|
|
695
677
|
|
|
696
|
-
|
|
697
|
-
declarations.map((declaration) => [declaration.sourcePath, declaration]),
|
|
698
|
-
);
|
|
699
|
-
return ok({
|
|
700
|
-
paths: rawPaths,
|
|
701
|
-
scriptablePackMeta,
|
|
702
|
-
declarations,
|
|
703
|
-
declarationsByPath,
|
|
704
|
-
sourceByGuid: new Map(guidToPath),
|
|
705
|
-
metaByGuid,
|
|
706
|
-
});
|
|
678
|
+
return ok(rawPaths);
|
|
707
679
|
}
|
|
708
680
|
|
|
709
|
-
/**
|
|
681
|
+
/** Scan one complete Pack inventory while preserving the public path-only API. */
|
|
710
682
|
export async function scan(
|
|
711
683
|
roots: readonly string[],
|
|
712
684
|
opts: ScanOptions = {},
|
|
713
685
|
): Promise<ScanResult<string[], PackError>> {
|
|
714
|
-
|
|
715
|
-
|
|
686
|
+
return scanValidated(roots, opts);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Return the validated source inventory without interpreting producer output kinds. */
|
|
690
|
+
export async function scanInventory(
|
|
691
|
+
roots: readonly string[],
|
|
692
|
+
opts: ScanOptions = {},
|
|
693
|
+
): Promise<ScanResult<ScanInventory, PackError>> {
|
|
694
|
+
const declarations = new Map<string, ScanSourceDeclaration>();
|
|
695
|
+
const scanned = await scanValidated(roots, opts, { declarations });
|
|
696
|
+
if (!scanned.ok) return scanned;
|
|
697
|
+
const inventory: InventoryDeclaration[] = [];
|
|
698
|
+
for (const sourcePath of scanned.value) {
|
|
699
|
+
const declaration = declarations.get(sourcePath);
|
|
700
|
+
if (declaration?.format !== 'pack.json') continue;
|
|
701
|
+
for (const [index, asset] of declaration.value.assets.entries()) {
|
|
702
|
+
inventory.push({
|
|
703
|
+
guid: asset.guid,
|
|
704
|
+
kind: asset.kind,
|
|
705
|
+
sourcePath,
|
|
706
|
+
sourceRevision: declaration.sourceRevision,
|
|
707
|
+
...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
|
|
708
|
+
...(asset.sourceIndex === undefined
|
|
709
|
+
? { sourceIndex: index }
|
|
710
|
+
: { sourceIndex: asset.sourceIndex }),
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return ok({ paths: scanned.value, inventory, declarations });
|
|
716
715
|
}
|