@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/errors.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// errors.ts — PackError class (feat-20260513-guid-asset-package-system w15)
|
|
2
|
+
//
|
|
3
|
+
// Four-field surface: .code / .expected / .hint / .detail
|
|
4
|
+
// Structurally parallel to AssetError / RhiError / RemoteError
|
|
5
|
+
// (charter proposition 5 consistent abstraction).
|
|
6
|
+
//
|
|
7
|
+
// .detail is narrowed per .code via PackErrorDetail discriminated union
|
|
8
|
+
// (requirements §6.2 AC-07; plan-strategy §D-5).
|
|
9
|
+
|
|
10
|
+
import type { PackErrorCode, PackErrorDetail } from '@forgeax/engine-types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Structured error for the engine-pack disk scanner fail-fast chain.
|
|
14
|
+
*
|
|
15
|
+
* AI users consume via exhaustive switch on .code — no default case needed:
|
|
16
|
+
* ```ts
|
|
17
|
+
* switch (err.code) {
|
|
18
|
+
* case 'pack-guid-collision':
|
|
19
|
+
* console.error(err.detail.paths);
|
|
20
|
+
* break;
|
|
21
|
+
* case 'pack-cyclic-reference':
|
|
22
|
+
* console.error(err.detail.cycle);
|
|
23
|
+
* break;
|
|
24
|
+
* // ...all 8 cases
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export class PackError extends Error {
|
|
29
|
+
readonly code: PackErrorCode;
|
|
30
|
+
readonly expected: string;
|
|
31
|
+
readonly hint: string;
|
|
32
|
+
readonly detail: PackErrorDetail;
|
|
33
|
+
|
|
34
|
+
constructor(args: {
|
|
35
|
+
code: PackErrorCode;
|
|
36
|
+
expected: string;
|
|
37
|
+
hint: string;
|
|
38
|
+
detail: PackErrorDetail;
|
|
39
|
+
}) {
|
|
40
|
+
super(`[PackError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);
|
|
41
|
+
this.name = 'PackError';
|
|
42
|
+
this.code = args.code;
|
|
43
|
+
this.expected = args.expected;
|
|
44
|
+
this.hint = args.hint;
|
|
45
|
+
this.detail = args.detail;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MaterialAsset,
|
|
3
|
+
MaterialParameter,
|
|
4
|
+
MaterialPass,
|
|
5
|
+
MaterialTextureReference,
|
|
6
|
+
MaterialTextureValue,
|
|
7
|
+
MaterialValue,
|
|
8
|
+
Result,
|
|
9
|
+
} from '@forgeax/engine-types';
|
|
10
|
+
import { err, MATERIAL_TEXTURE_SLOTS, ok } from '@forgeax/engine-types';
|
|
11
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
12
|
+
import { bytesToHex } from '@noble/hashes/utils.js';
|
|
13
|
+
|
|
14
|
+
export interface MaterialCookRefs {
|
|
15
|
+
readonly parent: readonly string[];
|
|
16
|
+
readonly textures: readonly string[];
|
|
17
|
+
readonly samplers: readonly string[];
|
|
18
|
+
readonly modules: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MaterialCookArtifact {
|
|
22
|
+
readonly mediaType: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly digest: string;
|
|
25
|
+
readonly bytes: Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MaterialCookReceipt {
|
|
29
|
+
readonly schemaVersion: 'material-cook/2';
|
|
30
|
+
readonly sourceClosure: readonly string[];
|
|
31
|
+
readonly profile: string;
|
|
32
|
+
readonly compilerVersion: string;
|
|
33
|
+
readonly inputDigest: string;
|
|
34
|
+
readonly outputDigest: string;
|
|
35
|
+
readonly layoutIdentity: string;
|
|
36
|
+
readonly derivedInterface: { readonly layoutIdentity: string };
|
|
37
|
+
readonly publicationGeneration?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MaterialParameterContract {
|
|
41
|
+
readonly parameters: readonly MaterialParameter[];
|
|
42
|
+
readonly values: Readonly<Record<string, MaterialValue | null>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CookedMaterialRecord {
|
|
46
|
+
readonly schemaVersion: 'material-cook/2';
|
|
47
|
+
readonly guid: string;
|
|
48
|
+
readonly authored?: MaterialAsset;
|
|
49
|
+
readonly materialGuid?: string;
|
|
50
|
+
readonly publicationGeneration?: number;
|
|
51
|
+
readonly specializationKey?: string;
|
|
52
|
+
readonly artifactDigest?: string;
|
|
53
|
+
readonly sourceClosure?: readonly string[];
|
|
54
|
+
readonly parameterContract?: MaterialParameterContract;
|
|
55
|
+
readonly resolved: {
|
|
56
|
+
readonly passes: readonly MaterialPass[];
|
|
57
|
+
readonly parameters: readonly MaterialParameter[];
|
|
58
|
+
readonly values: Readonly<Record<string, MaterialValue | null>>;
|
|
59
|
+
};
|
|
60
|
+
readonly refs: MaterialCookRefs;
|
|
61
|
+
readonly artifact: MaterialCookArtifact;
|
|
62
|
+
readonly receipt: MaterialCookReceipt;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface MaterialCookRecordError {
|
|
66
|
+
readonly code: 'material-cook-record-invalid';
|
|
67
|
+
readonly expected: string;
|
|
68
|
+
readonly hint: string;
|
|
69
|
+
readonly detail: { readonly field: string };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MaterialCookIdentityExpectation {
|
|
73
|
+
readonly layoutIdentity?: string;
|
|
74
|
+
readonly artifactDigest?: string;
|
|
75
|
+
readonly inputDigest?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function unique(values: readonly string[]): readonly string[] {
|
|
79
|
+
return [...new Set(values)].sort();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function guidText(value: string | Uint8Array): string {
|
|
83
|
+
return typeof value === 'string'
|
|
84
|
+
? value
|
|
85
|
+
: Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function cookGuidText(value: MaterialTextureReference): string | undefined {
|
|
89
|
+
return typeof value === 'number' ? undefined : guidText(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function textureValues(
|
|
93
|
+
values: Readonly<Record<string, MaterialValue | null>> | undefined,
|
|
94
|
+
textureFields: ReadonlySet<string>,
|
|
95
|
+
): readonly MaterialTextureValue[] {
|
|
96
|
+
return Object.entries(values ?? {}).flatMap(([name, value]) => {
|
|
97
|
+
if (value === null) return [];
|
|
98
|
+
if (typeof value === 'string') {
|
|
99
|
+
return textureFields.has(name) ? [{ texture: value }] : [];
|
|
100
|
+
}
|
|
101
|
+
return value !== null &&
|
|
102
|
+
typeof value === 'object' &&
|
|
103
|
+
!Array.isArray(value) &&
|
|
104
|
+
'texture' in value
|
|
105
|
+
? [value as MaterialTextureValue]
|
|
106
|
+
: [];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function collectMaterialCookRefs(material: Partial<MaterialAsset>): MaterialCookRefs {
|
|
111
|
+
const textureFields =
|
|
112
|
+
material.parameters === undefined
|
|
113
|
+
? new Set<string>(MATERIAL_TEXTURE_SLOTS)
|
|
114
|
+
: new Set(
|
|
115
|
+
material.parameters
|
|
116
|
+
.filter((parameter) => parameter.type === 'texture')
|
|
117
|
+
.map((parameter) => parameter.name),
|
|
118
|
+
);
|
|
119
|
+
const textures = textureValues(material.values, textureFields);
|
|
120
|
+
return {
|
|
121
|
+
parent: material.parent ? [guidText(material.parent)] : [],
|
|
122
|
+
textures: unique(
|
|
123
|
+
textures.flatMap((value) => {
|
|
124
|
+
const guid = cookGuidText(value.texture);
|
|
125
|
+
return guid === undefined ? [] : [guid];
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
samplers: unique(
|
|
129
|
+
textures.flatMap((value) => {
|
|
130
|
+
if (value.sampler === undefined) return [];
|
|
131
|
+
const guid = cookGuidText(value.sampler);
|
|
132
|
+
return guid === undefined ? [] : [guid];
|
|
133
|
+
}),
|
|
134
|
+
),
|
|
135
|
+
modules: unique((material.passes ?? []).map((pass) => pass.program.module)),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function createMaterialArtifactDigest(bytes: Uint8Array): string {
|
|
140
|
+
return `sha256:${bytesToHex(sha256(bytes))}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function jsonValue(value: unknown): unknown {
|
|
144
|
+
if (value instanceof Uint8Array) return [...value];
|
|
145
|
+
if (Array.isArray(value)) return value.map(jsonValue);
|
|
146
|
+
if (value !== null && typeof value === 'object') {
|
|
147
|
+
return Object.fromEntries(
|
|
148
|
+
Object.entries(value)
|
|
149
|
+
.filter(([, entry]) => entry !== undefined)
|
|
150
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
151
|
+
.map(([key, entry]) => [key, jsonValue(entry)]),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function serializeCookedMaterialRecord(record: CookedMaterialRecord): string {
|
|
158
|
+
return JSON.stringify(jsonValue(record));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function serializeMaterialCookReceipt(receipt: MaterialCookReceipt): string {
|
|
162
|
+
return JSON.stringify(jsonValue({ ...receipt, sourceClosure: unique(receipt.sourceClosure) }));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function invalid(field: string): Result<never, MaterialCookRecordError> {
|
|
166
|
+
return err({
|
|
167
|
+
code: 'material-cook-record-invalid',
|
|
168
|
+
expected: 'a complete material-cook/2 record with derived layout identity',
|
|
169
|
+
hint: 're-cook the material and publish its record, artifact, references, and receipt together',
|
|
170
|
+
detail: { field },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function validateMaterialCookReceipt(
|
|
175
|
+
value: unknown,
|
|
176
|
+
expected: MaterialCookIdentityExpectation = {},
|
|
177
|
+
): Result<MaterialCookReceipt, MaterialCookRecordError> {
|
|
178
|
+
if (value === null || typeof value !== 'object') return invalid('receipt');
|
|
179
|
+
const candidate = value as Record<string, unknown>;
|
|
180
|
+
if (candidate.schemaVersion !== 'material-cook/2') return invalid('receipt.schemaVersion');
|
|
181
|
+
if (typeof candidate.layoutIdentity !== 'string' || candidate.layoutIdentity.length === 0) {
|
|
182
|
+
return invalid('receipt.layoutIdentity');
|
|
183
|
+
}
|
|
184
|
+
if (candidate.derivedInterface === null || typeof candidate.derivedInterface !== 'object') {
|
|
185
|
+
return invalid('receipt.derivedInterface');
|
|
186
|
+
}
|
|
187
|
+
const derivedInterface = candidate.derivedInterface as Record<string, unknown>;
|
|
188
|
+
if (derivedInterface.layoutIdentity !== candidate.layoutIdentity) {
|
|
189
|
+
return invalid('receipt.derivedInterface.layoutIdentity');
|
|
190
|
+
}
|
|
191
|
+
for (const field of [
|
|
192
|
+
'sourceClosure',
|
|
193
|
+
'profile',
|
|
194
|
+
'compilerVersion',
|
|
195
|
+
'inputDigest',
|
|
196
|
+
'outputDigest',
|
|
197
|
+
]) {
|
|
198
|
+
const fieldValue = candidate[field];
|
|
199
|
+
if (
|
|
200
|
+
(field === 'sourceClosure' && !Array.isArray(fieldValue)) ||
|
|
201
|
+
(field !== 'sourceClosure' && typeof fieldValue !== 'string')
|
|
202
|
+
) {
|
|
203
|
+
return invalid(`receipt.${field}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const sourceClosure = candidate.sourceClosure;
|
|
207
|
+
if (Array.isArray(sourceClosure) && sourceClosure.some((path) => typeof path !== 'string')) {
|
|
208
|
+
return invalid('receipt.sourceClosure');
|
|
209
|
+
}
|
|
210
|
+
if (
|
|
211
|
+
expected.layoutIdentity !== undefined &&
|
|
212
|
+
candidate.layoutIdentity !== expected.layoutIdentity
|
|
213
|
+
) {
|
|
214
|
+
return invalid('receipt.layoutIdentity');
|
|
215
|
+
}
|
|
216
|
+
if (expected.artifactDigest !== undefined && candidate.outputDigest !== expected.artifactDigest) {
|
|
217
|
+
return invalid('receipt.outputDigest');
|
|
218
|
+
}
|
|
219
|
+
if (expected.inputDigest !== undefined && candidate.inputDigest !== expected.inputDigest) {
|
|
220
|
+
return invalid('receipt.inputDigest');
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
candidate.publicationGeneration !== undefined &&
|
|
224
|
+
(typeof candidate.publicationGeneration !== 'number' ||
|
|
225
|
+
!Number.isSafeInteger(candidate.publicationGeneration) ||
|
|
226
|
+
candidate.publicationGeneration < 1)
|
|
227
|
+
) {
|
|
228
|
+
return invalid('receipt.publicationGeneration');
|
|
229
|
+
}
|
|
230
|
+
return ok({
|
|
231
|
+
schemaVersion: 'material-cook/2',
|
|
232
|
+
sourceClosure: candidate.sourceClosure as string[],
|
|
233
|
+
profile: candidate.profile as string,
|
|
234
|
+
compilerVersion: candidate.compilerVersion as string,
|
|
235
|
+
inputDigest: candidate.inputDigest as string,
|
|
236
|
+
outputDigest: candidate.outputDigest as string,
|
|
237
|
+
layoutIdentity: candidate.layoutIdentity,
|
|
238
|
+
derivedInterface: { layoutIdentity: derivedInterface.layoutIdentity as string },
|
|
239
|
+
...(candidate.publicationGeneration === undefined
|
|
240
|
+
? {}
|
|
241
|
+
: { publicationGeneration: candidate.publicationGeneration as number }),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function validateCookedMaterialRecord(
|
|
246
|
+
value: unknown,
|
|
247
|
+
): Result<CookedMaterialRecord, MaterialCookRecordError> {
|
|
248
|
+
if (value === null || typeof value !== 'object') return invalid('record');
|
|
249
|
+
const candidate = value as Record<string, unknown>;
|
|
250
|
+
if (candidate.schemaVersion !== 'material-cook/2') return invalid('schemaVersion');
|
|
251
|
+
if (typeof candidate.guid !== 'string' || !candidate.guid) return invalid('guid');
|
|
252
|
+
if (candidate.materialGuid !== undefined && typeof candidate.materialGuid !== 'string')
|
|
253
|
+
return invalid('materialGuid');
|
|
254
|
+
if (
|
|
255
|
+
candidate.publicationGeneration !== undefined &&
|
|
256
|
+
(typeof candidate.publicationGeneration !== 'number' ||
|
|
257
|
+
!Number.isSafeInteger(candidate.publicationGeneration) ||
|
|
258
|
+
candidate.publicationGeneration < 1)
|
|
259
|
+
)
|
|
260
|
+
return invalid('publicationGeneration');
|
|
261
|
+
if (candidate.specializationKey !== undefined && typeof candidate.specializationKey !== 'string')
|
|
262
|
+
return invalid('specializationKey');
|
|
263
|
+
if (candidate.artifactDigest !== undefined && typeof candidate.artifactDigest !== 'string')
|
|
264
|
+
return invalid('artifactDigest');
|
|
265
|
+
if (
|
|
266
|
+
candidate.sourceClosure !== undefined &&
|
|
267
|
+
(!Array.isArray(candidate.sourceClosure) ||
|
|
268
|
+
candidate.sourceClosure.some((path) => typeof path !== 'string'))
|
|
269
|
+
)
|
|
270
|
+
return invalid('sourceClosure');
|
|
271
|
+
if (candidate.parameterContract !== undefined) {
|
|
272
|
+
if (candidate.parameterContract === null || typeof candidate.parameterContract !== 'object')
|
|
273
|
+
return invalid('parameterContract');
|
|
274
|
+
const parameterContract = candidate.parameterContract as Record<string, unknown>;
|
|
275
|
+
if (!Array.isArray(parameterContract.parameters))
|
|
276
|
+
return invalid('parameterContract.parameters');
|
|
277
|
+
if (
|
|
278
|
+
parameterContract.values === null ||
|
|
279
|
+
typeof parameterContract.values !== 'object' ||
|
|
280
|
+
Array.isArray(parameterContract.values)
|
|
281
|
+
)
|
|
282
|
+
return invalid('parameterContract.values');
|
|
283
|
+
}
|
|
284
|
+
if (candidate.resolved === null || typeof candidate.resolved !== 'object')
|
|
285
|
+
return invalid('resolved');
|
|
286
|
+
if (candidate.refs === null || typeof candidate.refs !== 'object') return invalid('refs');
|
|
287
|
+
if (candidate.artifact === null || typeof candidate.artifact !== 'object')
|
|
288
|
+
return invalid('artifact');
|
|
289
|
+
if (candidate.receipt === null || typeof candidate.receipt !== 'object')
|
|
290
|
+
return invalid('receipt');
|
|
291
|
+
const artifact = candidate.artifact as Record<string, unknown>;
|
|
292
|
+
if (
|
|
293
|
+
typeof artifact.digest !== 'string' ||
|
|
294
|
+
(!Array.isArray(artifact.bytes) && !ArrayBuffer.isView(artifact.bytes))
|
|
295
|
+
) {
|
|
296
|
+
return invalid('artifact');
|
|
297
|
+
}
|
|
298
|
+
const receiptResult = validateMaterialCookReceipt(candidate.receipt, {
|
|
299
|
+
artifactDigest: artifact.digest as string,
|
|
300
|
+
});
|
|
301
|
+
if (!receiptResult.ok) return receiptResult;
|
|
302
|
+
if (candidate.artifactDigest !== undefined && candidate.artifactDigest !== artifact.digest)
|
|
303
|
+
return invalid('artifactDigest');
|
|
304
|
+
if (
|
|
305
|
+
candidate.publicationGeneration !== undefined &&
|
|
306
|
+
receiptResult.value.publicationGeneration !== candidate.publicationGeneration
|
|
307
|
+
)
|
|
308
|
+
return invalid('receipt.publicationGeneration');
|
|
309
|
+
const normalized = {
|
|
310
|
+
...candidate,
|
|
311
|
+
artifact: {
|
|
312
|
+
...artifact,
|
|
313
|
+
bytes: ArrayBuffer.isView(artifact.bytes)
|
|
314
|
+
? Uint8Array.from(artifact.bytes as Uint8Array)
|
|
315
|
+
: Uint8Array.from(artifact.bytes as number[]),
|
|
316
|
+
},
|
|
317
|
+
receipt: receiptResult.value,
|
|
318
|
+
} as CookedMaterialRecord;
|
|
319
|
+
return ok(normalized);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function projectCookedMaterialRecord(
|
|
323
|
+
record: CookedMaterialRecord,
|
|
324
|
+
): Omit<CookedMaterialRecord, 'guid' | 'authored'> {
|
|
325
|
+
return {
|
|
326
|
+
resolved: record.resolved,
|
|
327
|
+
refs: record.refs,
|
|
328
|
+
artifact: record.artifact,
|
|
329
|
+
receipt: record.receipt,
|
|
330
|
+
schemaVersion: record.schemaVersion,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ArtifactDescriptor,
|
|
3
|
+
ArtifactVerificationStatus,
|
|
4
|
+
AssetEvidence,
|
|
5
|
+
AssetEvidenceError,
|
|
6
|
+
AssetEvidenceInputs,
|
|
7
|
+
AssetEvidenceLocator,
|
|
8
|
+
CookProduct,
|
|
9
|
+
CookReceipt,
|
|
10
|
+
PackageVerificationEvidence,
|
|
11
|
+
SourceDeclarationEvidence,
|
|
12
|
+
} from '@forgeax/engine-types';
|
|
13
|
+
import {
|
|
14
|
+
projectAssetEvidence,
|
|
15
|
+
projectCookProductEvidence,
|
|
16
|
+
type Result,
|
|
17
|
+
} from '@forgeax/engine-types';
|
|
18
|
+
|
|
19
|
+
/** Pack artifact descriptor with verification supplied by an offline verifier. */
|
|
20
|
+
export interface OfflineArtifactInput extends ArtifactDescriptor {
|
|
21
|
+
readonly verification?: ArtifactVerificationStatus;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Validated Pack v2 package facts consumed by the offline projector. */
|
|
25
|
+
export interface OfflinePackageInput {
|
|
26
|
+
readonly guid: string;
|
|
27
|
+
readonly digest?: string;
|
|
28
|
+
readonly artifacts: Readonly<Record<string, OfflineArtifactInput>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Node/CLI input shape; it intentionally has no runtime or WebSocket fields. */
|
|
32
|
+
export interface OfflineAssetEvidenceInput {
|
|
33
|
+
readonly guid: string;
|
|
34
|
+
readonly source?: SourceDeclarationEvidence;
|
|
35
|
+
readonly locator?: AssetEvidenceLocator;
|
|
36
|
+
readonly receipt?: CookReceipt;
|
|
37
|
+
readonly package?: OfflinePackageInput;
|
|
38
|
+
readonly product?: CookProduct;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function packageEvidence(input: OfflinePackageInput): NonNullable<AssetEvidenceInputs['package']> {
|
|
42
|
+
return {
|
|
43
|
+
guid: input.guid,
|
|
44
|
+
...(input.digest === undefined ? {} : { digest: input.digest }),
|
|
45
|
+
artifacts: Object.fromEntries(
|
|
46
|
+
Object.entries(input.artifacts).map(([key, artifact]) => [
|
|
47
|
+
key,
|
|
48
|
+
{
|
|
49
|
+
descriptor: artifact,
|
|
50
|
+
verification: artifact.verification ?? 'passed',
|
|
51
|
+
},
|
|
52
|
+
]),
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Build the shared evidence view without loading runtime assets or opening WS. */
|
|
58
|
+
export function buildOfflineAssetEvidence(
|
|
59
|
+
input: OfflineAssetEvidenceInput,
|
|
60
|
+
): Promise<Result<AssetEvidence, AssetEvidenceError>> {
|
|
61
|
+
if (input.product !== undefined) {
|
|
62
|
+
return Promise.resolve(projectCookProductEvidence(input.product, input.locator));
|
|
63
|
+
}
|
|
64
|
+
const evidenceInput: AssetEvidenceInputs = {
|
|
65
|
+
guid: input.guid,
|
|
66
|
+
...(input.source === undefined ? {} : { source: input.source }),
|
|
67
|
+
...(input.locator === undefined ? {} : { locator: input.locator }),
|
|
68
|
+
...(input.receipt === undefined ? {} : { receipt: input.receipt }),
|
|
69
|
+
...(input.package === undefined ? {} : { package: packageEvidence(input.package) }),
|
|
70
|
+
};
|
|
71
|
+
return Promise.resolve(projectAssetEvidence(evidenceInput));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function packageVerification(
|
|
75
|
+
input: OfflinePackageInput | undefined,
|
|
76
|
+
): PackageVerificationEvidence | undefined {
|
|
77
|
+
if (input === undefined) return undefined;
|
|
78
|
+
const packageInput = packageEvidence(input);
|
|
79
|
+
const statuses = Object.values(packageInput.artifacts).map(
|
|
80
|
+
(artifact) => artifact.verification ?? 'notChecked',
|
|
81
|
+
);
|
|
82
|
+
return {
|
|
83
|
+
status: statuses.some((status) => status === 'failed')
|
|
84
|
+
? 'failed'
|
|
85
|
+
: statuses.length > 0 && statuses.every((status) => status === 'passed')
|
|
86
|
+
? 'passed'
|
|
87
|
+
: 'notChecked',
|
|
88
|
+
...(input?.digest === undefined ? {} : { digest: input.digest }),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import type { SourceDeclarationEvidence } from '@forgeax/engine-types';
|
|
5
|
+
import { SCANNER_BLACKLIST, scan } from '../scanner.js';
|
|
6
|
+
import { projectScriptablePackMeta } from '../scriptable-pack.js';
|
|
7
|
+
import { loadScriptablePack } from '../scriptable-pack-node.js';
|
|
8
|
+
|
|
9
|
+
/** Project root and GUID used to locate an authoritative source declaration. */
|
|
10
|
+
export interface SourceInventoryRequest {
|
|
11
|
+
readonly projectRoot: string;
|
|
12
|
+
readonly guid: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProducerSemanticIdentityInput {
|
|
16
|
+
readonly producerRoot: string;
|
|
17
|
+
readonly sourcePath: string;
|
|
18
|
+
readonly sourceDigest: string;
|
|
19
|
+
readonly schemaVersion: string;
|
|
20
|
+
readonly importer: string;
|
|
21
|
+
readonly codec: string;
|
|
22
|
+
readonly settings: unknown;
|
|
23
|
+
readonly producer: string;
|
|
24
|
+
readonly profile: string;
|
|
25
|
+
readonly declaredGuids?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProducerRelativeDdcIdentity {
|
|
29
|
+
readonly logicalPath: string;
|
|
30
|
+
readonly sourceDigest: string;
|
|
31
|
+
readonly key: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stableValue(value: unknown): unknown {
|
|
35
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
36
|
+
if (value !== null && typeof value === 'object') {
|
|
37
|
+
return Object.fromEntries(
|
|
38
|
+
Object.entries(value as Record<string, unknown>)
|
|
39
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
40
|
+
.map(([key, entry]) => [key, stableValue(entry)]),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function producerRelativeLogicalPath(producerRoot: string, sourcePath: string): string {
|
|
47
|
+
const root = resolve(producerRoot);
|
|
48
|
+
const source = resolve(sourcePath);
|
|
49
|
+
const path = relative(root, source);
|
|
50
|
+
if (
|
|
51
|
+
path.length === 0 ||
|
|
52
|
+
path === '..' ||
|
|
53
|
+
path.startsWith(`..${sep}`) ||
|
|
54
|
+
resolve(root, path) !== source
|
|
55
|
+
) {
|
|
56
|
+
throw new Error('producer source must be inside the injected producer root');
|
|
57
|
+
}
|
|
58
|
+
return path.split(sep).join('/');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function producerRelativeDdcKey(input: ProducerSemanticIdentityInput): string {
|
|
62
|
+
const logicalPath = producerRelativeLogicalPath(input.producerRoot, input.sourcePath);
|
|
63
|
+
const semantic = {
|
|
64
|
+
schemaVersion: input.schemaVersion,
|
|
65
|
+
logicalPath,
|
|
66
|
+
sourceDigest: input.sourceDigest,
|
|
67
|
+
importer: input.importer,
|
|
68
|
+
codec: input.codec,
|
|
69
|
+
settings: stableValue(input.settings),
|
|
70
|
+
producer: input.producer,
|
|
71
|
+
profile: input.profile,
|
|
72
|
+
declaredGuids: [...(input.declaredGuids ?? [])].sort(),
|
|
73
|
+
};
|
|
74
|
+
return createHash('sha256').update(JSON.stringify(semantic)).digest('hex');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function producerRelativeDdcIdentity(
|
|
78
|
+
input: ProducerSemanticIdentityInput,
|
|
79
|
+
): ProducerRelativeDdcIdentity {
|
|
80
|
+
return {
|
|
81
|
+
logicalPath: producerRelativeLogicalPath(input.producerRoot, input.sourcePath),
|
|
82
|
+
sourceDigest: input.sourceDigest,
|
|
83
|
+
key: producerRelativeDdcKey(input),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fingerprint(meta: string, source: Uint8Array | undefined): string {
|
|
88
|
+
const hash = createHash('sha256').update(meta);
|
|
89
|
+
if (source !== undefined) hash.update(source);
|
|
90
|
+
return hash.digest('base64');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sourceFromMeta(
|
|
94
|
+
metaPath: string,
|
|
95
|
+
meta: {
|
|
96
|
+
readonly source?: unknown;
|
|
97
|
+
readonly inputFingerprint?: unknown;
|
|
98
|
+
readonly importSettings?: unknown;
|
|
99
|
+
readonly subAssets?: readonly { readonly guid?: unknown }[];
|
|
100
|
+
},
|
|
101
|
+
guid: string,
|
|
102
|
+
raw: string,
|
|
103
|
+
): SourceDeclarationEvidence | undefined {
|
|
104
|
+
if (
|
|
105
|
+
!meta.subAssets?.some(
|
|
106
|
+
(asset) => typeof asset.guid === 'string' && asset.guid.toLowerCase() === guid.toLowerCase(),
|
|
107
|
+
)
|
|
108
|
+
) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const source =
|
|
112
|
+
typeof meta.source === 'string' ? meta.source : basename(metaPath).replace(/\.meta\.json$/, '');
|
|
113
|
+
const sourcePath = resolve(dirname(metaPath), source);
|
|
114
|
+
const inputFingerprint =
|
|
115
|
+
typeof meta.inputFingerprint === 'string' ? meta.inputFingerprint : fingerprint(raw, undefined);
|
|
116
|
+
return { origin: 'sourceMeta', sourcePath, inputFingerprint };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function collectDeclarationPaths(root: string): Promise<string[]> {
|
|
120
|
+
const paths: string[] = [];
|
|
121
|
+
async function visit(dir: string): Promise<void> {
|
|
122
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
const path = join(dir, entry.name);
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
if (!SCANNER_BLACKLIST.has(entry.name)) await visit(path);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
entry.isFile() &&
|
|
131
|
+
(entry.name.endsWith('.meta.json') ||
|
|
132
|
+
entry.name.endsWith('.pack.json') ||
|
|
133
|
+
entry.name.endsWith('.pack.ts'))
|
|
134
|
+
) {
|
|
135
|
+
paths.push(path);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await visit(root);
|
|
140
|
+
return paths;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Read `.meta.json` or `.pack.json` source declarations without runtime imports. */
|
|
144
|
+
export async function readSourceInventory(
|
|
145
|
+
request: SourceInventoryRequest,
|
|
146
|
+
): Promise<SourceDeclarationEvidence | undefined> {
|
|
147
|
+
const result = await scan([request.projectRoot]);
|
|
148
|
+
const paths = result.ok ? result.value : await collectDeclarationPaths(request.projectRoot);
|
|
149
|
+
const guid = request.guid.toLowerCase();
|
|
150
|
+
let authored: SourceDeclarationEvidence | undefined;
|
|
151
|
+
for (const path of paths) {
|
|
152
|
+
if (path.endsWith('.pack.ts')) {
|
|
153
|
+
const loaded = await loadScriptablePack(path, { metadataOnly: true });
|
|
154
|
+
if (!loaded.ok) continue;
|
|
155
|
+
const meta = projectScriptablePackMeta(loaded.value, path);
|
|
156
|
+
if (meta.subAssets.some((asset) => asset.guid.toLowerCase() === guid)) {
|
|
157
|
+
return { origin: 'sourceMeta', sourcePath: path };
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const raw = await readFile(path, 'utf8').catch(() => undefined);
|
|
162
|
+
if (raw === undefined) continue;
|
|
163
|
+
let parsed: {
|
|
164
|
+
readonly assets?: readonly { readonly guid?: unknown }[];
|
|
165
|
+
readonly source?: unknown;
|
|
166
|
+
readonly inputFingerprint?: unknown;
|
|
167
|
+
readonly importSettings?: unknown;
|
|
168
|
+
readonly subAssets?: readonly { readonly guid?: unknown }[];
|
|
169
|
+
};
|
|
170
|
+
try {
|
|
171
|
+
parsed = JSON.parse(raw) as typeof parsed;
|
|
172
|
+
} catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (path.endsWith('.pack.json')) {
|
|
176
|
+
if (
|
|
177
|
+
parsed.assets?.some(
|
|
178
|
+
(asset) =>
|
|
179
|
+
typeof asset.guid === 'string' &&
|
|
180
|
+
(asset.guid === request.guid || asset.guid.toLowerCase() === guid),
|
|
181
|
+
)
|
|
182
|
+
) {
|
|
183
|
+
authored = { origin: 'authoredPack', sourcePath: path };
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (path.endsWith('.meta.json')) {
|
|
188
|
+
const source = sourceFromMeta(path, parsed, request.guid, raw);
|
|
189
|
+
if (source !== undefined) return source;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return authored;
|
|
193
|
+
}
|