@forgeax/engine-pack 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +370 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/ai-recovery.unit.test.d.ts +2 -0
- package/dist/__tests__/ai-recovery.unit.test.d.ts.map +1 -0
- package/dist/__tests__/artifact-path.test.d.ts +2 -0
- package/dist/__tests__/artifact-path.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-error-hints.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-error-hints.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-lifecycle.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-lifecycle.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-schema.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-schema.test.d.ts.map +1 -0
- package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts +2 -0
- package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts.map +1 -0
- package/dist/__tests__/atlas-error-code-owner.test-d.d.ts +2 -0
- package/dist/__tests__/atlas-error-code-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/cli-asset-evidence.integration.test.d.ts +2 -0
- package/dist/__tests__/cli-asset-evidence.integration.test.d.ts.map +1 -0
- package/dist/__tests__/cli.unit.test.d.ts +2 -0
- package/dist/__tests__/cli.unit.test.d.ts.map +1 -0
- package/dist/__tests__/evidence-source-inventory.unit.test.d.ts +2 -0
- package/dist/__tests__/evidence-source-inventory.unit.test.d.ts.map +1 -0
- package/dist/__tests__/guid-collision.unit.test.d.ts +2 -0
- package/dist/__tests__/guid-collision.unit.test.d.ts.map +1 -0
- package/dist/__tests__/guid-validator-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/guid-validator-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/load-asset-config.test.d.ts +2 -0
- package/dist/__tests__/load-asset-config.test.d.ts.map +1 -0
- package/dist/__tests__/material-cook-dependencies.unit.test.d.ts +2 -0
- package/dist/__tests__/material-cook-dependencies.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-cook-receipt.unit.test.d.ts +2 -0
- package/dist/__tests__/material-cook-receipt.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-cook-schema.unit.test.d.ts +2 -0
- package/dist/__tests__/material-cook-schema.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meta-schema-optional.test.d.ts +2 -0
- package/dist/__tests__/meta-schema-optional.test.d.ts.map +1 -0
- package/dist/__tests__/meta-source-overrides.unit.test.d.ts +2 -0
- package/dist/__tests__/meta-source-overrides.unit.test.d.ts.map +1 -0
- package/dist/__tests__/native-cooker-registry.test.d.ts +2 -0
- package/dist/__tests__/native-cooker-registry.test.d.ts.map +1 -0
- package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts +2 -0
- package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/offline-evidence.integration.test.d.ts +2 -0
- package/dist/__tests__/offline-evidence.integration.test.d.ts.map +1 -0
- package/dist/__tests__/pack-v2-fixture-migration.test.d.ts +2 -0
- package/dist/__tests__/pack-v2-fixture-migration.test.d.ts.map +1 -0
- package/dist/__tests__/pack-v2-schema.test.d.ts +2 -0
- package/dist/__tests__/pack-v2-schema.test.d.ts.map +1 -0
- package/dist/__tests__/pack.unit.test.d.ts +2 -0
- package/dist/__tests__/pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/producer-contract-schema.unit.test.d.ts +2 -0
- package/dist/__tests__/producer-contract-schema.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-asset-source.test.d.ts +2 -0
- package/dist/__tests__/resolve-asset-source.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-projection.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-projection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-inventory.test.d.ts +2 -0
- package/dist/__tests__/scanner-inventory.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-mount-cycle.test.d.ts +2 -0
- package/dist/__tests__/scanner-mount-cycle.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack.test-d.d.ts +2 -0
- package/dist/__tests__/scriptable-pack.test-d.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/topology-actions.unit.test.d.ts +2 -0
- package/dist/__tests__/topology-actions.unit.test.d.ts.map +1 -0
- package/dist/__tests__/topology.unit.test.d.ts +2 -0
- package/dist/__tests__/topology.unit.test.d.ts.map +1 -0
- package/dist/artifact-path.d.ts +9 -0
- package/dist/artifact-path.d.ts.map +1 -0
- package/dist/atlas/run-atlas.d.ts +8 -0
- package/dist/atlas/run-atlas.d.ts.map +1 -0
- package/dist/atlas/shelf-pack.d.ts +55 -0
- package/dist/atlas/shelf-pack.d.ts.map +1 -0
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.mjs +3 -0
- package/dist/bridge.mjs.map +1 -0
- package/dist/builtin.d.ts +24 -0
- package/dist/builtin.d.ts.map +1 -0
- package/dist/cli-asset.d.ts +22 -0
- package/dist/cli-asset.d.ts.map +1 -0
- package/dist/cli-asset.mjs +9950 -0
- package/dist/cli-asset.mjs.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.mjs +29 -0
- package/dist/config.mjs.map +1 -0
- package/dist/deriveAssetName.d.ts +15 -0
- package/dist/deriveAssetName.d.ts.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.mjs +19 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/evidence/material-cook.d.ts +70 -0
- package/dist/evidence/material-cook.d.ts.map +1 -0
- package/dist/evidence/offline-evidence.d.ts +25 -0
- package/dist/evidence/offline-evidence.d.ts.map +1 -0
- package/dist/evidence/source-inventory.d.ts +29 -0
- package/dist/evidence/source-inventory.d.ts.map +1 -0
- package/dist/guid.d.ts +45 -0
- package/dist/guid.d.ts.map +1 -0
- package/dist/guid.mjs +94 -0
- package/dist/guid.mjs.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +8475 -0
- package/dist/index.mjs.map +1 -0
- package/dist/material/artifact-writer.d.ts +14 -0
- package/dist/material/artifact-writer.d.ts.map +1 -0
- package/dist/mesh-bin-contract.d.ts +36 -0
- package/dist/mesh-bin-contract.d.ts.map +1 -0
- package/dist/name.mjs +22 -0
- package/dist/name.mjs.map +1 -0
- package/dist/native-cooker-registry.d.ts +28 -0
- package/dist/native-cooker-registry.d.ts.map +1 -0
- package/dist/native-cooker.d.ts +3 -0
- package/dist/native-cooker.d.ts.map +1 -0
- package/dist/native-cooker.mjs +105 -0
- package/dist/native-cooker.mjs.map +1 -0
- package/dist/producer-contract.d.ts +19 -0
- package/dist/producer-contract.d.ts.map +1 -0
- package/dist/resolve-asset-source.d.ts +4 -0
- package/dist/resolve-asset-source.d.ts.map +1 -0
- package/dist/resolve-asset-source.mjs +99 -0
- package/dist/resolve-asset-source.mjs.map +1 -0
- package/dist/runtime-projection.d.ts +14 -0
- package/dist/runtime-projection.d.ts.map +1 -0
- package/dist/scanner.d.ts +80 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.mjs +9015 -0
- package/dist/scanner.mjs.map +1 -0
- package/dist/schema-compiled.d.ts +12 -0
- package/dist/schema-compiled.d.ts.map +1 -0
- package/dist/schema.mjs +7369 -0
- package/dist/schema.mjs.map +1 -0
- package/dist/scriptable-pack-node.d.ts +67 -0
- package/dist/scriptable-pack-node.d.ts.map +1 -0
- package/dist/scriptable-pack-node.mjs +1590 -0
- package/dist/scriptable-pack-node.mjs.map +1 -0
- package/dist/scriptable-pack-worker.d.ts +2 -0
- package/dist/scriptable-pack-worker.d.ts.map +1 -0
- package/dist/scriptable-pack-worker.mjs +223 -0
- package/dist/scriptable-pack-worker.mjs.map +1 -0
- package/dist/scriptable-pack.d.ts +525 -0
- package/dist/scriptable-pack.d.ts.map +1 -0
- package/dist/scriptable-pack.mjs +583 -0
- package/dist/scriptable-pack.mjs.map +1 -0
- package/dist/topology.d.ts +10 -0
- package/dist/topology.d.ts.map +1 -0
- package/package.json +129 -0
- package/schema/meta.schema.json +173 -0
- package/schema/pack.schema.json +232 -0
- package/src/__tests__/ai-recovery.unit.test.ts +114 -0
- package/src/__tests__/artifact-path.test.ts +41 -0
- package/src/__tests__/asset-evidence-error-hints.test.ts +46 -0
- package/src/__tests__/asset-evidence-lifecycle.test.ts +104 -0
- package/src/__tests__/asset-evidence-schema.test.ts +86 -0
- package/src/__tests__/atlas-cli-region-mismatch.test.ts +134 -0
- package/src/__tests__/atlas-error-code-owner.test-d.ts +23 -0
- package/src/__tests__/cli-asset-evidence.integration.test.ts +172 -0
- package/src/__tests__/cli.unit.test.ts +448 -0
- package/src/__tests__/evidence-source-inventory.unit.test.ts +37 -0
- package/src/__tests__/guid-collision.unit.test.ts +112 -0
- package/src/__tests__/guid-validator-surface.unit.test.ts +20 -0
- package/src/__tests__/load-asset-config.test.ts +121 -0
- package/src/__tests__/material-cook-dependencies.unit.test.ts +45 -0
- package/src/__tests__/material-cook-receipt.unit.test.ts +91 -0
- package/src/__tests__/material-cook-schema.unit.test.ts +98 -0
- package/src/__tests__/mesh-bin-consumer-surface.unit.test.ts +21 -0
- package/src/__tests__/mesh-bin-contract.unit.test.ts +32 -0
- package/src/__tests__/meta-schema-optional.test.ts +116 -0
- package/src/__tests__/meta-source-overrides.unit.test.ts +71 -0
- package/src/__tests__/native-cooker-registry.test.ts +64 -0
- package/src/__tests__/offline-evidence-artifact-verification-owner.test-d.ts +64 -0
- package/src/__tests__/offline-evidence.integration.test.ts +106 -0
- package/src/__tests__/pack-v2-fixture-migration.test.ts +64 -0
- package/src/__tests__/pack-v2-schema.test.ts +142 -0
- package/src/__tests__/pack.unit.test.ts +2218 -0
- package/src/__tests__/producer-contract-schema.unit.test.ts +92 -0
- package/src/__tests__/resolve-asset-source.test.ts +121 -0
- package/src/__tests__/runtime-projection.unit.test.ts +87 -0
- package/src/__tests__/scanner-inventory.test.ts +72 -0
- package/src/__tests__/scanner-mount-cycle.test.ts +232 -0
- package/src/__tests__/scriptable-pack-cli.integration.test.ts +47 -0
- package/src/__tests__/scriptable-pack-ddc-key.unit.test.ts +63 -0
- package/src/__tests__/scriptable-pack.test-d.ts +131 -0
- package/src/__tests__/scriptable-pack.unit.test.ts +747 -0
- package/src/__tests__/topology-actions.unit.test.ts +94 -0
- package/src/__tests__/topology.unit.test.ts +85 -0
- package/src/artifact-path.ts +69 -0
- package/src/atlas/run-atlas.ts +358 -0
- package/src/atlas/shelf-pack.ts +176 -0
- package/src/atlas/upng-ambient.d.ts +13 -0
- package/src/bridge.ts +2 -0
- package/src/builtin.ts +79 -0
- package/src/cli-asset.ts +590 -0
- package/src/config.ts +36 -0
- package/src/deriveAssetName.ts +42 -0
- package/src/errors.ts +47 -0
- package/src/evidence/material-cook.ts +332 -0
- package/src/evidence/offline-evidence.ts +90 -0
- package/src/evidence/source-inventory.ts +193 -0
- package/src/guid.ts +99 -0
- package/src/index.ts +104 -0
- package/src/material/artifact-writer.ts +26 -0
- package/src/mesh-bin-contract.ts +150 -0
- package/src/native-cooker-registry.ts +163 -0
- package/src/native-cooker.ts +2 -0
- package/src/producer-contract.ts +277 -0
- package/src/resolve-asset-source.ts +93 -0
- package/src/runtime-projection.ts +100 -0
- package/src/scanner.ts +716 -0
- package/src/schema/material-cook.schema.json +79 -0
- package/src/schema-compiled.ts +238 -0
- package/src/scriptable-pack-node.ts +1390 -0
- package/src/scriptable-pack-worker.ts +306 -0
- package/src/scriptable-pack.ts +884 -0
- package/src/topology.ts +156 -0
package/src/scanner.ts
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import type { ImportedOutputDeclaration, PackErrorCode } from '@forgeax/engine-types';
|
|
4
|
+
import { PACK_ERROR_HINTS } from '@forgeax/engine-types';
|
|
5
|
+
import { loadAssetConfig } from './config.js';
|
|
6
|
+
import { PackError } from './errors.js';
|
|
7
|
+
import { isValidAssetGuidString } from './guid.js';
|
|
8
|
+
import { validateProducerContract, validateProducerOutputs } from './producer-contract.js';
|
|
9
|
+
import { resolveAssetSource } from './resolve-asset-source.js';
|
|
10
|
+
import { validateMeta, validatePack } from './schema-compiled.js';
|
|
11
|
+
import { projectScriptablePackMeta, type ScriptablePackMetaJson } from './scriptable-pack.js';
|
|
12
|
+
import {
|
|
13
|
+
createScriptablePackModuleExecutorPool,
|
|
14
|
+
inventoryScriptablePackSource,
|
|
15
|
+
type LoadScriptablePackOptions,
|
|
16
|
+
loadScriptablePack,
|
|
17
|
+
type ScriptablePackSourceClosureEntry,
|
|
18
|
+
} from './scriptable-pack-node.js';
|
|
19
|
+
|
|
20
|
+
// Minimal Result<T, E> — structurally compatible with @forgeax/engine-rhi Result
|
|
21
|
+
// but defined locally to avoid a heavy runtime dep in this build-time package.
|
|
22
|
+
export type ScanResult<T, E> =
|
|
23
|
+
| { readonly ok: true; readonly value: T }
|
|
24
|
+
| { readonly ok: false; readonly error: E };
|
|
25
|
+
|
|
26
|
+
function ok<T>(value: T): ScanResult<T, never> {
|
|
27
|
+
return { ok: true, value };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function packErr<E>(error: E): ScanResult<never, E> {
|
|
31
|
+
return { ok: false, error };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Host-owned source paths that should not enter the Pack catalog. */
|
|
35
|
+
export interface ScanOptions {
|
|
36
|
+
readonly ignorePath?: (path: string) => boolean;
|
|
37
|
+
/** Timeout/executor owner for ScriptablePack metadata loading. */
|
|
38
|
+
readonly scriptablePackLoad?: LoadScriptablePackOptions;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type AssetInventoryProducerKind = 'direct-pack' | 'source-meta' | 'scriptable-pack';
|
|
42
|
+
|
|
43
|
+
export interface AssetInventoryOutput {
|
|
44
|
+
readonly guid: string;
|
|
45
|
+
readonly sourceKey?: string;
|
|
46
|
+
readonly sourceIndex?: number;
|
|
47
|
+
readonly kind: string;
|
|
48
|
+
readonly name?: string;
|
|
49
|
+
readonly execution?: 'direct' | 'cooked';
|
|
50
|
+
readonly refs: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AssetInventoryProjectedMeta {
|
|
54
|
+
readonly schemaVersion?: string;
|
|
55
|
+
readonly packageId?: string;
|
|
56
|
+
readonly importer?: string;
|
|
57
|
+
readonly source?: string;
|
|
58
|
+
readonly name?: string;
|
|
59
|
+
readonly outputs: readonly AssetInventoryOutput[];
|
|
60
|
+
readonly externalGuids: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AssetInventoryDiagnostic {
|
|
64
|
+
readonly code: string;
|
|
65
|
+
readonly message: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AssetInventoryDeclaration {
|
|
69
|
+
readonly sourcePath: string;
|
|
70
|
+
readonly canonicalSourcePath: string;
|
|
71
|
+
readonly producer: AssetInventoryProducerKind;
|
|
72
|
+
readonly packageId?: string;
|
|
73
|
+
readonly projectedMeta: AssetInventoryProjectedMeta;
|
|
74
|
+
readonly outputs: readonly AssetInventoryOutput[];
|
|
75
|
+
readonly externalGuids: readonly string[];
|
|
76
|
+
readonly sourceClosure?: readonly ScriptablePackSourceClosureEntry[];
|
|
77
|
+
readonly diagnostics: readonly AssetInventoryDiagnostic[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface AssetInventory {
|
|
81
|
+
readonly paths: readonly string[];
|
|
82
|
+
readonly scriptablePackMeta: ReadonlyMap<string, ScriptablePackMetaJson>;
|
|
83
|
+
/** One declaration record per authored package/source, keyed by source path. */
|
|
84
|
+
readonly declarations: readonly AssetInventoryDeclaration[];
|
|
85
|
+
readonly declarationsByPath: ReadonlyMap<string, AssetInventoryDeclaration>;
|
|
86
|
+
/** Producer ownership map used by Catalog, watcher, and demand cook. */
|
|
87
|
+
readonly sourceByGuid: ReadonlyMap<string, string>;
|
|
88
|
+
readonly metaByGuid: ReadonlyMap<string, string>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Directory names that are skipped during recursive traversal unless
|
|
93
|
+
* explicitly provided as a root in the `roots` parameter (whitelist override).
|
|
94
|
+
* Requirements §3.4 + §5 blacklist.
|
|
95
|
+
*
|
|
96
|
+
* Re-exported as `SCANNER_BLACKLIST` for cross-package reuse: the
|
|
97
|
+
* `forgeax-engine-remote-asset import --check` traversal (M4 / w21 +
|
|
98
|
+
* plan-strategy section 2.8 path b) walks the same set of source-orphan
|
|
99
|
+
* candidates as the scanner, so we share the single SSOT here.
|
|
100
|
+
*/
|
|
101
|
+
const BLACKLIST = new Set([
|
|
102
|
+
'node_modules',
|
|
103
|
+
'.forgeax-harness',
|
|
104
|
+
'.forgeax',
|
|
105
|
+
'.git',
|
|
106
|
+
'dist',
|
|
107
|
+
'.forgeax-asset-cache',
|
|
108
|
+
'forgeax-engine-assets',
|
|
109
|
+
'coverage',
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
export const SCANNER_BLACKLIST: ReadonlySet<string> = BLACKLIST;
|
|
113
|
+
|
|
114
|
+
function makePackError(
|
|
115
|
+
code: PackErrorCode,
|
|
116
|
+
detail: ConstructorParameters<typeof PackError>[0]['detail'],
|
|
117
|
+
): PackError {
|
|
118
|
+
return new PackError({
|
|
119
|
+
code,
|
|
120
|
+
expected: `pack error: ${code}`,
|
|
121
|
+
hint: PACK_ERROR_HINTS[code],
|
|
122
|
+
detail,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* For scene assets with `payload.mounts[]`, return the lowercased GUID
|
|
128
|
+
* each `mount.source` integer resolves to via `asset.refs[]`. Returns an
|
|
129
|
+
* empty iterable for non-scene assets, scene assets without mounts, or
|
|
130
|
+
* mounts with malformed `source` (out-of-range integer / non-integer) —
|
|
131
|
+
* those are caught by ajv schema validation upstream. The yielded GUIDs
|
|
132
|
+
* feed scanner step-6's mount-asset cycle DFS (D-1, R10).
|
|
133
|
+
*/
|
|
134
|
+
function* extractMountSourceGuids(asset: {
|
|
135
|
+
kind?: unknown;
|
|
136
|
+
payload?: unknown;
|
|
137
|
+
refs: readonly string[];
|
|
138
|
+
}): Generator<string> {
|
|
139
|
+
if (asset.kind !== 'scene') return;
|
|
140
|
+
const payload = asset.payload as { mounts?: unknown } | undefined;
|
|
141
|
+
if (!payload || !Array.isArray(payload.mounts)) return;
|
|
142
|
+
for (const rawMount of payload.mounts) {
|
|
143
|
+
const mount = rawMount as { source?: unknown };
|
|
144
|
+
const idx = mount.source;
|
|
145
|
+
if (typeof idx !== 'number' || !Number.isInteger(idx)) continue;
|
|
146
|
+
if (idx < 0 || idx >= asset.refs.length) continue;
|
|
147
|
+
const resolved = asset.refs[idx];
|
|
148
|
+
if (typeof resolved !== 'string') continue;
|
|
149
|
+
yield resolved.toLowerCase();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Scan one or more root directories for `.meta.json`, `.pack.json`, and `.pack.ts` files.
|
|
155
|
+
* Runs a 7-step fail-fast validation chain (w17 + M7-T01):
|
|
156
|
+
* Step 1 - collect all .meta.json + .pack.json + .pack.ts paths (blacklist skipped)
|
|
157
|
+
* Step 2 - schema validation (ajv strict)
|
|
158
|
+
* Step 3 - GUID string format validation
|
|
159
|
+
* Step 4 - GUID collision detection
|
|
160
|
+
* Step 5 - orphan .meta.json detection
|
|
161
|
+
* Step 6 - cyclic reference detection (hand-written DFS)
|
|
162
|
+
* Step 7 - complete pack and source closure validation
|
|
163
|
+
*
|
|
164
|
+
* Returns one reusable `AssetInventory` or `Err(PackError)` on the first violation.
|
|
165
|
+
*
|
|
166
|
+
* NOTE: source files without a .meta.json are logged but not fatal (requirements §5).
|
|
167
|
+
*/
|
|
168
|
+
export async function scanInventory(
|
|
169
|
+
roots: readonly string[],
|
|
170
|
+
opts: ScanOptions = {},
|
|
171
|
+
): Promise<ScanResult<AssetInventory, PackError>> {
|
|
172
|
+
// Step 1: collect all authored package declarations. ScriptablePack metadata
|
|
173
|
+
// is projected once through the trusted loader; the resulting inventory is
|
|
174
|
+
// shared by CLI, Catalog, watcher, and producer adapters.
|
|
175
|
+
const rawPaths: string[] = [];
|
|
176
|
+
const explicitRootSet = new Set(roots);
|
|
177
|
+
|
|
178
|
+
async function traverse(dir: string): Promise<void> {
|
|
179
|
+
let entries: import('node:fs').Dirent[];
|
|
180
|
+
try {
|
|
181
|
+
entries = (await readdir(dir, { withFileTypes: true })) as import('node:fs').Dirent[];
|
|
182
|
+
} catch {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
const fullPath = join(dir, entry.name);
|
|
188
|
+
|
|
189
|
+
if (opts.ignorePath?.(fullPath) === true) continue;
|
|
190
|
+
|
|
191
|
+
if (entry.isDirectory()) {
|
|
192
|
+
// Skip blacklisted subdirectories unless the subdir is itself an explicit root
|
|
193
|
+
if (BLACKLIST.has(basename(fullPath)) && !explicitRootSet.has(fullPath)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
await traverse(fullPath);
|
|
197
|
+
} else if (entry.isFile()) {
|
|
198
|
+
const name = entry.name;
|
|
199
|
+
if (
|
|
200
|
+
name.endsWith('.meta.json') ||
|
|
201
|
+
name.endsWith('.pack.json') ||
|
|
202
|
+
name.endsWith('.pack.ts')
|
|
203
|
+
) {
|
|
204
|
+
rawPaths.push(fullPath);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const root of roots) {
|
|
211
|
+
// Explicit file roots are useful when a host wants one source package
|
|
212
|
+
// from a larger asset tree without also scanning sibling packages. Keep
|
|
213
|
+
// directory-root behaviour unchanged; this is only an opt-in whitelist.
|
|
214
|
+
try {
|
|
215
|
+
const rootStat = await stat(root);
|
|
216
|
+
if (rootStat.isFile()) {
|
|
217
|
+
if (opts.ignorePath?.(root) === true) continue;
|
|
218
|
+
if (
|
|
219
|
+
root.endsWith('.meta.json') ||
|
|
220
|
+
root.endsWith('.pack.json') ||
|
|
221
|
+
root.endsWith('.pack.ts')
|
|
222
|
+
) {
|
|
223
|
+
rawPaths.push(root);
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// The existing directory traversal treats missing roots as empty.
|
|
229
|
+
}
|
|
230
|
+
await traverse(root);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Separate meta and pack paths
|
|
234
|
+
const metaPaths = rawPaths.filter((p) => p.endsWith('.meta.json'));
|
|
235
|
+
const packPaths = rawPaths.filter((p) => p.endsWith('.pack.json'));
|
|
236
|
+
const scriptablePaths = rawPaths.filter((p) => p.endsWith('.pack.ts'));
|
|
237
|
+
|
|
238
|
+
// Step 2 + 3: parse + schema validate + GUID format validate each pack file
|
|
239
|
+
// One normalized GUID map covers pack assets and meta subAssets. The source
|
|
240
|
+
// kind stays in the path/detail evidence; identity is the normalized GUID.
|
|
241
|
+
const guidToPath = new Map<string, string>();
|
|
242
|
+
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
|
+
|
|
247
|
+
for (const packPath of packPaths) {
|
|
248
|
+
let parsed: unknown;
|
|
249
|
+
try {
|
|
250
|
+
const raw = await readFile(packPath, 'utf-8');
|
|
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
|
+
}
|
|
273
|
+
|
|
274
|
+
const packageContract = validateProducerContract(parsed);
|
|
275
|
+
if (!packageContract.ok) {
|
|
276
|
+
return packErr(
|
|
277
|
+
makePackError('pack-malformed-pack', {
|
|
278
|
+
path: packPath,
|
|
279
|
+
ajvErrors: [{ instancePath: '', message: packageContract.error.code }],
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 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
|
+
};
|
|
299
|
+
for (const asset of packObj.assets) {
|
|
300
|
+
if (
|
|
301
|
+
asset.kind === 'particle-effect' &&
|
|
302
|
+
asset.execution === 'direct' &&
|
|
303
|
+
(asset.refs.length > 0 || Object.keys(asset.artifacts ?? {}).length > 0)
|
|
304
|
+
) {
|
|
305
|
+
return packErr(
|
|
306
|
+
makePackError('pack-malformed-pack', {
|
|
307
|
+
path: packPath,
|
|
308
|
+
ajvErrors: [
|
|
309
|
+
{
|
|
310
|
+
instancePath: '/assets',
|
|
311
|
+
message:
|
|
312
|
+
'authored particle-effect assets are source-only; refs and artifacts must be empty',
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const producerAssets = packObj.assets.filter(
|
|
320
|
+
(asset) => asset.sourceKey !== undefined || asset.sourceIndex !== undefined,
|
|
321
|
+
);
|
|
322
|
+
if (producerAssets.length > 0) {
|
|
323
|
+
for (const asset of producerAssets) {
|
|
324
|
+
const assetContract = validateProducerContract(asset);
|
|
325
|
+
if (!assetContract.ok) {
|
|
326
|
+
return packErr(
|
|
327
|
+
makePackError('pack-malformed-pack', {
|
|
328
|
+
path: packPath,
|
|
329
|
+
ajvErrors: [{ instancePath: '/assets', message: assetContract.error.code }],
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const topologyContract = validateProducerOutputs(
|
|
335
|
+
producerAssets as unknown as readonly ImportedOutputDeclaration[],
|
|
336
|
+
);
|
|
337
|
+
if (!topologyContract.ok) {
|
|
338
|
+
return packErr(
|
|
339
|
+
makePackError('pack-malformed-pack', {
|
|
340
|
+
path: packPath,
|
|
341
|
+
ajvErrors: [{ instancePath: '/assets', message: topologyContract.error.code }],
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const asset of packObj.assets) {
|
|
347
|
+
if (!isValidAssetGuidString(asset.guid)) {
|
|
348
|
+
return packErr(
|
|
349
|
+
makePackError('pack-guid-malformed', {
|
|
350
|
+
raw: asset.guid,
|
|
351
|
+
reason: 'expected 36-char RFC 4122 dash-form UUID',
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
for (const ref of asset.refs) {
|
|
356
|
+
if (!isValidAssetGuidString(ref)) {
|
|
357
|
+
return packErr(
|
|
358
|
+
makePackError('pack-guid-malformed', {
|
|
359
|
+
raw: ref,
|
|
360
|
+
reason: 'expected 36-char RFC 4122 dash-form UUID in refs[]',
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Step 4: collision check
|
|
367
|
+
const normalizedGuid = asset.guid.toLowerCase();
|
|
368
|
+
const existing = guidToPath.get(normalizedGuid);
|
|
369
|
+
if (existing !== undefined) {
|
|
370
|
+
return packErr(
|
|
371
|
+
makePackError('pack-guid-collision', {
|
|
372
|
+
paths: [existing, packPath],
|
|
373
|
+
guid: normalizedGuid,
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
guidToPath.set(normalizedGuid, packPath);
|
|
378
|
+
|
|
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
|
+
// feat-20260608-scene-nesting-ecs-fication M1 / w14 (D-1):
|
|
386
|
+
// mount-payload-extract — for scene assets, redundantly inject the
|
|
387
|
+
// mount.source -> resolved GUID edge into the cycle graph alongside
|
|
388
|
+
// asset.refs[]. By the .pack.json convention mount.source is an
|
|
389
|
+
// integer index into the same asset.refs[], so the resolved GUID is
|
|
390
|
+
// already present in `existingRefs`; this defensive pass guarantees
|
|
391
|
+
// that any author-supplied mounts[] references participate in the
|
|
392
|
+
// cycle DFS even if the schema-emitter forgot to mirror them into
|
|
393
|
+
// refs[]. The `kind: 'mount-asset'` tag on the resulting
|
|
394
|
+
// pack-cyclic-reference detail is set by the cycle producer below
|
|
395
|
+
// (R10).
|
|
396
|
+
for (const guid of extractMountSourceGuids(asset)) {
|
|
397
|
+
existingRefs.push(guid);
|
|
398
|
+
}
|
|
399
|
+
packRefs.set(normalizedGuid, existingRefs);
|
|
400
|
+
}
|
|
401
|
+
const outputs = packObj.assets.map((asset) => ({
|
|
402
|
+
guid: asset.guid,
|
|
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({
|
|
411
|
+
sourcePath: packPath,
|
|
412
|
+
canonicalSourcePath: packPath.replace(/\\/g, '/'),
|
|
413
|
+
producer: 'direct-pack',
|
|
414
|
+
...(packObj.packageId === undefined ? {} : { packageId: packObj.packageId }),
|
|
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: [],
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Step 2 + 3 + 5: parse + schema validate + GUID format validate + orphan check for meta files
|
|
428
|
+
const { paths: assetPaths } = loadAssetConfig(process.cwd());
|
|
429
|
+
for (const metaPath of metaPaths) {
|
|
430
|
+
let parsed: unknown;
|
|
431
|
+
try {
|
|
432
|
+
const raw = await readFile(metaPath, 'utf-8');
|
|
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
|
+
}
|
|
455
|
+
|
|
456
|
+
const metaContract = validateProducerContract(parsed);
|
|
457
|
+
if (!metaContract.ok) {
|
|
458
|
+
return packErr(
|
|
459
|
+
makePackError('pack-malformed-meta', {
|
|
460
|
+
path: metaPath,
|
|
461
|
+
ajvErrors: [{ instancePath: '', message: metaContract.error.code }],
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Step 3: validate GUIDs in meta subAssets
|
|
467
|
+
const metaObj = parsed as {
|
|
468
|
+
packageId?: string;
|
|
469
|
+
importer?: string;
|
|
470
|
+
source?: string;
|
|
471
|
+
name?: string;
|
|
472
|
+
importSettings?: {
|
|
473
|
+
readonly externalAssets?: readonly { readonly guid?: unknown }[];
|
|
474
|
+
};
|
|
475
|
+
subAssets: {
|
|
476
|
+
guid: string;
|
|
477
|
+
sourceIndex: number;
|
|
478
|
+
sourceKey?: string;
|
|
479
|
+
kind: string;
|
|
480
|
+
name?: string;
|
|
481
|
+
}[];
|
|
482
|
+
sourceOverrides?: unknown;
|
|
483
|
+
};
|
|
484
|
+
for (const sub of metaObj.subAssets) {
|
|
485
|
+
if (!isValidAssetGuidString(sub.guid)) {
|
|
486
|
+
return packErr(
|
|
487
|
+
makePackError('pack-guid-malformed', {
|
|
488
|
+
raw: sub.guid,
|
|
489
|
+
reason: 'expected 36-char RFC 4122 dash-form UUID in subAssets[].guid',
|
|
490
|
+
}),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
const normalizedGuid = sub.guid.toLowerCase();
|
|
494
|
+
const existing = guidToPath.get(normalizedGuid);
|
|
495
|
+
if (existing !== undefined) {
|
|
496
|
+
return packErr(
|
|
497
|
+
makePackError('pack-guid-collision', {
|
|
498
|
+
paths: [existing, metaPath],
|
|
499
|
+
guid: normalizedGuid,
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
guidToPath.set(normalizedGuid, metaPath);
|
|
504
|
+
metaByGuid.set(normalizedGuid, metaPath);
|
|
505
|
+
}
|
|
506
|
+
const producerSubAssets = metaObj.subAssets.length > 1 ? metaObj.subAssets : [];
|
|
507
|
+
if (producerSubAssets.length > 0) {
|
|
508
|
+
const topologyContract = validateProducerOutputs(
|
|
509
|
+
producerSubAssets as unknown as readonly ImportedOutputDeclaration[],
|
|
510
|
+
);
|
|
511
|
+
if (!topologyContract.ok) {
|
|
512
|
+
return packErr(
|
|
513
|
+
makePackError('pack-malformed-meta', {
|
|
514
|
+
path: metaPath,
|
|
515
|
+
ajvErrors: [{ instancePath: '/subAssets', message: topologyContract.error.code }],
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Step 5: orphan .meta.json check — the source file declared in meta.source must exist.
|
|
522
|
+
// Uses resolveAssetSource for three-mode dispatch (undefined derivation / @name/ path table /
|
|
523
|
+
// relative path), replacing the former hardcoded join(metaDir, metaObj.source).
|
|
524
|
+
const sourceResolved = resolveAssetSource(metaPath, metaObj.source, assetPaths);
|
|
525
|
+
if (!sourceResolved.ok) {
|
|
526
|
+
return packErr(sourceResolved.error);
|
|
527
|
+
}
|
|
528
|
+
const expectedSourcePath = sourceResolved.value;
|
|
529
|
+
try {
|
|
530
|
+
await stat(expectedSourcePath);
|
|
531
|
+
} catch {
|
|
532
|
+
return packErr(
|
|
533
|
+
makePackError('pack-orphan-meta', {
|
|
534
|
+
metaPath,
|
|
535
|
+
expectedFile: expectedSourcePath,
|
|
536
|
+
}),
|
|
537
|
+
);
|
|
538
|
+
}
|
|
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
|
+
}
|
|
568
|
+
|
|
569
|
+
// ScriptablePack identity enters the same topology and collision authority as JSON declarations.
|
|
570
|
+
const scriptablePackExecutorPool =
|
|
571
|
+
scriptablePaths.length === 0 || opts.scriptablePackLoad?.executor !== undefined
|
|
572
|
+
? undefined
|
|
573
|
+
: createScriptablePackModuleExecutorPool();
|
|
574
|
+
try {
|
|
575
|
+
for (const sourcePath of scriptablePaths) {
|
|
576
|
+
const executor = await scriptablePackExecutorPool?.acquire();
|
|
577
|
+
const loaded = await loadScriptablePack(sourcePath, {
|
|
578
|
+
...opts.scriptablePackLoad,
|
|
579
|
+
metadataOnly: true,
|
|
580
|
+
...(executor === undefined ? {} : { executor }),
|
|
581
|
+
});
|
|
582
|
+
if (!loaded.ok) {
|
|
583
|
+
const diagnostic =
|
|
584
|
+
'diagnostic' in loaded.error.detail ? loaded.error.detail.diagnostic : undefined;
|
|
585
|
+
return packErr(
|
|
586
|
+
makePackError('pack-malformed-meta', {
|
|
587
|
+
path: sourcePath,
|
|
588
|
+
ajvErrors: [
|
|
589
|
+
{
|
|
590
|
+
instancePath: '',
|
|
591
|
+
message:
|
|
592
|
+
diagnostic === undefined
|
|
593
|
+
? loaded.error.code
|
|
594
|
+
: `${loaded.error.code}: ${diagnostic}`,
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
const meta = projectScriptablePackMeta(loaded.value, sourcePath);
|
|
601
|
+
scriptablePackMeta.set(sourcePath, meta);
|
|
602
|
+
const sourceClosure = await inventoryScriptablePackSource(sourcePath);
|
|
603
|
+
const topology = validateProducerOutputs(
|
|
604
|
+
meta.subAssets as unknown as readonly ImportedOutputDeclaration[],
|
|
605
|
+
);
|
|
606
|
+
if (!topology.ok) {
|
|
607
|
+
return packErr(
|
|
608
|
+
makePackError('pack-malformed-meta', {
|
|
609
|
+
path: sourcePath,
|
|
610
|
+
ajvErrors: [{ instancePath: '/subAssets', message: topology.error.code }],
|
|
611
|
+
}),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
for (const sub of meta.subAssets) {
|
|
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
|
+
});
|
|
652
|
+
}
|
|
653
|
+
} finally {
|
|
654
|
+
await scriptablePackExecutorPool?.dispose();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Step 6: cyclic reference detection via hand-written DFS (no graphlib dep)
|
|
658
|
+
// visited: nodes fully processed; recStack: nodes in current DFS path
|
|
659
|
+
const visited = new Set<string>();
|
|
660
|
+
const recStack = new Set<string>();
|
|
661
|
+
|
|
662
|
+
function dfs(guid: string, path: string[]): string[] | null {
|
|
663
|
+
visited.add(guid);
|
|
664
|
+
recStack.add(guid);
|
|
665
|
+
|
|
666
|
+
for (const ref of packRefs.get(guid) ?? []) {
|
|
667
|
+
if (!visited.has(ref)) {
|
|
668
|
+
const cycle = dfs(ref, [...path, ref]);
|
|
669
|
+
if (cycle !== null) return cycle;
|
|
670
|
+
} else if (recStack.has(ref)) {
|
|
671
|
+
// Found a back-edge: reconstruct cycle from the repeated node
|
|
672
|
+
const cycleStart = path.indexOf(ref);
|
|
673
|
+
return cycleStart >= 0 ? [...path.slice(cycleStart), ref] : [...path, ref];
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
recStack.delete(guid);
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
for (const guid of guidToPath.keys()) {
|
|
682
|
+
if (!visited.has(guid)) {
|
|
683
|
+
const cycle = dfs(guid, [guid]);
|
|
684
|
+
if (cycle !== null) {
|
|
685
|
+
return packErr(
|
|
686
|
+
makePackError('pack-cyclic-reference', {
|
|
687
|
+
code: 'pack-cyclic-reference',
|
|
688
|
+
kind: 'mount-asset',
|
|
689
|
+
cycle,
|
|
690
|
+
}),
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const declarationsByPath = new Map(
|
|
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
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Compatibility projection for callers that only need discovered paths. */
|
|
710
|
+
export async function scan(
|
|
711
|
+
roots: readonly string[],
|
|
712
|
+
opts: ScanOptions = {},
|
|
713
|
+
): Promise<ScanResult<string[], PackError>> {
|
|
714
|
+
const inventory = await scanInventory(roots, opts);
|
|
715
|
+
return inventory.ok ? ok([...inventory.value.paths]) : inventory;
|
|
716
|
+
}
|