@forgeax/engine-pack 0.0.0-dev.8d955ade1c79
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 +429 -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__/owner-chain.integration.test.d.ts +2 -0
- package/dist/__tests__/owner-chain.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__/package-finalizer.contract.test.d.ts +2 -0
- package/dist/__tests__/package-finalizer.contract.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__/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/__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/artifact-path.mjs +53 -0
- package/dist/artifact-path.mjs.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/build.d.ts +19 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.mjs +10203 -0
- package/dist/build.mjs.map +1 -0
- package/dist/builtin.d.ts +44 -0
- package/dist/builtin.d.ts.map +1 -0
- 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.d.ts +22 -0
- package/dist/cli-asset.d.ts.map +1 -0
- package/dist/cli-asset.mjs +9697 -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 +92 -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 +8612 -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/material-cook.d.ts +2 -0
- package/dist/material-cook.d.ts.map +1 -0
- package/dist/material-cook.mjs +248 -0
- package/dist/material-cook.mjs.map +1 -0
- package/dist/mesh-bin-contract.d.ts +36 -0
- package/dist/mesh-bin-contract.d.ts.map +1 -0
- package/dist/mesh-bin-contract.mjs +96 -0
- package/dist/mesh-bin-contract.mjs.map +1 -0
- package/dist/name.mjs +22 -0
- package/dist/name.mjs.map +1 -0
- package/dist/native-cooker-registry.d.ts +47 -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 +142 -0
- package/dist/native-cooker.mjs.map +1 -0
- package/dist/package-finalizer.d.ts +121 -0
- package/dist/package-finalizer.d.ts.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 +109 -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/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 +113 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.mjs +8819 -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 +62 -0
- package/dist/scriptable-pack-node.d.ts.map +1 -0
- package/dist/scriptable-pack-node.mjs +1674 -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 +224 -0
- package/dist/scriptable-pack-worker.mjs.map +1 -0
- package/dist/scriptable-pack.d.ts +558 -0
- package/dist/scriptable-pack.d.ts.map +1 -0
- package/dist/scriptable-pack.mjs +672 -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 +154 -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 +112 -0
- package/src/__tests__/material-cook-receipt.unit.test.ts +148 -0
- package/src/__tests__/material-cook-schema.unit.test.ts +152 -0
- package/src/__tests__/mesh-bin-consumer-surface.unit.test.ts +22 -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__/owner-chain.integration.test.ts +78 -0
- package/src/__tests__/pack-v2-fixture-migration.test.ts +87 -0
- package/src/__tests__/pack-v2-schema.test.ts +142 -0
- package/src/__tests__/pack.unit.test.ts +2218 -0
- package/src/__tests__/package-finalizer.contract.test.ts +105 -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__/runtime.browser.test.ts +15 -0
- package/src/__tests__/scanner-inventory.contract.test.ts +167 -0
- package/src/__tests__/scanner-inventory.test.ts +76 -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 +789 -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 +172 -0
- package/src/atlas/upng-ambient.d.ts +13 -0
- package/src/bridge.ts +2 -0
- package/src/build.ts +139 -0
- package/src/builtin.ts +117 -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 +586 -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 +411 -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 +108 -0
- package/src/material/artifact-writer.ts +26 -0
- package/src/material-cook.ts +15 -0
- package/src/mesh-bin-contract.ts +150 -0
- package/src/native-cooker-registry.ts +235 -0
- package/src/native-cooker.ts +2 -0
- package/src/package-finalizer.ts +394 -0
- package/src/producer-contract.ts +277 -0
- package/src/resolve-asset-source.ts +88 -0
- package/src/runtime-projection.ts +100 -0
- package/src/runtime-publication.ts +190 -0
- package/src/runtime.ts +23 -0
- package/src/scanner.ts +715 -0
- package/src/schema/material-cook.schema.json +119 -0
- package/src/schema-compiled.ts +238 -0
- package/src/scriptable-pack-node.ts +1394 -0
- package/src/scriptable-pack-worker.ts +308 -0
- package/src/scriptable-pack.ts +1006 -0
- package/src/topology.ts +157 -0
package/src/scanner.ts
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import type {
|
|
5
|
+
CatalogDiagnostic,
|
|
6
|
+
PackErrorCode,
|
|
7
|
+
ProviderProvenance,
|
|
8
|
+
ResourceRevision,
|
|
9
|
+
SourceOverrideDescriptor,
|
|
10
|
+
} from '@forgeax/engine-types';
|
|
11
|
+
import { PACK_ERROR_HINTS } from '@forgeax/engine-types';
|
|
12
|
+
import { loadAssetConfig } from './config.js';
|
|
13
|
+
import { PackError } from './errors.js';
|
|
14
|
+
import { isValidAssetGuidString } from './guid.js';
|
|
15
|
+
import { validateProducerContract, validateProducerOutputs } from './producer-contract.js';
|
|
16
|
+
import { resolveAssetSource } from './resolve-asset-source.js';
|
|
17
|
+
import { validateMeta, validatePack } from './schema-compiled.js';
|
|
18
|
+
import {
|
|
19
|
+
projectScriptablePackMeta,
|
|
20
|
+
type ScriptablePackDefinition,
|
|
21
|
+
type ScriptablePackMetaJson,
|
|
22
|
+
type ScriptablePackSourceClosureEntry,
|
|
23
|
+
} from './scriptable-pack.js';
|
|
24
|
+
import {
|
|
25
|
+
inventoryScriptablePackSource,
|
|
26
|
+
loadScriptablePack,
|
|
27
|
+
type ScriptablePackModuleExecutor,
|
|
28
|
+
} from './scriptable-pack-node.js';
|
|
29
|
+
|
|
30
|
+
// Minimal Result<T, E> — structurally compatible with @forgeax/engine-rhi Result
|
|
31
|
+
// but defined locally to avoid a heavy runtime dep in this build-time package.
|
|
32
|
+
export type ScanResult<T, E> =
|
|
33
|
+
| { readonly ok: true; readonly value: T }
|
|
34
|
+
| { readonly ok: false; readonly error: E };
|
|
35
|
+
|
|
36
|
+
function ok<T>(value: T): ScanResult<T, never> {
|
|
37
|
+
return { ok: true, value };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function packErr<E>(error: E): ScanResult<never, E> {
|
|
41
|
+
return { ok: false, error };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Host-owned source paths that should not enter the Pack catalog. */
|
|
45
|
+
export interface ScanOptions {
|
|
46
|
+
readonly ignorePath?: (path: string) => boolean;
|
|
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;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Stable default policy shared by inventory and production owners. */
|
|
60
|
+
export const STANDARD_SCRIPTABLE_PACK_SCAN_OPTIONS = Object.freeze(
|
|
61
|
+
{},
|
|
62
|
+
) satisfies ScriptablePackScanOptions;
|
|
63
|
+
|
|
64
|
+
export interface InventoryDeclaration {
|
|
65
|
+
readonly guid: string;
|
|
66
|
+
readonly kind: string;
|
|
67
|
+
readonly sourcePath: string;
|
|
68
|
+
readonly sourceRevision: string;
|
|
69
|
+
readonly sourceKey?: string;
|
|
70
|
+
readonly sourceIndex?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
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';
|
|
91
|
+
readonly packageId?: string;
|
|
92
|
+
readonly name?: string;
|
|
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[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface MetaInventorySubAsset {
|
|
106
|
+
readonly guid: string;
|
|
107
|
+
readonly sourceIndex: number;
|
|
108
|
+
readonly sourceKey?: string;
|
|
109
|
+
readonly name?: string;
|
|
110
|
+
readonly kind: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface PackInventoryDocument {
|
|
114
|
+
readonly schemaVersion: '1.0.0' | '2.0.0';
|
|
115
|
+
readonly kind: 'internal-text-package';
|
|
116
|
+
readonly packageId?: string;
|
|
117
|
+
readonly provenance?: ProviderProvenance;
|
|
118
|
+
readonly revision?: ResourceRevision;
|
|
119
|
+
readonly diagnostics?: readonly CatalogDiagnostic[];
|
|
120
|
+
readonly assets: readonly PackInventoryAsset[];
|
|
121
|
+
}
|
|
122
|
+
|
|
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>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Directory names that are skipped during recursive traversal unless
|
|
162
|
+
* explicitly provided as a root in the `roots` parameter (whitelist override).
|
|
163
|
+
* Requirements §3.4 + §5 blacklist.
|
|
164
|
+
*
|
|
165
|
+
* Re-exported as `SCANNER_BLACKLIST` for cross-package reuse: the
|
|
166
|
+
* `forgeax-engine-remote-asset import --check` traversal (M4 / w21 +
|
|
167
|
+
* plan-strategy section 2.8 path b) walks the same set of source-orphan
|
|
168
|
+
* candidates as the scanner, so we share the single SSOT here.
|
|
169
|
+
*/
|
|
170
|
+
const BLACKLIST = new Set([
|
|
171
|
+
'node_modules',
|
|
172
|
+
'.forgeax-harness',
|
|
173
|
+
'.forgeax',
|
|
174
|
+
'.git',
|
|
175
|
+
'dist',
|
|
176
|
+
'.forgeax-asset-cache',
|
|
177
|
+
'forgeax-engine-assets',
|
|
178
|
+
'coverage',
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
export const SCANNER_BLACKLIST: ReadonlySet<string> = BLACKLIST;
|
|
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
|
+
|
|
222
|
+
function makePackError(
|
|
223
|
+
code: PackErrorCode,
|
|
224
|
+
detail: ConstructorParameters<typeof PackError>[0]['detail'],
|
|
225
|
+
): PackError {
|
|
226
|
+
return new PackError({
|
|
227
|
+
code,
|
|
228
|
+
expected: `pack error: ${code}`,
|
|
229
|
+
hint: PACK_ERROR_HINTS[code],
|
|
230
|
+
detail,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* For scene assets with `payload.mounts[]`, return the lowercased GUID
|
|
236
|
+
* each `mount.source` integer resolves to via `asset.refs[]`. Returns an
|
|
237
|
+
* empty iterable for non-scene assets, scene assets without mounts, or
|
|
238
|
+
* mounts with malformed `source` (out-of-range integer / non-integer) —
|
|
239
|
+
* those are caught by ajv schema validation upstream. The yielded GUIDs
|
|
240
|
+
* feed scanner step-6's mount-asset cycle DFS (D-1, R10).
|
|
241
|
+
*/
|
|
242
|
+
function* extractMountSourceGuids(asset: {
|
|
243
|
+
kind?: unknown;
|
|
244
|
+
payload?: unknown;
|
|
245
|
+
refs: readonly string[];
|
|
246
|
+
}): Generator<string> {
|
|
247
|
+
if (asset.kind !== 'scene') return;
|
|
248
|
+
const payload = asset.payload as { mounts?: unknown } | undefined;
|
|
249
|
+
if (!payload || !Array.isArray(payload.mounts)) return;
|
|
250
|
+
for (const rawMount of payload.mounts) {
|
|
251
|
+
const mount = rawMount as { source?: unknown };
|
|
252
|
+
const idx = mount.source;
|
|
253
|
+
if (typeof idx !== 'number' || !Number.isInteger(idx)) continue;
|
|
254
|
+
if (idx < 0 || idx >= asset.refs.length) continue;
|
|
255
|
+
const resolved = asset.refs[idx];
|
|
256
|
+
if (typeof resolved !== 'string') continue;
|
|
257
|
+
yield resolved.toLowerCase();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Scan one or more root directories for `.meta.json`, `.pack.json`, and `.pack.ts` files.
|
|
263
|
+
* Runs a 7-step fail-fast validation chain (w17 + M7-T01):
|
|
264
|
+
* Step 1 - collect all .meta.json + .pack.json paths (blacklist skipped)
|
|
265
|
+
* Step 2 - schema validation (ajv strict)
|
|
266
|
+
* Step 3 - GUID string format validation
|
|
267
|
+
* Step 4 - GUID collision detection
|
|
268
|
+
* Step 5 - orphan .meta.json detection
|
|
269
|
+
* Step 6 - cyclic reference detection (hand-written DFS)
|
|
270
|
+
* Step 7 - complete pack and source closure validation
|
|
271
|
+
*
|
|
272
|
+
* Returns `Ok(paths)` or `Err(PackError)` on the first violation.
|
|
273
|
+
*
|
|
274
|
+
* NOTE: source files without a .meta.json are logged but not fatal (requirements §5).
|
|
275
|
+
*/
|
|
276
|
+
async function scanValidated(
|
|
277
|
+
roots: readonly string[],
|
|
278
|
+
opts: ScanOptions = {},
|
|
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.
|
|
284
|
+
const rawPaths: string[] = [];
|
|
285
|
+
const explicitRootSet = new Set(roots);
|
|
286
|
+
|
|
287
|
+
async function traverse(dir: string): Promise<void> {
|
|
288
|
+
let entries: import('node:fs').Dirent[];
|
|
289
|
+
try {
|
|
290
|
+
entries = (await readdir(dir, { withFileTypes: true })) as import('node:fs').Dirent[];
|
|
291
|
+
} catch {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const entry of entries) {
|
|
296
|
+
const fullPath = join(dir, entry.name);
|
|
297
|
+
|
|
298
|
+
if (opts.ignorePath?.(fullPath) === true) continue;
|
|
299
|
+
|
|
300
|
+
if (entry.isDirectory()) {
|
|
301
|
+
// Skip blacklisted subdirectories unless the subdir is itself an explicit root
|
|
302
|
+
if (BLACKLIST.has(basename(fullPath)) && !explicitRootSet.has(fullPath)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
await traverse(fullPath);
|
|
306
|
+
} else if (entry.isFile()) {
|
|
307
|
+
const name = entry.name;
|
|
308
|
+
if (
|
|
309
|
+
name.endsWith('.meta.json') ||
|
|
310
|
+
name.endsWith('.pack.json') ||
|
|
311
|
+
name.endsWith('.pack.ts')
|
|
312
|
+
) {
|
|
313
|
+
rawPaths.push(fullPath);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const root of roots) {
|
|
320
|
+
// Explicit file roots are useful when a host wants one source package
|
|
321
|
+
// from a larger asset tree without also scanning sibling packages. Keep
|
|
322
|
+
// directory-root behaviour unchanged; this is only an opt-in whitelist.
|
|
323
|
+
try {
|
|
324
|
+
const rootStat = await stat(root);
|
|
325
|
+
if (rootStat.isFile()) {
|
|
326
|
+
if (opts.ignorePath?.(root) === true) continue;
|
|
327
|
+
if (
|
|
328
|
+
root.endsWith('.meta.json') ||
|
|
329
|
+
root.endsWith('.pack.json') ||
|
|
330
|
+
root.endsWith('.pack.ts')
|
|
331
|
+
) {
|
|
332
|
+
rawPaths.push(root);
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
// The existing directory traversal treats missing roots as empty.
|
|
338
|
+
}
|
|
339
|
+
await traverse(root);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Separate meta and pack paths
|
|
343
|
+
const metaPaths = rawPaths.filter((p) => p.endsWith('.meta.json'));
|
|
344
|
+
const packPaths = rawPaths.filter((p) => p.endsWith('.pack.json'));
|
|
345
|
+
const scriptablePaths = rawPaths.filter((p) => p.endsWith('.pack.ts'));
|
|
346
|
+
|
|
347
|
+
// Step 2 + 3: parse + schema validate + GUID format validate each pack file
|
|
348
|
+
// One normalized GUID map covers pack assets and meta subAssets. The source
|
|
349
|
+
// kind stays in the path/detail evidence; identity is the normalized GUID.
|
|
350
|
+
const guidToPath = new Map<string, string>();
|
|
351
|
+
const packRefs = new Map<string, string[]>(); // guid -> refs[]
|
|
352
|
+
|
|
353
|
+
for (const packPath of packPaths) {
|
|
354
|
+
const loaded = await readValidatedJson(packPath, 'pack-malformed-pack', validatePack);
|
|
355
|
+
if (!loaded.ok) return loaded;
|
|
356
|
+
const { raw, parsed } = loaded.value;
|
|
357
|
+
|
|
358
|
+
const packageContract = validateProducerContract(parsed);
|
|
359
|
+
if (!packageContract.ok) {
|
|
360
|
+
return packErr(
|
|
361
|
+
makePackError('pack-malformed-pack', {
|
|
362
|
+
path: packPath,
|
|
363
|
+
ajvErrors: [{ instancePath: '', message: packageContract.error.code }],
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Step 3: validate GUIDs in pack
|
|
369
|
+
const packObj = parsed as unknown as PackInventoryDocument;
|
|
370
|
+
for (const asset of packObj.assets) {
|
|
371
|
+
if (
|
|
372
|
+
asset.kind === 'particle-effect' &&
|
|
373
|
+
asset.execution === 'direct' &&
|
|
374
|
+
(asset.refs.length > 0 || Object.keys(asset.artifacts ?? {}).length > 0)
|
|
375
|
+
) {
|
|
376
|
+
return packErr(
|
|
377
|
+
makePackError('pack-malformed-pack', {
|
|
378
|
+
path: packPath,
|
|
379
|
+
ajvErrors: [
|
|
380
|
+
{
|
|
381
|
+
instancePath: '/assets',
|
|
382
|
+
message:
|
|
383
|
+
'authored particle-effect assets are source-only; refs and artifacts must be empty',
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const producerAssets = packObj.assets.filter(
|
|
391
|
+
(asset) => asset.sourceKey !== undefined || asset.sourceIndex !== undefined,
|
|
392
|
+
);
|
|
393
|
+
if (producerAssets.length > 0) {
|
|
394
|
+
for (const asset of producerAssets) {
|
|
395
|
+
const assetContract = validateProducerContract(asset);
|
|
396
|
+
if (!assetContract.ok) {
|
|
397
|
+
return packErr(
|
|
398
|
+
makePackError('pack-malformed-pack', {
|
|
399
|
+
path: packPath,
|
|
400
|
+
ajvErrors: [{ instancePath: '/assets', message: assetContract.error.code }],
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const topologyContract = validateProducerOutputs(
|
|
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
|
+
})),
|
|
412
|
+
);
|
|
413
|
+
if (!topologyContract.ok) {
|
|
414
|
+
return packErr(
|
|
415
|
+
makePackError('pack-malformed-pack', {
|
|
416
|
+
path: packPath,
|
|
417
|
+
ajvErrors: [{ instancePath: '/assets', message: topologyContract.error.code }],
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const asset of packObj.assets) {
|
|
423
|
+
if (!isValidAssetGuidString(asset.guid)) {
|
|
424
|
+
return packErr(
|
|
425
|
+
makePackError('pack-guid-malformed', {
|
|
426
|
+
raw: asset.guid,
|
|
427
|
+
reason: 'expected 36-char RFC 4122 dash-form UUID',
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
for (const ref of asset.refs) {
|
|
432
|
+
if (!isValidAssetGuidString(ref)) {
|
|
433
|
+
return packErr(
|
|
434
|
+
makePackError('pack-guid-malformed', {
|
|
435
|
+
raw: ref,
|
|
436
|
+
reason: 'expected 36-char RFC 4122 dash-form UUID in refs[]',
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Step 4: collision check
|
|
443
|
+
const normalizedGuid = asset.guid.toLowerCase();
|
|
444
|
+
const existing = guidToPath.get(normalizedGuid);
|
|
445
|
+
if (existing !== undefined) {
|
|
446
|
+
return packErr(
|
|
447
|
+
makePackError('pack-guid-collision', {
|
|
448
|
+
paths: [existing, packPath],
|
|
449
|
+
guid: normalizedGuid,
|
|
450
|
+
}),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
guidToPath.set(normalizedGuid, packPath);
|
|
454
|
+
|
|
455
|
+
// feat-20260608-scene-nesting-ecs-fication M1 / w14 (D-1):
|
|
456
|
+
// mount-payload-extract — for scene assets, redundantly inject the
|
|
457
|
+
// mount.source -> resolved GUID edge into the cycle graph alongside
|
|
458
|
+
// asset.refs[]. By the .pack.json convention mount.source is an
|
|
459
|
+
// integer index into the same asset.refs[], so the resolved GUID is
|
|
460
|
+
// already present in `existingRefs`; this defensive pass guarantees
|
|
461
|
+
// that any author-supplied mounts[] references participate in the
|
|
462
|
+
// cycle DFS even if the schema-emitter forgot to mirror them into
|
|
463
|
+
// refs[]. The `kind: 'mount-asset'` tag on the resulting
|
|
464
|
+
// pack-cyclic-reference detail is set by the cycle producer below
|
|
465
|
+
// (R10).
|
|
466
|
+
packRefs.set(normalizedGuid, [
|
|
467
|
+
...asset.refs.map((ref) => ref.toLowerCase()),
|
|
468
|
+
...extractMountSourceGuids(asset),
|
|
469
|
+
]);
|
|
470
|
+
}
|
|
471
|
+
capture?.declarations.set(packPath, {
|
|
472
|
+
format: 'pack.json',
|
|
473
|
+
sourcePath: packPath,
|
|
474
|
+
sourceRevision: `sha256:${createHash('sha256').update(raw).digest('hex')}`,
|
|
475
|
+
sourceText: raw,
|
|
476
|
+
value: packObj,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Step 2 + 3 + 5: parse + schema validate + GUID format validate + orphan check for meta files
|
|
481
|
+
const { paths: assetPaths } = loadAssetConfig(process.cwd());
|
|
482
|
+
for (const metaPath of metaPaths) {
|
|
483
|
+
const loaded = await readValidatedJson(metaPath, 'pack-malformed-meta', validateMeta);
|
|
484
|
+
if (!loaded.ok) return loaded;
|
|
485
|
+
const { raw, parsed } = loaded.value;
|
|
486
|
+
|
|
487
|
+
const metaContract = validateProducerContract(parsed);
|
|
488
|
+
if (!metaContract.ok) {
|
|
489
|
+
return packErr(
|
|
490
|
+
makePackError('pack-malformed-meta', {
|
|
491
|
+
path: metaPath,
|
|
492
|
+
ajvErrors: [{ instancePath: '', message: metaContract.error.code }],
|
|
493
|
+
}),
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Step 3: validate GUIDs in meta subAssets
|
|
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
|
+
});
|
|
505
|
+
for (const sub of metaObj.subAssets) {
|
|
506
|
+
if (!isValidAssetGuidString(sub.guid)) {
|
|
507
|
+
return packErr(
|
|
508
|
+
makePackError('pack-guid-malformed', {
|
|
509
|
+
raw: sub.guid,
|
|
510
|
+
reason: 'expected 36-char RFC 4122 dash-form UUID in subAssets[].guid',
|
|
511
|
+
}),
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
const normalizedGuid = sub.guid.toLowerCase();
|
|
515
|
+
const existing = guidToPath.get(normalizedGuid);
|
|
516
|
+
if (existing !== undefined) {
|
|
517
|
+
return packErr(
|
|
518
|
+
makePackError('pack-guid-collision', {
|
|
519
|
+
paths: [existing, metaPath],
|
|
520
|
+
guid: normalizedGuid,
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
guidToPath.set(normalizedGuid, metaPath);
|
|
525
|
+
}
|
|
526
|
+
const producerSubAssets = metaObj.subAssets.length > 1 ? metaObj.subAssets : [];
|
|
527
|
+
if (producerSubAssets.length > 0) {
|
|
528
|
+
const topologyContract = validateProducerOutputs(producerSubAssets);
|
|
529
|
+
if (!topologyContract.ok) {
|
|
530
|
+
return packErr(
|
|
531
|
+
makePackError('pack-malformed-meta', {
|
|
532
|
+
path: metaPath,
|
|
533
|
+
ajvErrors: [{ instancePath: '/subAssets', message: topologyContract.error.code }],
|
|
534
|
+
}),
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Step 5: orphan .meta.json check — the source file declared in meta.source must exist.
|
|
540
|
+
// Uses resolveAssetSource for three-mode dispatch (undefined derivation / @name/ path table /
|
|
541
|
+
// relative path), replacing the former hardcoded join(metaDir, metaObj.source).
|
|
542
|
+
const sourceResolved = resolveAssetSource(metaPath, metaObj.source, assetPaths);
|
|
543
|
+
if (!sourceResolved.ok) {
|
|
544
|
+
return packErr(sourceResolved.error);
|
|
545
|
+
}
|
|
546
|
+
const expectedSourcePath = sourceResolved.value;
|
|
547
|
+
try {
|
|
548
|
+
await stat(expectedSourcePath);
|
|
549
|
+
} catch {
|
|
550
|
+
return packErr(
|
|
551
|
+
makePackError('pack-orphan-meta', {
|
|
552
|
+
metaPath,
|
|
553
|
+
expectedFile: expectedSourcePath,
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ScriptablePack identity enters the same topology and collision authority as JSON declarations.
|
|
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
|
+
}),
|
|
603
|
+
);
|
|
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) {
|
|
631
|
+
return packErr(
|
|
632
|
+
makePackError('pack-guid-collision', { paths: [existing, sourcePath], guid }),
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
guidToPath.set(guid, sourcePath);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Step 6: cyclic reference detection via hand-written DFS (no graphlib dep)
|
|
640
|
+
// visited: nodes fully processed; recStack: nodes in current DFS path
|
|
641
|
+
const visited = new Set<string>();
|
|
642
|
+
const recStack = new Set<string>();
|
|
643
|
+
|
|
644
|
+
function dfs(guid: string, path: string[]): string[] | null {
|
|
645
|
+
visited.add(guid);
|
|
646
|
+
recStack.add(guid);
|
|
647
|
+
|
|
648
|
+
for (const ref of packRefs.get(guid) ?? []) {
|
|
649
|
+
if (!visited.has(ref)) {
|
|
650
|
+
const cycle = dfs(ref, [...path, ref]);
|
|
651
|
+
if (cycle !== null) return cycle;
|
|
652
|
+
} else if (recStack.has(ref)) {
|
|
653
|
+
// Found a back-edge: reconstruct cycle from the repeated node
|
|
654
|
+
const cycleStart = path.indexOf(ref);
|
|
655
|
+
return cycleStart >= 0 ? [...path.slice(cycleStart), ref] : [...path, ref];
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
recStack.delete(guid);
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
for (const guid of guidToPath.keys()) {
|
|
664
|
+
if (!visited.has(guid)) {
|
|
665
|
+
const cycle = dfs(guid, [guid]);
|
|
666
|
+
if (cycle !== null) {
|
|
667
|
+
return packErr(
|
|
668
|
+
makePackError('pack-cyclic-reference', {
|
|
669
|
+
code: 'pack-cyclic-reference',
|
|
670
|
+
kind: 'mount-asset',
|
|
671
|
+
cycle,
|
|
672
|
+
}),
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return ok(rawPaths);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Scan one complete Pack inventory while preserving the public path-only API. */
|
|
682
|
+
export async function scan(
|
|
683
|
+
roots: readonly string[],
|
|
684
|
+
opts: ScanOptions = {},
|
|
685
|
+
): Promise<ScanResult<string[], PackError>> {
|
|
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 });
|
|
715
|
+
}
|