@aura3d/cli 1.3.2 → 1.4.0
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/dist/asset-constants.d.ts +5 -0
- package/dist/asset-constants.d.ts.map +1 -0
- package/dist/asset-constants.js +5 -0
- package/dist/asset-constants.js.map +1 -0
- package/dist/asset-core-types.d.ts +206 -0
- package/dist/asset-core-types.d.ts.map +1 -0
- package/dist/asset-core-types.js +2 -0
- package/dist/asset-core-types.js.map +1 -0
- package/dist/asset-inspection-types.d.ts +131 -0
- package/dist/asset-inspection-types.d.ts.map +1 -0
- package/dist/asset-inspection-types.js +2 -0
- package/dist/asset-inspection-types.js.map +1 -0
- package/dist/asset-manifest.d.ts +8 -0
- package/dist/asset-manifest.d.ts.map +1 -0
- package/dist/asset-manifest.js +116 -0
- package/dist/asset-manifest.js.map +1 -0
- package/dist/asset-readiness-types.d.ts +164 -0
- package/dist/asset-readiness-types.d.ts.map +1 -0
- package/dist/asset-readiness-types.js +2 -0
- package/dist/asset-readiness-types.js.map +1 -0
- package/dist/asset-source-ast.d.ts +8 -0
- package/dist/asset-source-ast.d.ts.map +1 -0
- package/dist/asset-source-ast.js +241 -0
- package/dist/asset-source-ast.js.map +1 -0
- package/dist/asset-source-roles.d.ts +4 -0
- package/dist/asset-source-roles.d.ts.map +1 -0
- package/dist/asset-source-roles.js +79 -0
- package/dist/asset-source-roles.js.map +1 -0
- package/dist/asset-source-validation.d.ts +4 -0
- package/dist/asset-source-validation.d.ts.map +1 -0
- package/dist/asset-source-validation.js +200 -0
- package/dist/asset-source-validation.js.map +1 -0
- package/dist/character-assembly-types.d.ts +39 -0
- package/dist/character-assembly-types.d.ts.map +1 -0
- package/dist/character-assembly-types.js +2 -0
- package/dist/character-assembly-types.js.map +1 -0
- package/dist/cli-help.d.ts +4 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +48 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-options.d.ts +66 -0
- package/dist/cli-options.d.ts.map +1 -0
- package/dist/cli-options.js +234 -0
- package/dist/cli-options.js.map +1 -0
- package/dist/cli.js +20 -177
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +12 -402
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +948 -90
- package/dist/index.js.map +1 -1
- package/dist/pull-bridge/adapters.d.ts +4 -0
- package/dist/pull-bridge/adapters.d.ts.map +1 -0
- package/dist/pull-bridge/adapters.js +38 -0
- package/dist/pull-bridge/adapters.js.map +1 -0
- package/dist/pull-bridge/download.d.ts +6 -0
- package/dist/pull-bridge/download.d.ts.map +1 -0
- package/dist/pull-bridge/download.js +208 -0
- package/dist/pull-bridge/download.js.map +1 -0
- package/dist/pull-bridge/profiles.d.ts +14 -0
- package/dist/pull-bridge/profiles.d.ts.map +1 -0
- package/dist/pull-bridge/profiles.js +63 -0
- package/dist/pull-bridge/profiles.js.map +1 -0
- package/dist/pull-bridge/provenance.d.ts +9 -0
- package/dist/pull-bridge/provenance.d.ts.map +1 -0
- package/dist/pull-bridge/provenance.js +99 -0
- package/dist/pull-bridge/provenance.js.map +1 -0
- package/dist/pull-bridge/pullable.d.ts +14 -0
- package/dist/pull-bridge/pullable.d.ts.map +1 -0
- package/dist/pull-bridge/pullable.js +58 -0
- package/dist/pull-bridge/pullable.js.map +1 -0
- package/dist/pull-bridge/scoring.d.ts +21 -0
- package/dist/pull-bridge/scoring.d.ts.map +1 -0
- package/dist/pull-bridge/scoring.js +177 -0
- package/dist/pull-bridge/scoring.js.map +1 -0
- package/dist/pull-bridge/search.d.ts +39 -0
- package/dist/pull-bridge/search.d.ts.map +1 -0
- package/dist/pull-bridge/search.js +114 -0
- package/dist/pull-bridge/search.js.map +1 -0
- package/dist/pull-bridge/types.d.ts +9 -0
- package/dist/pull-bridge/types.d.ts.map +1 -0
- package/dist/pull-bridge/types.js +2 -0
- package/dist/pull-bridge/types.js.map +1 -0
- package/dist/pull-bridge/vector3.d.ts +2 -0
- package/dist/pull-bridge/vector3.d.ts.map +1 -0
- package/dist/pull-bridge/vector3.js +4 -0
- package/dist/pull-bridge/vector3.js.map +1 -0
- package/dist/pull-bridge.d.ts +18 -100
- package/dist/pull-bridge.d.ts.map +1 -1
- package/dist/pull-bridge.js +59 -526
- package/dist/pull-bridge.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,97 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { inflateSync } from "node:zlib";
|
|
5
|
+
import { DEFAULT_AURA_ASSET_MANIFEST, DEFAULT_AURA_ASSET_OUTPUT_DIR, DEFAULT_AURA_ASSET_PUBLIC_PATH, DEFAULT_AURA_ASSET_TYPEGEN } from "./asset-constants.js";
|
|
6
|
+
import { shouldScanSource, validateAssetSource } from "./asset-source-validation.js";
|
|
7
|
+
import { listAssets, readAssetManifest, writeAssetManifest, writeTypedAssets } from "./asset-manifest.js";
|
|
4
8
|
export { animationCliAssetProfiles, getAnimationAssetProfileDefinition } from "./animation-asset-profiles.js";
|
|
5
9
|
export { validateAnimationAssets, parseAnimationClipMap, DEFAULT_ANIMATION_ACTIONS } from "./animation-asset-validator.js";
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
+
export { DEFAULT_AURA_ASSET_MANIFEST, DEFAULT_AURA_ASSET_OUTPUT_DIR, DEFAULT_AURA_ASSET_PUBLIC_PATH, DEFAULT_AURA_ASSET_TYPEGEN } from "./asset-constants.js";
|
|
11
|
+
export { listAssets, readAssetManifest, writeAssetManifest, writeTypedAssets } from "./asset-manifest.js";
|
|
12
|
+
export function readRenderedProbeMetadata(options) {
|
|
13
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
14
|
+
const metadataPath = resolve(projectDir, options.file);
|
|
15
|
+
if (!existsSync(metadataPath)) {
|
|
16
|
+
throw new Error(`Aura3D rendered probe metadata failed: "${options.file}" does not exist.`);
|
|
17
|
+
}
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new Error(`Aura3D rendered probe metadata failed: "${options.file}" is not valid JSON (${error instanceof Error ? error.message : String(error)}).`);
|
|
24
|
+
}
|
|
25
|
+
const root = objectValue(parsed);
|
|
26
|
+
const record = objectValue(root?.renderedProbe) ?? root;
|
|
27
|
+
const failures = [];
|
|
28
|
+
if (!record) {
|
|
29
|
+
failures.push("metadata must be an object or contain a renderedProbe object");
|
|
30
|
+
}
|
|
31
|
+
const url = stringValue(record?.url);
|
|
32
|
+
const kind = stringValue(record?.kind);
|
|
33
|
+
const renderer = stringValue(record?.renderer);
|
|
34
|
+
const route = stringValue(record?.route);
|
|
35
|
+
const sha = stringValue(record?.sha256);
|
|
36
|
+
const assetHash = stringValue(record?.assetHash);
|
|
37
|
+
const checkedAt = stringValue(record?.checkedAt);
|
|
38
|
+
const width = readPositiveInteger(record, "width", failures);
|
|
39
|
+
const height = readPositiveInteger(record, "height", failures);
|
|
40
|
+
const nonBlankPixels = readPositiveInteger(record, "nonBlankPixels", failures);
|
|
41
|
+
const colorBuckets = readPositiveInteger(record, "colorBuckets", failures);
|
|
42
|
+
const foregroundBounds = readOptionalForegroundBounds(record, failures);
|
|
43
|
+
if (!url)
|
|
44
|
+
failures.push("missing url");
|
|
45
|
+
if (!kind) {
|
|
46
|
+
failures.push("missing kind");
|
|
47
|
+
}
|
|
48
|
+
else if (kind !== "browser-screenshot" && kind !== "aura-probe-render") {
|
|
49
|
+
failures.push(`kind "${kind}" is not browser-screenshot or aura-probe-render`);
|
|
50
|
+
}
|
|
51
|
+
if (!renderer) {
|
|
52
|
+
failures.push("missing renderer");
|
|
53
|
+
}
|
|
54
|
+
else if (!/createAuraApp|@aura3d\/engine/i.test(renderer)) {
|
|
55
|
+
failures.push(`renderer "${renderer}" is not root @aura3d/engine/createAuraApp proof`);
|
|
56
|
+
}
|
|
57
|
+
if (!route)
|
|
58
|
+
failures.push("missing route");
|
|
59
|
+
if (!sha) {
|
|
60
|
+
failures.push("missing image sha256");
|
|
61
|
+
}
|
|
62
|
+
else if (!/^sha256-[a-f0-9]{64}$/i.test(sha)) {
|
|
63
|
+
failures.push("image sha256 must be sha256-<64 hex>");
|
|
64
|
+
}
|
|
65
|
+
if (!assetHash) {
|
|
66
|
+
failures.push("missing assetHash");
|
|
67
|
+
}
|
|
68
|
+
else if (!/^sha256-[a-f0-9]{64}$/i.test(assetHash)) {
|
|
69
|
+
failures.push("assetHash must be sha256-<64 hex>");
|
|
70
|
+
}
|
|
71
|
+
if (!checkedAt) {
|
|
72
|
+
failures.push("missing checkedAt");
|
|
73
|
+
}
|
|
74
|
+
else if (Number.isNaN(Date.parse(checkedAt))) {
|
|
75
|
+
failures.push("checkedAt must be a valid timestamp");
|
|
76
|
+
}
|
|
77
|
+
if (failures.length > 0) {
|
|
78
|
+
throw new Error(`Aura3D rendered probe metadata failed: ${failures.join("; ")}.`);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
url: url,
|
|
82
|
+
kind: kind,
|
|
83
|
+
renderer,
|
|
84
|
+
route,
|
|
85
|
+
sha256: sha,
|
|
86
|
+
assetHash,
|
|
87
|
+
width,
|
|
88
|
+
height,
|
|
89
|
+
nonBlankPixels,
|
|
90
|
+
colorBuckets,
|
|
91
|
+
checkedAt,
|
|
92
|
+
...(foregroundBounds ? { foregroundBounds } : {})
|
|
93
|
+
};
|
|
94
|
+
}
|
|
10
95
|
export function addAsset(options) {
|
|
11
96
|
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
12
97
|
const sourcePath = resolve(projectDir, options.file);
|
|
@@ -51,12 +136,18 @@ export function addAsset(options) {
|
|
|
51
136
|
humanoid: inspection.humanoid,
|
|
52
137
|
skeleton: inspection.skeleton,
|
|
53
138
|
morphTargets: inspection.morphTargets,
|
|
139
|
+
hierarchy: inspection.hierarchy,
|
|
54
140
|
provenance: createAssetProvenance(projectDir, sourcePath, options, mergeDetectedProvenance(existing?.provenance, inspection.provenance)),
|
|
55
141
|
textures: inspection.textures,
|
|
56
142
|
dependencies: inspection.dependencies,
|
|
57
143
|
orientation: inspection.orientation,
|
|
58
144
|
nodeNames: inspection.nodeNames,
|
|
59
145
|
thumbnailUrl,
|
|
146
|
+
quality: options.quality ?? existing?.quality ?? "ungraded",
|
|
147
|
+
role: options.role ?? existing?.role ?? "unknown",
|
|
148
|
+
...(options.suitabilityReason ?? existing?.suitabilityReason ? { suitabilityReason: options.suitabilityReason ?? existing?.suitabilityReason } : {}),
|
|
149
|
+
...(options.renderedProbe ?? existing?.renderedProbe ? { renderedProbe: options.renderedProbe ?? existing?.renderedProbe } : {}),
|
|
150
|
+
...(options.gameGeometry ?? existing?.gameGeometry ? { gameGeometry: options.gameGeometry ?? existing?.gameGeometry } : {}),
|
|
60
151
|
warnings: createAssetWarnings(sourcePath, inspection)
|
|
61
152
|
};
|
|
62
153
|
const manifest = sortManifest({
|
|
@@ -145,6 +236,9 @@ export function validateAssets(options = {}) {
|
|
|
145
236
|
const sourceManifest = readAssetManifest(projectDir);
|
|
146
237
|
const manifest = filterAssetManifest(sourceManifest, options.assetIds);
|
|
147
238
|
const externalProvenance = readExternalProvenance(projectDir, options.provenanceFile);
|
|
239
|
+
const release = options.release === true;
|
|
240
|
+
const noPlaceholders = options.noPlaceholders === true || release;
|
|
241
|
+
const requireLicense = options.requireLicense === true || release;
|
|
148
242
|
const failures = manifestMissing
|
|
149
243
|
? [`Missing ${DEFAULT_AURA_ASSET_MANIFEST}. Suggested fix: run aura3d assets add ./asset.glb --name product or aura3d assets scan ./assets.`]
|
|
150
244
|
: [];
|
|
@@ -155,6 +249,7 @@ export function validateAssets(options = {}) {
|
|
|
155
249
|
failures.push(`Missing asset provenance evidence file: ${options.provenanceFile}`);
|
|
156
250
|
}
|
|
157
251
|
const warnings = [];
|
|
252
|
+
warnings.push(...createDuplicateHashWarnings(manifest, externalProvenance));
|
|
158
253
|
for (const asset of manifest.assets) {
|
|
159
254
|
const outputPath = resolve(projectDir, asset.outputPath);
|
|
160
255
|
if (!existsSync(outputPath)) {
|
|
@@ -165,13 +260,31 @@ export function validateAssets(options = {}) {
|
|
|
165
260
|
if (actualHash !== asset.hash)
|
|
166
261
|
failures.push(`Hash mismatch for "${asset.id}": expected ${asset.hash}, found ${actualHash}`);
|
|
167
262
|
const provenance = resolveAssetProvenance(asset, externalProvenance);
|
|
168
|
-
if (
|
|
263
|
+
if (noPlaceholders && isPlaceholderAsset(asset, provenance)) {
|
|
169
264
|
failures.push(`Placeholder asset is not allowed in strict release validation: "${asset.id}". Replace it with a real typed asset and provenance.`);
|
|
170
265
|
}
|
|
171
|
-
if (
|
|
266
|
+
if (requireLicense && !hasUsableLicenseEvidence(provenance)) {
|
|
172
267
|
failures.push(`Missing license/provenance evidence for "${asset.id}". Add it with assets add --license ... --source-url ... or pass --provenance <evidence.json>.`);
|
|
173
268
|
}
|
|
174
|
-
|
|
269
|
+
if (release)
|
|
270
|
+
warnings.push(...createDurableReleaseProvenanceWarnings(asset, provenance));
|
|
271
|
+
if (release)
|
|
272
|
+
warnings.push(...createReleaseStructuredQualityWarnings(asset));
|
|
273
|
+
if (release)
|
|
274
|
+
warnings.push(...createReleaseAssetQualityWarnings(asset));
|
|
275
|
+
if (release)
|
|
276
|
+
warnings.push(...createReleaseRenderedProbeWarnings(projectDir, manifest, asset));
|
|
277
|
+
if (release)
|
|
278
|
+
warnings.push(...createManifestOrientationOverrideWarnings(asset));
|
|
279
|
+
if (release)
|
|
280
|
+
warnings.push(...createRoleAwareReleaseQualityWarnings(projectDir, manifest, asset));
|
|
281
|
+
if (release)
|
|
282
|
+
warnings.push(...createReleaseThumbnailWarnings(projectDir, manifest, asset));
|
|
283
|
+
const tempProvenance = createTempProvenanceWarning(asset, provenance);
|
|
284
|
+
if (tempProvenance)
|
|
285
|
+
warnings.push(tempProvenance);
|
|
286
|
+
const storedWarnings = release ? releaseStoredAssetWarnings(asset) : asset.warnings ?? [];
|
|
287
|
+
warnings.push(...storedWarnings.map((warning) => `${asset.id}: ${warning}`));
|
|
175
288
|
if (asset.format === "gltf") {
|
|
176
289
|
for (const dependency of asset.dependencies ?? asset.textures) {
|
|
177
290
|
if (dependency.startsWith("data:"))
|
|
@@ -187,68 +300,26 @@ export function validateAssets(options = {}) {
|
|
|
187
300
|
const typegenPath = resolve(projectDir, manifest.typegen);
|
|
188
301
|
if (!existsSync(typegenPath))
|
|
189
302
|
failures.push(`Missing typed asset module: ${manifest.typegen}. Run assets typegen.`);
|
|
303
|
+
const source = shouldScanSource(options) ? validateAssetSource(projectDir, options.source, sourceManifest) : undefined;
|
|
304
|
+
if (source) {
|
|
305
|
+
failures.push(...source.failures);
|
|
306
|
+
warnings.push(...source.warnings);
|
|
307
|
+
}
|
|
308
|
+
if (release) {
|
|
309
|
+
for (const warning of warnings) {
|
|
310
|
+
failures.push(`Release validation warning is blocking: ${warning}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
190
313
|
return {
|
|
191
314
|
ok: failures.length === 0,
|
|
192
315
|
manifestPath,
|
|
193
316
|
manifest,
|
|
317
|
+
...(source ? { source } : {}),
|
|
194
318
|
failures,
|
|
195
319
|
warnings,
|
|
196
|
-
messages: failures.length === 0 ? ["Asset manifest is valid."] : failures
|
|
320
|
+
messages: failures.length === 0 ? [release ? "Asset manifest is release-valid." : "Asset manifest is valid."] : failures
|
|
197
321
|
};
|
|
198
322
|
}
|
|
199
|
-
export function writeTypedAssets(projectDir, manifest = readAssetManifest(projectDir)) {
|
|
200
|
-
const path = resolve(projectDir, manifest.typegen);
|
|
201
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
202
|
-
const lines = [
|
|
203
|
-
`import { defineAuraAssets } from "@aura3d/engine";`,
|
|
204
|
-
"",
|
|
205
|
-
"export const assets = defineAuraAssets({",
|
|
206
|
-
...manifest.assets.map((asset) => {
|
|
207
|
-
const metadata = {
|
|
208
|
-
materials: asset.materials,
|
|
209
|
-
animations: asset.animations,
|
|
210
|
-
animationClips: asset.animations,
|
|
211
|
-
animationMetadata: asset.animationMetadata ?? createReadinessAnimationMetadata(asset.animations),
|
|
212
|
-
humanoid: asset.humanoid?.humanoid ?? false,
|
|
213
|
-
humanoidStatus: asset.humanoid?.status ?? "unknown",
|
|
214
|
-
humanoidConfidence: asset.humanoid?.confidence ?? "low",
|
|
215
|
-
skeleton: asset.skeleton,
|
|
216
|
-
morphTargets: asset.morphTargets,
|
|
217
|
-
provenance: asset.provenance,
|
|
218
|
-
sourcePath: asset.source,
|
|
219
|
-
outputPath: asset.outputPath,
|
|
220
|
-
license: asset.provenance?.license,
|
|
221
|
-
author: asset.provenance?.author,
|
|
222
|
-
boundsMetadata: asset.boundsMetadata,
|
|
223
|
-
materialMetadata: asset.materialMetadata,
|
|
224
|
-
orientation: asset.orientation,
|
|
225
|
-
nodeNames: asset.nodeNames ?? [],
|
|
226
|
-
textures: asset.textures,
|
|
227
|
-
dependencies: asset.dependencies ?? [],
|
|
228
|
-
thumbnailUrl: asset.thumbnailUrl
|
|
229
|
-
};
|
|
230
|
-
const fields = [
|
|
231
|
-
`type: ${JSON.stringify(asset.type)}`,
|
|
232
|
-
`format: ${JSON.stringify(asset.format)}`,
|
|
233
|
-
`url: ${JSON.stringify(asset.url)}`,
|
|
234
|
-
`hash: ${JSON.stringify(asset.hash)}`,
|
|
235
|
-
`bounds: ${JSON.stringify(asset.bounds ?? [0, 0, 0])}`,
|
|
236
|
-
`sizeBytes: ${asset.sizeBytes}`,
|
|
237
|
-
`metadata: ${JSON.stringify(metadata)}`
|
|
238
|
-
];
|
|
239
|
-
return ` ${JSON.stringify(asset.id)}: { ${fields.join(", ")} },`;
|
|
240
|
-
}),
|
|
241
|
-
"} as const);",
|
|
242
|
-
"",
|
|
243
|
-
"export type AuraGeneratedAssets = typeof assets;",
|
|
244
|
-
""
|
|
245
|
-
];
|
|
246
|
-
writeFileSync(path, lines.join("\n"));
|
|
247
|
-
return path;
|
|
248
|
-
}
|
|
249
|
-
export function listAssets(options = {}) {
|
|
250
|
-
return readAssetManifest(resolve(options.projectDir ?? process.cwd())).assets;
|
|
251
|
-
}
|
|
252
323
|
export function createAssetThumbnails(options = {}) {
|
|
253
324
|
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
254
325
|
const manifest = readAssetManifest(projectDir);
|
|
@@ -281,7 +352,8 @@ export function doctor(options = {}) {
|
|
|
281
352
|
}
|
|
282
353
|
export function checkDeploy(options = {}) {
|
|
283
354
|
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
284
|
-
const
|
|
355
|
+
const sourceManifest = readAssetManifest(projectDir);
|
|
356
|
+
const manifest = filterAssetManifest(sourceManifest, options.assetIds);
|
|
285
357
|
const distDir = normalizeRelativePath(options.distDir ?? "dist");
|
|
286
358
|
const failures = [];
|
|
287
359
|
const warnings = [];
|
|
@@ -294,16 +366,27 @@ export function checkDeploy(options = {}) {
|
|
|
294
366
|
if (!/[a-f0-9]{8}\.[^.]+$/i.test(asset.url))
|
|
295
367
|
warnings.push(`${asset.id}: URL is not fingerprinted: ${asset.url}`);
|
|
296
368
|
}
|
|
297
|
-
const validation = validateAssets({
|
|
369
|
+
const validation = validateAssets({
|
|
370
|
+
projectDir,
|
|
371
|
+
noPlaceholders: options.noPlaceholders,
|
|
372
|
+
requireLicense: options.requireLicense,
|
|
373
|
+
provenanceFile: options.provenanceFile,
|
|
374
|
+
assetIds: options.assetIds,
|
|
375
|
+
source: options.source,
|
|
376
|
+
release: options.release
|
|
377
|
+
});
|
|
298
378
|
failures.push(...validation.failures);
|
|
299
379
|
warnings.push(...validation.warnings);
|
|
300
380
|
return {
|
|
301
381
|
ok: failures.length === 0,
|
|
302
382
|
manifestPath: resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
|
|
303
383
|
manifest,
|
|
384
|
+
...(validation.source ? { source: validation.source } : {}),
|
|
304
385
|
failures,
|
|
305
386
|
warnings,
|
|
306
|
-
messages: failures.length === 0
|
|
387
|
+
messages: failures.length === 0
|
|
388
|
+
? [options.release ? "Deploy check passed with release asset gates." : "Deploy check passed."]
|
|
389
|
+
: failures
|
|
307
390
|
};
|
|
308
391
|
}
|
|
309
392
|
export function validateGameAssets(options = {}) {
|
|
@@ -623,9 +706,9 @@ function validateAssetReadiness(profile, options) {
|
|
|
623
706
|
return report;
|
|
624
707
|
}
|
|
625
708
|
function filterAssetManifest(manifest, assetIds) {
|
|
626
|
-
|
|
627
|
-
if (normalized.length === 0)
|
|
709
|
+
if (assetIds === undefined)
|
|
628
710
|
return manifest;
|
|
711
|
+
const normalized = normalizeAssetIdFilter(assetIds);
|
|
629
712
|
const allowed = new Set(normalized);
|
|
630
713
|
return {
|
|
631
714
|
...manifest,
|
|
@@ -633,6 +716,8 @@ function filterAssetManifest(manifest, assetIds) {
|
|
|
633
716
|
};
|
|
634
717
|
}
|
|
635
718
|
function findMissingAssetIds(manifest, assetIds) {
|
|
719
|
+
if (assetIds === undefined)
|
|
720
|
+
return [];
|
|
636
721
|
const normalized = normalizeAssetIdFilter(assetIds);
|
|
637
722
|
if (normalized.length === 0)
|
|
638
723
|
return [];
|
|
@@ -1139,6 +1224,64 @@ function pushUnique(target, values) {
|
|
|
1139
1224
|
target.push(value);
|
|
1140
1225
|
}
|
|
1141
1226
|
}
|
|
1227
|
+
function createDuplicateHashWarnings(manifest, externalProvenance) {
|
|
1228
|
+
const byHash = new Map();
|
|
1229
|
+
for (const asset of manifest.assets) {
|
|
1230
|
+
const existing = byHash.get(asset.hash) ?? [];
|
|
1231
|
+
existing.push(asset);
|
|
1232
|
+
byHash.set(asset.hash, existing);
|
|
1233
|
+
}
|
|
1234
|
+
const warnings = [];
|
|
1235
|
+
for (const [hash, assets] of byHash) {
|
|
1236
|
+
if (assets.length < 2)
|
|
1237
|
+
continue;
|
|
1238
|
+
const unexplained = assets.filter((asset) => !hasDuplicateHashAllowlist(resolveAssetProvenance(asset, externalProvenance)));
|
|
1239
|
+
if (unexplained.length > 0) {
|
|
1240
|
+
warnings.push(`duplicate asset hash ${hash} used by ${assets.map((asset) => `"${asset.id}"`).join(", ")} without duplicate-ok provenance evidence.`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return warnings;
|
|
1244
|
+
}
|
|
1245
|
+
function hasDuplicateHashAllowlist(provenance) {
|
|
1246
|
+
const text = [
|
|
1247
|
+
provenance?.sourceFamily,
|
|
1248
|
+
provenance?.sourcePath,
|
|
1249
|
+
provenance?.sourcePage,
|
|
1250
|
+
provenance?.downloadUrl,
|
|
1251
|
+
provenance?.sourceUrl,
|
|
1252
|
+
provenance?.license,
|
|
1253
|
+
provenance?.licenseName,
|
|
1254
|
+
provenance?.licenseUrl,
|
|
1255
|
+
provenance?.licenseRaw,
|
|
1256
|
+
provenance?.author,
|
|
1257
|
+
provenance?.attribution,
|
|
1258
|
+
...(provenance?.evidence ?? [])
|
|
1259
|
+
].filter(Boolean).join(" ");
|
|
1260
|
+
return /\b(?:duplicate-ok|intentional-duplicate|duplicate allowed|shared source allowed)\b/i.test(text);
|
|
1261
|
+
}
|
|
1262
|
+
function createTempProvenanceWarning(asset, provenance) {
|
|
1263
|
+
if (!provenance)
|
|
1264
|
+
return undefined;
|
|
1265
|
+
if (hasLocalOnlyProvenanceMarker(provenance))
|
|
1266
|
+
return undefined;
|
|
1267
|
+
const values = [asset.source, provenance.sourcePath, provenance.sourcePage, provenance.downloadUrl, provenance.sourceUrl].filter((value) => Boolean(value));
|
|
1268
|
+
if (!values.some(isTempProvenancePath))
|
|
1269
|
+
return undefined;
|
|
1270
|
+
return `${asset.id}: temp-path provenance is not durable (${values.find(isTempProvenancePath)}). Re-add with durable source/license evidence or mark local-only explicitly.`;
|
|
1271
|
+
}
|
|
1272
|
+
function isTempProvenancePath(value) {
|
|
1273
|
+
return /(?:^|[/\\])(?:var[/\\]folders|tmp|temp|private[/\\]var[/\\]folders)(?:[/\\]|$)/i.test(value) ||
|
|
1274
|
+
/(?:^|[/\\])T[/\\]aura3d-resolve-|aura3d-resolve-/i.test(value);
|
|
1275
|
+
}
|
|
1276
|
+
function hasLocalOnlyProvenanceMarker(provenance) {
|
|
1277
|
+
const text = [
|
|
1278
|
+
provenance.sourceFamily,
|
|
1279
|
+
provenance.license,
|
|
1280
|
+
provenance.attribution,
|
|
1281
|
+
...(provenance.evidence ?? [])
|
|
1282
|
+
].filter(Boolean).join(" ");
|
|
1283
|
+
return /\blocal-only\b/i.test(text);
|
|
1284
|
+
}
|
|
1142
1285
|
function resolvePublicArtifactPath(projectDir, manifest, url) {
|
|
1143
1286
|
if (/^https?:\/\//i.test(url))
|
|
1144
1287
|
return undefined;
|
|
@@ -1159,25 +1302,6 @@ function defaultAttachPoint(slot) {
|
|
|
1159
1302
|
return "spine";
|
|
1160
1303
|
return "root";
|
|
1161
1304
|
}
|
|
1162
|
-
export function readAssetManifest(projectDir) {
|
|
1163
|
-
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
1164
|
-
if (!existsSync(manifestPath)) {
|
|
1165
|
-
return {
|
|
1166
|
-
schema: "aura3d.assets/1.0",
|
|
1167
|
-
assetBasePath: DEFAULT_AURA_ASSET_PUBLIC_PATH,
|
|
1168
|
-
outputDir: DEFAULT_AURA_ASSET_OUTPUT_DIR,
|
|
1169
|
-
typegen: DEFAULT_AURA_ASSET_TYPEGEN,
|
|
1170
|
-
assets: []
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1174
|
-
if (parsed.schema !== "aura3d.assets/1.0")
|
|
1175
|
-
throw new Error(`Unsupported Aura3D asset manifest schema: ${String(parsed.schema)}`);
|
|
1176
|
-
return parsed;
|
|
1177
|
-
}
|
|
1178
|
-
export function writeAssetManifest(projectDir, manifest) {
|
|
1179
|
-
writeFileSync(resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1180
|
-
}
|
|
1181
1305
|
function inspectAssetFile(path, format) {
|
|
1182
1306
|
if (format === "gltf")
|
|
1183
1307
|
return inspectGltf(JSON.parse(readFileSync(path, "utf8")), dirname(path));
|
|
@@ -1191,6 +1315,7 @@ function inspectAssetFile(path, format) {
|
|
|
1191
1315
|
humanoid: unknownHumanoidInspection("Humanoid detection is only available for GLB/glTF model assets."),
|
|
1192
1316
|
skeleton: emptySkeletonInspection("Skeleton detection is only available for GLB/glTF model assets."),
|
|
1193
1317
|
morphTargets: emptyMorphTargetInspection("Morph target detection is only available for GLB/glTF model assets."),
|
|
1318
|
+
hierarchy: emptyHierarchyInspection("Scene hierarchy inspection is only available for GLB/glTF model assets."),
|
|
1194
1319
|
textures: [],
|
|
1195
1320
|
orientation: unknownOrientationInspection(),
|
|
1196
1321
|
nodeNames: [],
|
|
@@ -1233,6 +1358,7 @@ function inspectGltf(json, baseDir) {
|
|
|
1233
1358
|
humanoid: inspectGltfHumanoid(json),
|
|
1234
1359
|
skeleton: inspectGltfSkeleton(json),
|
|
1235
1360
|
morphTargets: inspectGltfMorphTargets(json),
|
|
1361
|
+
hierarchy: inspectGltfHierarchy(json),
|
|
1236
1362
|
provenance: inspectGltfProvenance(json),
|
|
1237
1363
|
textures: (json.images ?? []).map((image, index) => image.uri ?? image.name ?? `image-${index}`),
|
|
1238
1364
|
orientation: inspectGltfOrientation(json),
|
|
@@ -1375,21 +1501,87 @@ function emptyMorphTargetInspection(message) {
|
|
|
1375
1501
|
messages: [message]
|
|
1376
1502
|
};
|
|
1377
1503
|
}
|
|
1504
|
+
function emptyHierarchyInspection(message) {
|
|
1505
|
+
return {
|
|
1506
|
+
nodeCount: 0,
|
|
1507
|
+
meshCount: 0,
|
|
1508
|
+
materialCount: 0,
|
|
1509
|
+
textureCount: 0,
|
|
1510
|
+
animationClipCount: 0,
|
|
1511
|
+
skinCount: 0,
|
|
1512
|
+
morphTargetCount: 0,
|
|
1513
|
+
rootNodeNames: [],
|
|
1514
|
+
maxDepth: 0,
|
|
1515
|
+
messages: [message]
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
function inspectGltfHierarchy(json) {
|
|
1519
|
+
const nodes = json.nodes ?? [];
|
|
1520
|
+
const childIndexes = new Set();
|
|
1521
|
+
for (const node of nodes) {
|
|
1522
|
+
for (const child of node.children ?? []) {
|
|
1523
|
+
if (child >= 0 && child < nodes.length)
|
|
1524
|
+
childIndexes.add(child);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const sceneRoots = json.scenes?.[json.scene ?? 0]?.nodes?.filter((nodeIndex) => nodeIndex >= 0 && nodeIndex < nodes.length);
|
|
1528
|
+
const rootIndexes = sceneRoots && sceneRoots.length > 0
|
|
1529
|
+
? sceneRoots
|
|
1530
|
+
: nodes.map((_, index) => index).filter((index) => !childIndexes.has(index));
|
|
1531
|
+
const rootNodeNames = rootIndexes.map((nodeIndex) => nodes[nodeIndex]?.name ?? `node-${nodeIndex}`);
|
|
1532
|
+
return {
|
|
1533
|
+
nodeCount: nodes.length,
|
|
1534
|
+
meshCount: json.meshes?.length ?? 0,
|
|
1535
|
+
materialCount: json.materials?.length ?? 0,
|
|
1536
|
+
textureCount: json.images?.length ?? 0,
|
|
1537
|
+
animationClipCount: json.animations?.length ?? 0,
|
|
1538
|
+
skinCount: json.skins?.length ?? 0,
|
|
1539
|
+
morphTargetCount: inspectGltfMorphTargets(json).targetCount,
|
|
1540
|
+
rootNodeNames,
|
|
1541
|
+
maxDepth: rootIndexes.reduce((depth, nodeIndex) => Math.max(depth, measureGltfNodeDepth(nodes, nodeIndex, new Set())), 0),
|
|
1542
|
+
messages: nodes.length === 0
|
|
1543
|
+
? ["No glTF scene nodes detected."]
|
|
1544
|
+
: [`Detected ${nodes.length} node${nodes.length === 1 ? "" : "s"} across ${rootIndexes.length} root${rootIndexes.length === 1 ? "" : "s"}.`]
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
function measureGltfNodeDepth(nodes, nodeIndex, visited) {
|
|
1548
|
+
if (visited.has(nodeIndex))
|
|
1549
|
+
return 0;
|
|
1550
|
+
visited.add(nodeIndex);
|
|
1551
|
+
const children = nodes[nodeIndex]?.children ?? [];
|
|
1552
|
+
if (children.length === 0)
|
|
1553
|
+
return 1;
|
|
1554
|
+
return 1 + children.reduce((depth, childIndex) => {
|
|
1555
|
+
if (childIndex < 0 || childIndex >= nodes.length)
|
|
1556
|
+
return depth;
|
|
1557
|
+
return Math.max(depth, measureGltfNodeDepth(nodes, childIndex, new Set(visited)));
|
|
1558
|
+
}, 0);
|
|
1559
|
+
}
|
|
1378
1560
|
function inspectGltfProvenance(json) {
|
|
1379
1561
|
const assetExtras = objectValue(json.asset?.extras);
|
|
1380
1562
|
const auraExtras = objectValue(assetExtras?.aura3d) ?? assetExtras;
|
|
1381
1563
|
const provenance = objectValue(auraExtras?.provenance ?? auraExtras?.license ?? auraExtras?.source);
|
|
1564
|
+
const sourcePage = stringValue(provenance?.sourcePage ?? provenance?.page ?? auraExtras?.sourcePage);
|
|
1565
|
+
const downloadUrl = stringValue(provenance?.downloadUrl ?? provenance?.download ?? auraExtras?.downloadUrl);
|
|
1382
1566
|
const sourceUrl = stringValue(provenance?.sourceUrl ?? provenance?.url ?? auraExtras?.sourceUrl);
|
|
1383
1567
|
const license = stringValue(provenance?.license ?? provenance?.spdx ?? auraExtras?.license);
|
|
1568
|
+
const licenseName = stringValue(provenance?.licenseName ?? provenance?.licenseTitle ?? auraExtras?.licenseName);
|
|
1569
|
+
const licenseUrl = stringValue(provenance?.licenseUrl ?? provenance?.licensePage ?? auraExtras?.licenseUrl);
|
|
1570
|
+
const licenseRaw = stringValue(provenance?.licenseRaw ?? provenance?.rawLicense ?? auraExtras?.licenseRaw);
|
|
1384
1571
|
const author = stringValue(provenance?.author ?? provenance?.creator ?? auraExtras?.author);
|
|
1385
1572
|
const sourceFamily = stringValue(provenance?.sourceFamily ?? provenance?.source ?? auraExtras?.sourceFamily);
|
|
1386
1573
|
const attribution = stringValue(provenance?.attribution ?? auraExtras?.attribution);
|
|
1387
1574
|
const evidence = stringArrayValue(provenance?.evidence ?? auraExtras?.evidence);
|
|
1388
|
-
if (!sourceUrl && !license && !author && !sourceFamily && !attribution && evidence.length === 0)
|
|
1575
|
+
if (!sourcePage && !downloadUrl && !sourceUrl && !license && !licenseName && !licenseUrl && !licenseRaw && !author && !sourceFamily && !attribution && evidence.length === 0)
|
|
1389
1576
|
return undefined;
|
|
1390
1577
|
return {
|
|
1578
|
+
...(sourcePage ? { sourcePage } : {}),
|
|
1579
|
+
...(downloadUrl ? { downloadUrl } : {}),
|
|
1391
1580
|
...(sourceUrl ? { sourceUrl } : {}),
|
|
1392
1581
|
...(license ? { license } : {}),
|
|
1582
|
+
...(licenseName ? { licenseName } : {}),
|
|
1583
|
+
...(licenseUrl ? { licenseUrl } : {}),
|
|
1584
|
+
...(licenseRaw ? { licenseRaw } : {}),
|
|
1393
1585
|
...(author ? { author } : {}),
|
|
1394
1586
|
...(sourceFamily ? { sourceFamily } : {}),
|
|
1395
1587
|
...(attribution ? { attribution } : {}),
|
|
@@ -1534,6 +1726,46 @@ function numberValue(value) {
|
|
|
1534
1726
|
const number = Number(value);
|
|
1535
1727
|
return Number.isFinite(number) ? number : undefined;
|
|
1536
1728
|
}
|
|
1729
|
+
function readPositiveInteger(record, field, failures) {
|
|
1730
|
+
const value = record?.[field];
|
|
1731
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || Math.floor(value) !== value) {
|
|
1732
|
+
failures.push(`missing or invalid ${field}`);
|
|
1733
|
+
return 0;
|
|
1734
|
+
}
|
|
1735
|
+
return value;
|
|
1736
|
+
}
|
|
1737
|
+
function readNonNegativeInteger(record, field, failures, prefix) {
|
|
1738
|
+
const value = record?.[field];
|
|
1739
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || Math.floor(value) !== value) {
|
|
1740
|
+
failures.push(`missing or invalid ${prefix}.${field}`);
|
|
1741
|
+
return 0;
|
|
1742
|
+
}
|
|
1743
|
+
return value;
|
|
1744
|
+
}
|
|
1745
|
+
function readPositiveIntegerWithPrefix(record, field, failures, prefix) {
|
|
1746
|
+
const value = record?.[field];
|
|
1747
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || Math.floor(value) !== value) {
|
|
1748
|
+
failures.push(`missing or invalid ${prefix}.${field}`);
|
|
1749
|
+
return 0;
|
|
1750
|
+
}
|
|
1751
|
+
return value;
|
|
1752
|
+
}
|
|
1753
|
+
function readOptionalForegroundBounds(record, failures) {
|
|
1754
|
+
if (!record || !("foregroundBounds" in record))
|
|
1755
|
+
return undefined;
|
|
1756
|
+
const foregroundBounds = objectValue(record.foregroundBounds);
|
|
1757
|
+
if (!foregroundBounds) {
|
|
1758
|
+
failures.push("foregroundBounds must be an object when present");
|
|
1759
|
+
return undefined;
|
|
1760
|
+
}
|
|
1761
|
+
const x = readNonNegativeInteger(foregroundBounds, "x", failures, "foregroundBounds");
|
|
1762
|
+
const y = readNonNegativeInteger(foregroundBounds, "y", failures, "foregroundBounds");
|
|
1763
|
+
const width = readPositiveIntegerWithPrefix(foregroundBounds, "width", failures, "foregroundBounds");
|
|
1764
|
+
const height = readPositiveIntegerWithPrefix(foregroundBounds, "height", failures, "foregroundBounds");
|
|
1765
|
+
if (failures.some((failure) => failure.includes("foregroundBounds")))
|
|
1766
|
+
return undefined;
|
|
1767
|
+
return { x, y, width, height };
|
|
1768
|
+
}
|
|
1537
1769
|
function createAssetWarnings(path, inspection) {
|
|
1538
1770
|
const warnings = [];
|
|
1539
1771
|
const size = statSync(path).size;
|
|
@@ -1552,13 +1784,19 @@ function createAssetWarnings(path, inspection) {
|
|
|
1552
1784
|
function createAssetProvenance(projectDir, sourcePath, options, detected) {
|
|
1553
1785
|
return {
|
|
1554
1786
|
sourcePath: normalizeRelativePath(relative(projectDir, sourcePath)),
|
|
1787
|
+
...(options.sourcePage ?? detected?.sourcePage ? { sourcePage: options.sourcePage ?? detected?.sourcePage } : {}),
|
|
1788
|
+
...(options.downloadUrl ?? detected?.downloadUrl ? { downloadUrl: options.downloadUrl ?? detected?.downloadUrl } : {}),
|
|
1555
1789
|
...(options.sourceUrl ?? detected?.sourceUrl ? { sourceUrl: options.sourceUrl ?? detected?.sourceUrl } : {}),
|
|
1556
1790
|
...(options.license ?? detected?.license ? { license: options.license ?? detected?.license } : {}),
|
|
1791
|
+
...(options.licenseName ?? detected?.licenseName ? { licenseName: options.licenseName ?? detected?.licenseName } : {}),
|
|
1792
|
+
...(options.licenseUrl ?? detected?.licenseUrl ? { licenseUrl: options.licenseUrl ?? detected?.licenseUrl } : {}),
|
|
1793
|
+
...(options.licenseRaw ?? detected?.licenseRaw ? { licenseRaw: options.licenseRaw ?? detected?.licenseRaw } : {}),
|
|
1557
1794
|
...(options.author ?? detected?.author ? { author: options.author ?? detected?.author } : {}),
|
|
1558
1795
|
...(options.sourceFamily ?? detected?.sourceFamily ? { sourceFamily: options.sourceFamily ?? detected?.sourceFamily } : {}),
|
|
1559
1796
|
...(options.attribution ?? detected?.attribution ? { attribution: options.attribution ?? detected?.attribution } : {}),
|
|
1560
1797
|
...(options.sha256 ?? detected?.sha256 ? { sha256: options.sha256 ?? detected?.sha256 } : {}),
|
|
1561
1798
|
...(options.retrievedAt ?? detected?.retrievedAt ? { retrievedAt: options.retrievedAt ?? detected?.retrievedAt } : {}),
|
|
1799
|
+
...(options.resolveCandidate ?? detected?.resolveCandidate ? { resolveCandidate: options.resolveCandidate ?? detected?.resolveCandidate } : {}),
|
|
1562
1800
|
...(detected?.evidence && detected.evidence.length > 0 ? { evidence: detected.evidence } : {}),
|
|
1563
1801
|
checkedAt: new Date().toISOString()
|
|
1564
1802
|
};
|
|
@@ -1566,23 +1804,30 @@ function createAssetProvenance(projectDir, sourcePath, options, detected) {
|
|
|
1566
1804
|
/**
|
|
1567
1805
|
* Merge a freshly inspected asset's auto-detected provenance over the entry that
|
|
1568
1806
|
* already exists in the manifest, so a re-add/re-resolve preserves hand-authored
|
|
1569
|
-
* fields (license, attribution, author,
|
|
1570
|
-
* sha256) that the new pass did
|
|
1571
|
-
*
|
|
1572
|
-
* {@link
|
|
1807
|
+
* fields (license, license URL/name/raw, attribution, author, source page,
|
|
1808
|
+
* download URL, sourceUrl, sourceFamily, evidence, sha256) that the new pass did
|
|
1809
|
+
* not re-detect. Freshly detected values win over the prior entry; explicit
|
|
1810
|
+
* `addAsset` options still win over both (applied in {@link
|
|
1811
|
+
* createAssetProvenance}). (#26)
|
|
1573
1812
|
*/
|
|
1574
1813
|
function mergeDetectedProvenance(existing, detected) {
|
|
1575
1814
|
if (!existing)
|
|
1576
1815
|
return detected;
|
|
1577
1816
|
const evidence = [...(detected?.evidence ?? []), ...(existing.evidence ?? [])];
|
|
1578
1817
|
return {
|
|
1818
|
+
sourcePage: detected?.sourcePage ?? existing.sourcePage,
|
|
1819
|
+
downloadUrl: detected?.downloadUrl ?? existing.downloadUrl,
|
|
1579
1820
|
sourceUrl: detected?.sourceUrl ?? existing.sourceUrl,
|
|
1580
1821
|
license: detected?.license ?? existing.license,
|
|
1822
|
+
licenseName: detected?.licenseName ?? existing.licenseName,
|
|
1823
|
+
licenseUrl: detected?.licenseUrl ?? existing.licenseUrl,
|
|
1824
|
+
licenseRaw: detected?.licenseRaw ?? existing.licenseRaw,
|
|
1581
1825
|
author: detected?.author ?? existing.author,
|
|
1582
1826
|
sourceFamily: detected?.sourceFamily ?? existing.sourceFamily,
|
|
1583
1827
|
attribution: detected?.attribution ?? existing.attribution,
|
|
1584
1828
|
sha256: detected?.sha256 ?? existing.sha256,
|
|
1585
1829
|
retrievedAt: detected?.retrievedAt ?? existing.retrievedAt,
|
|
1830
|
+
resolveCandidate: detected?.resolveCandidate ?? existing.resolveCandidate,
|
|
1586
1831
|
...(evidence.length > 0 ? { evidence: [...new Set(evidence)] } : {})
|
|
1587
1832
|
};
|
|
1588
1833
|
}
|
|
@@ -1611,6 +1856,11 @@ function readExternalProvenance(projectDir, provenanceFile) {
|
|
|
1611
1856
|
const nestedProvenance = objectValue(record.provenance);
|
|
1612
1857
|
const sourcePath = stringValue(record.sourcePath ?? record.source ?? nestedProvenance?.builderOutput) ?? id;
|
|
1613
1858
|
const license = stringValue(record.license ?? record.licenseNote ?? nestedProvenance?.license);
|
|
1859
|
+
const licenseName = stringValue(record.licenseName ?? record.licenseTitle ?? nestedProvenance?.licenseName);
|
|
1860
|
+
const licenseUrl = stringValue(record.licenseUrl ?? record.licensePage ?? record.termsUrl ?? nestedProvenance?.licenseUrl);
|
|
1861
|
+
const licenseRaw = stringValue(record.licenseRaw ?? record.rawLicense ?? nestedProvenance?.licenseRaw);
|
|
1862
|
+
const sourcePage = stringValue(record.sourcePage ?? record.officialPage ?? nestedProvenance?.sourcePage);
|
|
1863
|
+
const downloadUrl = stringValue(record.downloadUrl ?? record.publicUrl ?? nestedProvenance?.downloadUrl);
|
|
1614
1864
|
const sourceUrl = stringValue(record.sourceUrl ?? record.publicUrl ?? record.officialPage ?? nestedProvenance?.sourceUrl);
|
|
1615
1865
|
const sourceFamily = stringValue(record.sourceFamily ?? nestedProvenance?.sourceFamily ?? nestedProvenance?.sourcePack);
|
|
1616
1866
|
const author = stringValue(record.author ?? nestedProvenance?.author);
|
|
@@ -1624,8 +1874,13 @@ function readExternalProvenance(projectDir, provenanceFile) {
|
|
|
1624
1874
|
];
|
|
1625
1875
|
byId.set(id, {
|
|
1626
1876
|
sourcePath,
|
|
1877
|
+
...(sourcePage ? { sourcePage } : {}),
|
|
1878
|
+
...(downloadUrl ? { downloadUrl } : {}),
|
|
1627
1879
|
...(sourceUrl ? { sourceUrl } : {}),
|
|
1628
1880
|
...(license ? { license } : {}),
|
|
1881
|
+
...(licenseName ? { licenseName } : {}),
|
|
1882
|
+
...(licenseUrl ? { licenseUrl } : {}),
|
|
1883
|
+
...(licenseRaw ? { licenseRaw } : {}),
|
|
1629
1884
|
...(author ? { author } : {}),
|
|
1630
1885
|
...(sourceFamily ? { sourceFamily } : {}),
|
|
1631
1886
|
...(attribution ? { attribution } : {}),
|
|
@@ -1651,6 +1906,609 @@ function hasUsableLicenseEvidence(provenance) {
|
|
|
1651
1906
|
return false;
|
|
1652
1907
|
return !/(unverified|unknown|candidate|needs[-\s]?confirmation|todo|placeholder)/i.test(license);
|
|
1653
1908
|
}
|
|
1909
|
+
function createDurableReleaseProvenanceWarnings(asset, provenance) {
|
|
1910
|
+
if (!provenance)
|
|
1911
|
+
return [`${asset.id}: durable provenance is missing; add source page, download URL, license URL/name, author, and acquisition timestamp.`];
|
|
1912
|
+
if (hasLocalOnlyProvenanceMarker(provenance))
|
|
1913
|
+
return [];
|
|
1914
|
+
const warnings = [];
|
|
1915
|
+
if (!nonEmpty(provenance.sourcePage))
|
|
1916
|
+
warnings.push(`${asset.id}: durable provenance missing sourcePage.`);
|
|
1917
|
+
if (!nonEmpty(provenance.downloadUrl))
|
|
1918
|
+
warnings.push(`${asset.id}: durable provenance missing downloadUrl.`);
|
|
1919
|
+
if (!nonEmpty(provenance.licenseName) && !nonEmpty(provenance.license))
|
|
1920
|
+
warnings.push(`${asset.id}: durable provenance missing licenseName.`);
|
|
1921
|
+
if (!nonEmpty(provenance.licenseUrl))
|
|
1922
|
+
warnings.push(`${asset.id}: durable provenance missing licenseUrl.`);
|
|
1923
|
+
if (!nonEmpty(provenance.author) && !nonEmpty(provenance.attribution))
|
|
1924
|
+
warnings.push(`${asset.id}: durable provenance missing author or attribution.`);
|
|
1925
|
+
if (!nonEmpty(provenance.retrievedAt))
|
|
1926
|
+
warnings.push(`${asset.id}: durable provenance missing acquisition timestamp (retrievedAt).`);
|
|
1927
|
+
return warnings;
|
|
1928
|
+
}
|
|
1929
|
+
function createReleaseStructuredQualityWarnings(asset) {
|
|
1930
|
+
if (asset.type !== "model")
|
|
1931
|
+
return [];
|
|
1932
|
+
const warnings = [];
|
|
1933
|
+
const quality = asset.quality ?? "ungraded";
|
|
1934
|
+
const role = asset.role ?? "unknown";
|
|
1935
|
+
if (quality === "ungraded") {
|
|
1936
|
+
warnings.push(`${asset.id}: release primary model quality grade missing or ungraded.`);
|
|
1937
|
+
}
|
|
1938
|
+
else if (quality !== "release") {
|
|
1939
|
+
warnings.push(`${asset.id}: release primary model quality grade "${quality}" is not release-safe.`);
|
|
1940
|
+
}
|
|
1941
|
+
if (role === "unknown") {
|
|
1942
|
+
warnings.push(`${asset.id}: release primary model missing intended role.`);
|
|
1943
|
+
}
|
|
1944
|
+
if (!nonEmpty(asset.suitabilityReason)) {
|
|
1945
|
+
warnings.push(`${asset.id}: release primary model missing suitability reason.`);
|
|
1946
|
+
}
|
|
1947
|
+
return warnings;
|
|
1948
|
+
}
|
|
1949
|
+
function createReleaseAssetQualityWarnings(asset) {
|
|
1950
|
+
if (asset.type !== "model")
|
|
1951
|
+
return [];
|
|
1952
|
+
const warnings = [];
|
|
1953
|
+
const size = asset.boundsMetadata?.size ?? asset.bounds;
|
|
1954
|
+
if (!size) {
|
|
1955
|
+
warnings.push(`${asset.id}: release primary model quality missing bounds metadata.`);
|
|
1956
|
+
}
|
|
1957
|
+
else {
|
|
1958
|
+
const maxDimension = Math.max(...size);
|
|
1959
|
+
const minNonZeroDimension = Math.min(...size.filter((value) => value > 0));
|
|
1960
|
+
if (maxDimension < 0.25)
|
|
1961
|
+
warnings.push(`${asset.id}: release primary model is too small to be a readable subject (max bounds ${maxDimension.toFixed(3)}).`);
|
|
1962
|
+
if (maxDimension > 250 && !hasHashBoundNormalizationEvidence(asset)) {
|
|
1963
|
+
warnings.push(`${asset.id}: release primary model has excessive scale mismatch (max bounds ${maxDimension.toFixed(3)}).`);
|
|
1964
|
+
}
|
|
1965
|
+
if (Number.isFinite(minNonZeroDimension) && maxDimension / minNonZeroDimension > 250) {
|
|
1966
|
+
warnings.push(`${asset.id}: release primary model has extreme aspect/scale mismatch.`);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
if (asset.materials.length === 0)
|
|
1970
|
+
warnings.push(`${asset.id}: release primary model has no material metadata.`);
|
|
1971
|
+
if (asset.materialMetadata?.some((material) => !material.visible || !material.readable)) {
|
|
1972
|
+
warnings.push(`${asset.id}: release primary model has invisible or unreadable material metadata.`);
|
|
1973
|
+
}
|
|
1974
|
+
if (["glb", "gltf"].includes(asset.format) && asset.textures.length === 0) {
|
|
1975
|
+
warnings.push(`${asset.id}: release primary model has no texture references; use only with explicit material/readability evidence.`);
|
|
1976
|
+
}
|
|
1977
|
+
return warnings;
|
|
1978
|
+
}
|
|
1979
|
+
function createReleaseRenderedProbeWarnings(projectDir, manifest, asset) {
|
|
1980
|
+
if (asset.type !== "model")
|
|
1981
|
+
return [];
|
|
1982
|
+
const renderedProbe = asset.renderedProbe;
|
|
1983
|
+
if (!renderedProbe?.url)
|
|
1984
|
+
return [`${asset.id}: release primary model missing renderedProbe evidence.`];
|
|
1985
|
+
const allowedKinds = ["browser-screenshot", "aura-probe-render"];
|
|
1986
|
+
const warnings = [];
|
|
1987
|
+
if (!allowedKinds.includes(renderedProbe.kind)) {
|
|
1988
|
+
warnings.push(`${asset.id}: release primary model renderedProbe kind "${renderedProbe.kind}" is not pixel-render proof.`);
|
|
1989
|
+
}
|
|
1990
|
+
if (!nonEmpty(renderedProbe.renderer)) {
|
|
1991
|
+
warnings.push(`${asset.id}: release primary model renderedProbe missing renderer metadata.`);
|
|
1992
|
+
}
|
|
1993
|
+
else if (!/createAuraApp|@aura3d\/engine/i.test(renderedProbe.renderer ?? "")) {
|
|
1994
|
+
warnings.push(`${asset.id}: release primary model renderedProbe renderer "${renderedProbe.renderer}" is not root Aura3D render proof.`);
|
|
1995
|
+
}
|
|
1996
|
+
if (!nonEmpty(renderedProbe.route))
|
|
1997
|
+
warnings.push(`${asset.id}: release primary model renderedProbe missing route metadata.`);
|
|
1998
|
+
if (!nonEmpty(renderedProbe.checkedAt)) {
|
|
1999
|
+
warnings.push(`${asset.id}: release primary model renderedProbe missing checkedAt timestamp.`);
|
|
2000
|
+
}
|
|
2001
|
+
else if (Number.isNaN(Date.parse(renderedProbe.checkedAt ?? ""))) {
|
|
2002
|
+
warnings.push(`${asset.id}: release primary model renderedProbe checkedAt timestamp is invalid.`);
|
|
2003
|
+
}
|
|
2004
|
+
if (!nonEmpty(renderedProbe.sha256))
|
|
2005
|
+
warnings.push(`${asset.id}: release primary model renderedProbe missing image sha256.`);
|
|
2006
|
+
if (!nonEmpty(renderedProbe.assetHash))
|
|
2007
|
+
warnings.push(`${asset.id}: release primary model renderedProbe missing asset hash binding.`);
|
|
2008
|
+
if (typeof renderedProbe.width !== "number" || typeof renderedProbe.height !== "number") {
|
|
2009
|
+
warnings.push(`${asset.id}: release primary model renderedProbe missing declared dimensions.`);
|
|
2010
|
+
}
|
|
2011
|
+
if (typeof renderedProbe.nonBlankPixels !== "number") {
|
|
2012
|
+
warnings.push(`${asset.id}: release primary model renderedProbe missing nonblank pixel count.`);
|
|
2013
|
+
}
|
|
2014
|
+
if (typeof renderedProbe.colorBuckets !== "number") {
|
|
2015
|
+
warnings.push(`${asset.id}: release primary model renderedProbe missing color bucket count.`);
|
|
2016
|
+
}
|
|
2017
|
+
const probePath = resolvePublicArtifactPath(projectDir, manifest, renderedProbe.url);
|
|
2018
|
+
if (!probePath || !existsSync(probePath)) {
|
|
2019
|
+
warnings.push(`${asset.id}: release primary model renderedProbe artifact is missing on disk (${renderedProbe.url}).`);
|
|
2020
|
+
return warnings;
|
|
2021
|
+
}
|
|
2022
|
+
const probeBytes = readFileSync(probePath);
|
|
2023
|
+
const size = probeBytes.byteLength;
|
|
2024
|
+
if (size < 200)
|
|
2025
|
+
warnings.push(`${asset.id}: release primary model renderedProbe artifact is too small (${size} bytes).`);
|
|
2026
|
+
const actualSha256 = `sha256-${createHash("sha256").update(probeBytes).digest("hex")}`;
|
|
2027
|
+
if (renderedProbe.sha256 && renderedProbe.sha256 !== actualSha256) {
|
|
2028
|
+
warnings.push(`${asset.id}: release primary model renderedProbe image sha256 mismatch.`);
|
|
2029
|
+
}
|
|
2030
|
+
if (renderedProbe.assetHash && renderedProbe.assetHash !== asset.hash) {
|
|
2031
|
+
warnings.push(`${asset.id}: release primary model renderedProbe asset hash binding is stale.`);
|
|
2032
|
+
}
|
|
2033
|
+
const probeMetrics = decodeRenderedProbePng(probeBytes);
|
|
2034
|
+
if (!probeMetrics.ok) {
|
|
2035
|
+
warnings.push(`${asset.id}: release primary model renderedProbe artifact is not PNG screenshot proof (${probeMetrics.reason}).`);
|
|
2036
|
+
return warnings;
|
|
2037
|
+
}
|
|
2038
|
+
if (probeMetrics.width < 320 || probeMetrics.height < 180) {
|
|
2039
|
+
warnings.push(`${asset.id}: release primary model renderedProbe dimensions are too small (${probeMetrics.width}x${probeMetrics.height}).`);
|
|
2040
|
+
}
|
|
2041
|
+
if (typeof renderedProbe.width === "number" && renderedProbe.width !== probeMetrics.width) {
|
|
2042
|
+
warnings.push(`${asset.id}: release primary model renderedProbe declared width ${renderedProbe.width} does not match PNG width ${probeMetrics.width}.`);
|
|
2043
|
+
}
|
|
2044
|
+
if (typeof renderedProbe.height === "number" && renderedProbe.height !== probeMetrics.height) {
|
|
2045
|
+
warnings.push(`${asset.id}: release primary model renderedProbe declared height ${renderedProbe.height} does not match PNG height ${probeMetrics.height}.`);
|
|
2046
|
+
}
|
|
2047
|
+
const minNonBlankPixels = Math.min(5000, Math.max(500, Math.floor(probeMetrics.width * probeMetrics.height * 0.005)));
|
|
2048
|
+
if (probeMetrics.nonBlankPixels < minNonBlankPixels) {
|
|
2049
|
+
warnings.push(`${asset.id}: release primary model renderedProbe appears blank (${probeMetrics.nonBlankPixels} nonblank pixels).`);
|
|
2050
|
+
}
|
|
2051
|
+
if (probeMetrics.colorBuckets < 3) {
|
|
2052
|
+
warnings.push(`${asset.id}: release primary model renderedProbe lacks visible color variation (${probeMetrics.colorBuckets} color buckets).`);
|
|
2053
|
+
}
|
|
2054
|
+
if (typeof renderedProbe.nonBlankPixels === "number" && renderedProbe.nonBlankPixels !== probeMetrics.nonBlankPixels) {
|
|
2055
|
+
warnings.push(`${asset.id}: release primary model renderedProbe nonblank pixel count is stale.`);
|
|
2056
|
+
}
|
|
2057
|
+
if (typeof renderedProbe.colorBuckets === "number" && renderedProbe.colorBuckets !== probeMetrics.colorBuckets) {
|
|
2058
|
+
warnings.push(`${asset.id}: release primary model renderedProbe color bucket count is stale.`);
|
|
2059
|
+
}
|
|
2060
|
+
warnings.push(...createRenderedProbeForegroundWarnings(asset.id, renderedProbe.foregroundBounds, probeMetrics));
|
|
2061
|
+
return warnings;
|
|
2062
|
+
}
|
|
2063
|
+
function createManifestOrientationOverrideWarnings(asset) {
|
|
2064
|
+
const orientation = asset.orientation;
|
|
2065
|
+
if (asset.type !== "model" || orientation?.source !== "manifest-override")
|
|
2066
|
+
return [];
|
|
2067
|
+
const warnings = [];
|
|
2068
|
+
if (!nonEmpty(orientation.assetHash)) {
|
|
2069
|
+
warnings.push(`${asset.id}: manifest orientation override missing asset hash binding.`);
|
|
2070
|
+
}
|
|
2071
|
+
else if (orientation.assetHash !== asset.hash) {
|
|
2072
|
+
warnings.push(`${asset.id}: manifest orientation override asset hash binding is stale.`);
|
|
2073
|
+
}
|
|
2074
|
+
if (!nonEmpty(orientation.generatedBy)) {
|
|
2075
|
+
warnings.push(`${asset.id}: manifest orientation override missing generatedBy provenance.`);
|
|
2076
|
+
}
|
|
2077
|
+
if (!nonEmpty(orientation.checkedAt)) {
|
|
2078
|
+
warnings.push(`${asset.id}: manifest orientation override missing checkedAt timestamp.`);
|
|
2079
|
+
}
|
|
2080
|
+
else if (Number.isNaN(Date.parse(orientation.checkedAt ?? ""))) {
|
|
2081
|
+
warnings.push(`${asset.id}: manifest orientation override checkedAt timestamp is invalid.`);
|
|
2082
|
+
}
|
|
2083
|
+
if (!nonEmpty(orientation.route)) {
|
|
2084
|
+
warnings.push(`${asset.id}: manifest orientation override missing route evidence.`);
|
|
2085
|
+
}
|
|
2086
|
+
const probe = orientation.renderedProbe;
|
|
2087
|
+
if (!probe) {
|
|
2088
|
+
warnings.push(`${asset.id}: manifest orientation override missing renderedProbe binding.`);
|
|
2089
|
+
}
|
|
2090
|
+
else {
|
|
2091
|
+
if (!nonEmpty(probe.url))
|
|
2092
|
+
warnings.push(`${asset.id}: manifest orientation override renderedProbe missing url.`);
|
|
2093
|
+
if (!nonEmpty(probe.sha256)) {
|
|
2094
|
+
warnings.push(`${asset.id}: manifest orientation override renderedProbe missing sha256.`);
|
|
2095
|
+
}
|
|
2096
|
+
else if (asset.renderedProbe?.sha256 && probe.sha256 !== asset.renderedProbe.sha256) {
|
|
2097
|
+
warnings.push(`${asset.id}: manifest orientation override renderedProbe sha256 does not match release renderedProbe.`);
|
|
2098
|
+
}
|
|
2099
|
+
if (!nonEmpty(probe.assetHash)) {
|
|
2100
|
+
warnings.push(`${asset.id}: manifest orientation override renderedProbe missing asset hash binding.`);
|
|
2101
|
+
}
|
|
2102
|
+
else if (probe.assetHash !== asset.hash) {
|
|
2103
|
+
warnings.push(`${asset.id}: manifest orientation override renderedProbe asset hash binding is stale.`);
|
|
2104
|
+
}
|
|
2105
|
+
if (!nonEmpty(probe.checkedAt)) {
|
|
2106
|
+
warnings.push(`${asset.id}: manifest orientation override renderedProbe missing checkedAt timestamp.`);
|
|
2107
|
+
}
|
|
2108
|
+
else if (Number.isNaN(Date.parse(probe.checkedAt ?? ""))) {
|
|
2109
|
+
warnings.push(`${asset.id}: manifest orientation override renderedProbe checkedAt timestamp is invalid.`);
|
|
2110
|
+
}
|
|
2111
|
+
if (!nonEmpty(probe.route)) {
|
|
2112
|
+
warnings.push(`${asset.id}: manifest orientation override renderedProbe missing route evidence.`);
|
|
2113
|
+
}
|
|
2114
|
+
else if (asset.renderedProbe?.route && probe.route !== asset.renderedProbe.route) {
|
|
2115
|
+
warnings.push(`${asset.id}: manifest orientation override renderedProbe route does not match release renderedProbe.`);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
const role = asset.role ?? "unknown";
|
|
2119
|
+
const hasAxisEvidence = nonEmpty(orientation.forwardAxis) && nonEmpty(orientation.upAxis);
|
|
2120
|
+
if (requiresForwardOrientation(role) && !hasAxisEvidence) {
|
|
2121
|
+
warnings.push(`${asset.id}: manifest orientation override for ${role} must include forwardAxis and upAxis.`);
|
|
2122
|
+
}
|
|
2123
|
+
if (!requiresForwardOrientation(role) && !hasAxisEvidence && !nonEmpty(orientation.view)) {
|
|
2124
|
+
warnings.push(`${asset.id}: manifest orientation override must include a role-specific view or forward/up axes.`);
|
|
2125
|
+
}
|
|
2126
|
+
return warnings;
|
|
2127
|
+
}
|
|
2128
|
+
function hasValidManifestOrientationOverride(asset) {
|
|
2129
|
+
return createManifestOrientationOverrideWarnings(asset).length === 0 &&
|
|
2130
|
+
asset.orientation?.source === "manifest-override";
|
|
2131
|
+
}
|
|
2132
|
+
function releaseStoredAssetWarnings(asset) {
|
|
2133
|
+
const warnings = asset.warnings ?? [];
|
|
2134
|
+
if (!hasValidManifestOrientationOverride(asset))
|
|
2135
|
+
return warnings;
|
|
2136
|
+
return warnings.filter((warning) => !/orientation metadata missing; facing direction cannot be validated/i.test(warning));
|
|
2137
|
+
}
|
|
2138
|
+
function createRenderedProbeForegroundWarnings(assetId, foregroundBounds, probeMetrics) {
|
|
2139
|
+
if (!foregroundBounds)
|
|
2140
|
+
return [];
|
|
2141
|
+
const warnings = [];
|
|
2142
|
+
const x = Number(foregroundBounds.x);
|
|
2143
|
+
const y = Number(foregroundBounds.y);
|
|
2144
|
+
const width = Number(foregroundBounds.width);
|
|
2145
|
+
const height = Number(foregroundBounds.height);
|
|
2146
|
+
if (!Number.isFinite(x) || x < 0 || Math.floor(x) !== x) {
|
|
2147
|
+
warnings.push(`${assetId}: release primary model renderedProbe foregroundBounds.x is invalid.`);
|
|
2148
|
+
}
|
|
2149
|
+
if (!Number.isFinite(y) || y < 0 || Math.floor(y) !== y) {
|
|
2150
|
+
warnings.push(`${assetId}: release primary model renderedProbe foregroundBounds.y is invalid.`);
|
|
2151
|
+
}
|
|
2152
|
+
if (!Number.isFinite(width) || width <= 0 || Math.floor(width) !== width) {
|
|
2153
|
+
warnings.push(`${assetId}: release primary model renderedProbe foregroundBounds.width is invalid.`);
|
|
2154
|
+
}
|
|
2155
|
+
if (!Number.isFinite(height) || height <= 0 || Math.floor(height) !== height) {
|
|
2156
|
+
warnings.push(`${assetId}: release primary model renderedProbe foregroundBounds.height is invalid.`);
|
|
2157
|
+
}
|
|
2158
|
+
if (warnings.length > 0)
|
|
2159
|
+
return warnings;
|
|
2160
|
+
if (x + width > probeMetrics.width || y + height > probeMetrics.height) {
|
|
2161
|
+
warnings.push(`${assetId}: release primary model renderedProbe foregroundBounds exceed PNG dimensions.`);
|
|
2162
|
+
}
|
|
2163
|
+
return warnings;
|
|
2164
|
+
}
|
|
2165
|
+
function createRoleAwareReleaseQualityWarnings(projectDir, manifest, asset) {
|
|
2166
|
+
void projectDir;
|
|
2167
|
+
void manifest;
|
|
2168
|
+
if (asset.type !== "model" || asset.quality !== "release")
|
|
2169
|
+
return [];
|
|
2170
|
+
const role = asset.role ?? "unknown";
|
|
2171
|
+
const warnings = [];
|
|
2172
|
+
const suitabilityReason = asset.suitabilityReason?.trim() ?? "";
|
|
2173
|
+
const size = asset.boundsMetadata?.size ?? asset.bounds;
|
|
2174
|
+
const dimensions = createRoleAwareDimensions(size);
|
|
2175
|
+
const hasMaterialEvidence = asset.materials.length > 0 && !(asset.materialMetadata?.every((material) => !material.visible || !material.readable) ?? false);
|
|
2176
|
+
const hasTextureEvidence = asset.textures.length > 0 || (asset.hierarchy?.textureCount ?? 0) > 0;
|
|
2177
|
+
if (role === "unknown") {
|
|
2178
|
+
warnings.push(`${asset.id}: role-aware release validation requires a declared asset role.`);
|
|
2179
|
+
return warnings;
|
|
2180
|
+
}
|
|
2181
|
+
if (!dimensions) {
|
|
2182
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation requires valid bounds/inspection dimensions.`);
|
|
2183
|
+
}
|
|
2184
|
+
else {
|
|
2185
|
+
const maxDimension = Math.max(...dimensions);
|
|
2186
|
+
const minDimension = Math.min(...dimensions);
|
|
2187
|
+
if (minDimension <= 0)
|
|
2188
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation found zero or invalid bounds dimensions.`);
|
|
2189
|
+
if (isTinyForReleaseRole(role, dimensions)) {
|
|
2190
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation found bounds too tiny for readable ${role} use.`);
|
|
2191
|
+
}
|
|
2192
|
+
if (isHugeForReleaseRole(role, dimensions) && !hasHashBoundNormalizationEvidence(asset)) {
|
|
2193
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation found huge bounds without explicit normalization evidence.`);
|
|
2194
|
+
}
|
|
2195
|
+
if ((role === "track" || role === "world" || role === "environment") && !hasMeaningfulWorldExtent(dimensions)) {
|
|
2196
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation needs a gameplay-scale footprint/extent.`);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
if (!hasRoleAwareSuitabilityReason(role, suitabilityReason)) {
|
|
2200
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation needs a specific suitabilityReason explaining role readiness.`);
|
|
2201
|
+
}
|
|
2202
|
+
if (requiresMaterialEvidence(role) && !hasMaterialEvidence) {
|
|
2203
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation requires readable material evidence.`);
|
|
2204
|
+
}
|
|
2205
|
+
if (requiresTextureEvidence(role, suitabilityReason) && !hasTextureEvidence) {
|
|
2206
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation requires texture evidence or explicit stylized-material rationale.`);
|
|
2207
|
+
}
|
|
2208
|
+
if (requiresForwardOrientation(role) && !hasForwardOrientationEvidence(asset, suitabilityReason)) {
|
|
2209
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation requires orientation/forward-axis evidence.`);
|
|
2210
|
+
}
|
|
2211
|
+
warnings.push(...createRoleAwareRenderedProbeWarnings(asset, role));
|
|
2212
|
+
if (role === "character") {
|
|
2213
|
+
if (claimsAnimatedOrSkinned(suitabilityReason) && asset.animations.length === 0 && (asset.skeleton?.jointCount ?? 0) === 0) {
|
|
2214
|
+
warnings.push(`${asset.id}: role-aware release character validation cannot claim animated/skinned readiness without animation or skeleton evidence.`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
if (role === "debug" || role === "abstract" || role === "set-dressing") {
|
|
2218
|
+
if (!/\b(non[-\s]?primary|diagnostic|debug|abstract|set[-\s]?dressing|background|decorative)\b/i.test(suitabilityReason)) {
|
|
2219
|
+
warnings.push(`${asset.id}: release ${role} assets cannot satisfy primary asset gates without an explicit non-primary rationale.`);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return warnings;
|
|
2223
|
+
}
|
|
2224
|
+
function createRoleAwareDimensions(size) {
|
|
2225
|
+
if (!size || size.length !== 3)
|
|
2226
|
+
return undefined;
|
|
2227
|
+
if (!size.every((value) => Number.isFinite(value)))
|
|
2228
|
+
return undefined;
|
|
2229
|
+
return size;
|
|
2230
|
+
}
|
|
2231
|
+
function isTinyForReleaseRole(role, size) {
|
|
2232
|
+
const maxDimension = Math.max(...size);
|
|
2233
|
+
if (role === "track" || role === "world" || role === "environment") {
|
|
2234
|
+
const footprint = Math.max(size[0], size[2]);
|
|
2235
|
+
return footprint < 5;
|
|
2236
|
+
}
|
|
2237
|
+
if (role === "vehicle")
|
|
2238
|
+
return maxDimension < 0.75;
|
|
2239
|
+
if (role === "character")
|
|
2240
|
+
return maxDimension < 0.9;
|
|
2241
|
+
if (role === "product" || role === "weapon")
|
|
2242
|
+
return maxDimension < 0.1;
|
|
2243
|
+
return maxDimension < 0.25;
|
|
2244
|
+
}
|
|
2245
|
+
function isHugeForReleaseRole(role, size) {
|
|
2246
|
+
const maxDimension = Math.max(...size);
|
|
2247
|
+
if (role === "track" || role === "world" || role === "environment")
|
|
2248
|
+
return maxDimension > 1000;
|
|
2249
|
+
if (role === "vehicle")
|
|
2250
|
+
return maxDimension > 25;
|
|
2251
|
+
if (role === "character")
|
|
2252
|
+
return maxDimension > 20;
|
|
2253
|
+
if (role === "product" || role === "weapon")
|
|
2254
|
+
return maxDimension > 50;
|
|
2255
|
+
return maxDimension > 250;
|
|
2256
|
+
}
|
|
2257
|
+
function hasMeaningfulWorldExtent(size) {
|
|
2258
|
+
const horizontalArea = Math.abs(size[0] * size[2]);
|
|
2259
|
+
return Math.max(size[0], size[2]) >= 10 && horizontalArea >= 100;
|
|
2260
|
+
}
|
|
2261
|
+
function hasHashBoundNormalizationEvidence(asset) {
|
|
2262
|
+
const probe = asset.renderedProbe;
|
|
2263
|
+
const foregroundBounds = probe?.foregroundBounds;
|
|
2264
|
+
if (!foregroundBounds)
|
|
2265
|
+
return false;
|
|
2266
|
+
return hasExplicitNormalizationEvidence(asset.suitabilityReason ?? "") &&
|
|
2267
|
+
probe?.assetHash === asset.hash &&
|
|
2268
|
+
typeof foregroundBounds.width === "number" &&
|
|
2269
|
+
typeof foregroundBounds.height === "number";
|
|
2270
|
+
}
|
|
2271
|
+
function hasExplicitNormalizationEvidence(suitabilityReason) {
|
|
2272
|
+
if (/\b(?:not|no|without|missing|lacks?|unproven|unverified|unsupported)\s+(?:explicit\s+)?(?:(?:route|render|asset|camera|bounds|unit|world|character|vehicle|track|model)[-\s]?)?(?:normaliz(?:ed|ation)|normalis(?:ed|ation)|rescal(?:ed|ing)|camera[-\s]?fit|bounds[-\s]?approved|unit[-\s]?scale)\b/i.test(suitabilityReason)) {
|
|
2273
|
+
return false;
|
|
2274
|
+
}
|
|
2275
|
+
const explicitPatterns = [
|
|
2276
|
+
/\b(?:normalized|normalised)\s+(?:camera[-\s]?fit|route|render|asset|bounds|scale|placement|model|world|character|vehicle|track)\b/i,
|
|
2277
|
+
/\b(?:route|render|asset|camera|bounds)[-\s]?(?:normalized|normalised)\b/i,
|
|
2278
|
+
/\bcamera[-\s]?fit\s+(?:placement|scale|evidence|normalization|normalisation|proof|route|render|bounds)\b/i,
|
|
2279
|
+
/\bbounds[-\s]?approved\s+(?:placement|scale|evidence|normalization|normalisation|proof|route|render|bounds)\b/i,
|
|
2280
|
+
/\b(?:rescaled|rescaling)\s+(?:for|to|against|with)\s+(?:route|camera|render|asset|bounds|gameplay|world|scene|placement)\b/i,
|
|
2281
|
+
/\bunit[-\s]?scale\s+(?:normalization|normalisation|evidence|proof|approved)\b/i
|
|
2282
|
+
];
|
|
2283
|
+
return explicitPatterns.some((pattern) => pattern.test(suitabilityReason));
|
|
2284
|
+
}
|
|
2285
|
+
function hasRoleAwareSuitabilityReason(role, suitabilityReason) {
|
|
2286
|
+
if (suitabilityReason.length < 48)
|
|
2287
|
+
return false;
|
|
2288
|
+
if (/\b(good|nice|usable|asset loaded|looks ok|works|test fixture)\b/i.test(suitabilityReason) && suitabilityReason.length < 96)
|
|
2289
|
+
return false;
|
|
2290
|
+
if (role === "track" || role === "world" || role === "environment") {
|
|
2291
|
+
return /\b(track|world|environment|gameplay|footprint|path|route|scene|scale|extent|normaliz|traversable)\b/i.test(suitabilityReason);
|
|
2292
|
+
}
|
|
2293
|
+
if (role === "vehicle") {
|
|
2294
|
+
return /\b(vehicle|car|footprint|wheel|track|forward|orientation|racing|drivable)\b/i.test(suitabilityReason);
|
|
2295
|
+
}
|
|
2296
|
+
if (role === "character") {
|
|
2297
|
+
return /\b(character|rig|humanoid|height|readable|orientation|animation|avatar|player)\b/i.test(suitabilityReason);
|
|
2298
|
+
}
|
|
2299
|
+
if (role === "product") {
|
|
2300
|
+
return /\b(product|commerce|configurator|viewer|material|texture|readable|dimension)\b/i.test(suitabilityReason);
|
|
2301
|
+
}
|
|
2302
|
+
if (role === "weapon") {
|
|
2303
|
+
return /\b(weapon|prop|held|orientation|readable|material|scale)\b/i.test(suitabilityReason);
|
|
2304
|
+
}
|
|
2305
|
+
return /\b(prop|set[-\s]?dressing|debug|abstract|decorative|supporting|non[-\s]?primary|readable)\b/i.test(suitabilityReason);
|
|
2306
|
+
}
|
|
2307
|
+
function requiresMaterialEvidence(role) {
|
|
2308
|
+
return role !== "debug" && role !== "abstract";
|
|
2309
|
+
}
|
|
2310
|
+
function requiresTextureEvidence(role, suitabilityReason) {
|
|
2311
|
+
if (role === "debug" || role === "abstract")
|
|
2312
|
+
return false;
|
|
2313
|
+
if (/\b(stylized|stylised|flat[-\s]?color|flat[-\s]?colour|untextured|procedural material|clay render|solid material)\b/i.test(suitabilityReason)) {
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
2316
|
+
return role === "character" || role === "vehicle" || role === "product" || role === "track" || role === "world" || role === "environment" || role === "weapon";
|
|
2317
|
+
}
|
|
2318
|
+
function requiresForwardOrientation(role) {
|
|
2319
|
+
return role === "character" || role === "vehicle" || role === "weapon";
|
|
2320
|
+
}
|
|
2321
|
+
function hasForwardOrientationEvidence(asset, suitabilityReason) {
|
|
2322
|
+
void suitabilityReason;
|
|
2323
|
+
if (asset.orientation?.source === "gltf-extras" && asset.orientation.forwardAxis && asset.orientation.upAxis)
|
|
2324
|
+
return true;
|
|
2325
|
+
if (hasValidManifestOrientationOverride(asset) && asset.orientation?.forwardAxis && asset.orientation?.upAxis)
|
|
2326
|
+
return true;
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
function createRoleAwareRenderedProbeWarnings(asset, role) {
|
|
2330
|
+
const probe = asset.renderedProbe;
|
|
2331
|
+
if (!probe?.url)
|
|
2332
|
+
return [`${asset.id}: role-aware release ${role} validation requires retained renderedProbe evidence.`];
|
|
2333
|
+
const warnings = [];
|
|
2334
|
+
if (!probe.foregroundBounds) {
|
|
2335
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation requires renderedProbe foregroundBounds for readability proof.`);
|
|
2336
|
+
return warnings;
|
|
2337
|
+
}
|
|
2338
|
+
if (typeof probe.width !== "number" || typeof probe.height !== "number" || probe.width <= 0 || probe.height <= 0) {
|
|
2339
|
+
warnings.push(`${asset.id}: role-aware release ${role} validation requires renderedProbe dimensions before foreground readability can be checked.`);
|
|
2340
|
+
return warnings;
|
|
2341
|
+
}
|
|
2342
|
+
const foregroundWarnings = createRenderedProbeForegroundWarnings(asset.id, probe.foregroundBounds, { width: probe.width, height: probe.height });
|
|
2343
|
+
warnings.push(...foregroundWarnings);
|
|
2344
|
+
if (foregroundWarnings.length > 0)
|
|
2345
|
+
return warnings;
|
|
2346
|
+
const rule = readabilityRuleForRole(role);
|
|
2347
|
+
const foreground = probe.foregroundBounds;
|
|
2348
|
+
const widthRatio = foreground.width / probe.width;
|
|
2349
|
+
const heightRatio = foreground.height / probe.height;
|
|
2350
|
+
const areaRatio = (foreground.width * foreground.height) / (probe.width * probe.height);
|
|
2351
|
+
if (foreground.width < rule.minWidthPx ||
|
|
2352
|
+
foreground.height < rule.minHeightPx ||
|
|
2353
|
+
widthRatio < rule.minWidthRatio ||
|
|
2354
|
+
heightRatio < rule.minHeightRatio ||
|
|
2355
|
+
areaRatio < rule.minAreaRatio) {
|
|
2356
|
+
warnings.push(`${asset.id}: role-aware release ${role} renderedProbe foreground is too small/readability-poor (${foreground.width}x${foreground.height} in ${probe.width}x${probe.height}).`);
|
|
2357
|
+
}
|
|
2358
|
+
return warnings;
|
|
2359
|
+
}
|
|
2360
|
+
function readabilityRuleForRole(role) {
|
|
2361
|
+
if (role === "character")
|
|
2362
|
+
return { minWidthPx: 32, minHeightPx: 120, minWidthRatio: 0.04, minHeightRatio: 0.25, minAreaRatio: 0.015 };
|
|
2363
|
+
if (role === "vehicle")
|
|
2364
|
+
return { minWidthPx: 120, minHeightPx: 50, minWidthRatio: 0.18, minHeightRatio: 0.1, minAreaRatio: 0.025 };
|
|
2365
|
+
if (role === "track" || role === "world" || role === "environment")
|
|
2366
|
+
return { minWidthPx: 224, minHeightPx: 120, minWidthRatio: 0.35, minHeightRatio: 0.25, minAreaRatio: 0.12 };
|
|
2367
|
+
if (role === "product" || role === "weapon")
|
|
2368
|
+
return { minWidthPx: 96, minHeightPx: 96, minWidthRatio: 0.12, minHeightRatio: 0.16, minAreaRatio: 0.02 };
|
|
2369
|
+
return { minWidthPx: 64, minHeightPx: 64, minWidthRatio: 0.1, minHeightRatio: 0.1, minAreaRatio: 0.01 };
|
|
2370
|
+
}
|
|
2371
|
+
function claimsAnimatedOrSkinned(suitabilityReason) {
|
|
2372
|
+
return /\b(animated|animation|rigged|skinned|skeleton|humanoid)\b/i.test(suitabilityReason);
|
|
2373
|
+
}
|
|
2374
|
+
function decodeRenderedProbePng(bytes) {
|
|
2375
|
+
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2376
|
+
if (bytes.byteLength < signature.byteLength + 12 || !bytes.subarray(0, signature.byteLength).equals(signature)) {
|
|
2377
|
+
return { ok: false, reason: "missing PNG signature" };
|
|
2378
|
+
}
|
|
2379
|
+
let offset = signature.byteLength;
|
|
2380
|
+
let width = 0;
|
|
2381
|
+
let height = 0;
|
|
2382
|
+
let bitDepth = 0;
|
|
2383
|
+
let colorType = 0;
|
|
2384
|
+
let compression = 0;
|
|
2385
|
+
let filterMethod = 0;
|
|
2386
|
+
let interlace = 0;
|
|
2387
|
+
const idatChunks = [];
|
|
2388
|
+
try {
|
|
2389
|
+
while (offset + 12 <= bytes.byteLength) {
|
|
2390
|
+
const length = bytes.readUInt32BE(offset);
|
|
2391
|
+
offset += 4;
|
|
2392
|
+
const type = bytes.subarray(offset, offset + 4).toString("ascii");
|
|
2393
|
+
offset += 4;
|
|
2394
|
+
if (offset + length + 4 > bytes.byteLength)
|
|
2395
|
+
return { ok: false, reason: `truncated ${type} chunk` };
|
|
2396
|
+
const data = bytes.subarray(offset, offset + length);
|
|
2397
|
+
offset += length + 4;
|
|
2398
|
+
if (type === "IHDR") {
|
|
2399
|
+
if (length !== 13)
|
|
2400
|
+
return { ok: false, reason: "invalid IHDR length" };
|
|
2401
|
+
width = data.readUInt32BE(0);
|
|
2402
|
+
height = data.readUInt32BE(4);
|
|
2403
|
+
bitDepth = data[8] ?? 0;
|
|
2404
|
+
colorType = data[9] ?? 0;
|
|
2405
|
+
compression = data[10] ?? 0;
|
|
2406
|
+
filterMethod = data[11] ?? 0;
|
|
2407
|
+
interlace = data[12] ?? 0;
|
|
2408
|
+
}
|
|
2409
|
+
else if (type === "IDAT") {
|
|
2410
|
+
idatChunks.push(Buffer.from(data));
|
|
2411
|
+
}
|
|
2412
|
+
else if (type === "IEND") {
|
|
2413
|
+
break;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
catch (error) {
|
|
2418
|
+
return { ok: false, reason: error instanceof Error ? error.message : "invalid PNG chunks" };
|
|
2419
|
+
}
|
|
2420
|
+
if (width <= 0 || height <= 0)
|
|
2421
|
+
return { ok: false, reason: "missing IHDR dimensions" };
|
|
2422
|
+
if (bitDepth !== 8)
|
|
2423
|
+
return { ok: false, reason: `unsupported bit depth ${bitDepth}` };
|
|
2424
|
+
if (compression !== 0 || filterMethod !== 0 || interlace !== 0)
|
|
2425
|
+
return { ok: false, reason: "unsupported PNG encoding" };
|
|
2426
|
+
const bytesPerPixel = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 0 ? 1 : 0;
|
|
2427
|
+
if (bytesPerPixel === 0)
|
|
2428
|
+
return { ok: false, reason: `unsupported color type ${colorType}` };
|
|
2429
|
+
if (idatChunks.length === 0)
|
|
2430
|
+
return { ok: false, reason: "missing IDAT data" };
|
|
2431
|
+
let inflated;
|
|
2432
|
+
try {
|
|
2433
|
+
inflated = inflateSync(Buffer.concat(idatChunks));
|
|
2434
|
+
}
|
|
2435
|
+
catch (error) {
|
|
2436
|
+
return { ok: false, reason: error instanceof Error ? error.message : "invalid compressed IDAT data" };
|
|
2437
|
+
}
|
|
2438
|
+
const rowLength = width * bytesPerPixel;
|
|
2439
|
+
const expectedLength = (rowLength + 1) * height;
|
|
2440
|
+
if (inflated.byteLength < expectedLength)
|
|
2441
|
+
return { ok: false, reason: "truncated decompressed pixel data" };
|
|
2442
|
+
let previous = new Uint8Array(rowLength);
|
|
2443
|
+
let nonBlankPixels = 0;
|
|
2444
|
+
const buckets = new Set();
|
|
2445
|
+
for (let y = 0; y < height; y += 1) {
|
|
2446
|
+
const rowOffset = y * (rowLength + 1);
|
|
2447
|
+
const filter = inflated[rowOffset] ?? 0;
|
|
2448
|
+
if (filter < 0 || filter > 4)
|
|
2449
|
+
return { ok: false, reason: `unsupported PNG filter ${filter}` };
|
|
2450
|
+
const row = new Uint8Array(rowLength);
|
|
2451
|
+
for (let x = 0; x < rowLength; x += 1) {
|
|
2452
|
+
const raw = inflated[rowOffset + 1 + x] ?? 0;
|
|
2453
|
+
const left = x >= bytesPerPixel ? row[x - bytesPerPixel] ?? 0 : 0;
|
|
2454
|
+
const up = previous[x] ?? 0;
|
|
2455
|
+
const upLeft = x >= bytesPerPixel ? previous[x - bytesPerPixel] ?? 0 : 0;
|
|
2456
|
+
row[x] = (raw + pngFilterPredictor(filter, left, up, upLeft)) & 0xff;
|
|
2457
|
+
}
|
|
2458
|
+
for (let pixel = 0; pixel < width; pixel += 1) {
|
|
2459
|
+
const pixelOffset = pixel * bytesPerPixel;
|
|
2460
|
+
const red = row[pixelOffset] ?? 0;
|
|
2461
|
+
const green = colorType === 0 ? red : row[pixelOffset + 1] ?? 0;
|
|
2462
|
+
const blue = colorType === 0 ? red : row[pixelOffset + 2] ?? 0;
|
|
2463
|
+
const alpha = colorType === 6 ? row[pixelOffset + 3] ?? 255 : 255;
|
|
2464
|
+
if (alpha > 8 && (red > 8 || green > 8 || blue > 8)) {
|
|
2465
|
+
nonBlankPixels += 1;
|
|
2466
|
+
buckets.add(`${red >> 5}:${green >> 5}:${blue >> 5}`);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
previous = row;
|
|
2470
|
+
}
|
|
2471
|
+
return { ok: true, width, height, nonBlankPixels, colorBuckets: buckets.size };
|
|
2472
|
+
}
|
|
2473
|
+
function pngFilterPredictor(filter, left, up, upLeft) {
|
|
2474
|
+
if (filter === 0)
|
|
2475
|
+
return 0;
|
|
2476
|
+
if (filter === 1)
|
|
2477
|
+
return left;
|
|
2478
|
+
if (filter === 2)
|
|
2479
|
+
return up;
|
|
2480
|
+
if (filter === 3)
|
|
2481
|
+
return Math.floor((left + up) / 2);
|
|
2482
|
+
if (filter === 4)
|
|
2483
|
+
return paethPredictor(left, up, upLeft);
|
|
2484
|
+
throw new Error(`unsupported PNG filter ${filter}`);
|
|
2485
|
+
}
|
|
2486
|
+
function paethPredictor(left, up, upLeft) {
|
|
2487
|
+
const prediction = left + up - upLeft;
|
|
2488
|
+
const leftDistance = Math.abs(prediction - left);
|
|
2489
|
+
const upDistance = Math.abs(prediction - up);
|
|
2490
|
+
const upLeftDistance = Math.abs(prediction - upLeft);
|
|
2491
|
+
if (leftDistance <= upDistance && leftDistance <= upLeftDistance)
|
|
2492
|
+
return left;
|
|
2493
|
+
return upDistance <= upLeftDistance ? up : upLeft;
|
|
2494
|
+
}
|
|
2495
|
+
function createReleaseThumbnailWarnings(projectDir, manifest, asset) {
|
|
2496
|
+
if (asset.type !== "model")
|
|
2497
|
+
return [];
|
|
2498
|
+
if (!asset.thumbnailUrl)
|
|
2499
|
+
return [`${asset.id}: release primary model missing thumbnail/probe artifact.`];
|
|
2500
|
+
const thumbnailPath = resolvePublicArtifactPath(projectDir, manifest, asset.thumbnailUrl);
|
|
2501
|
+
if (!thumbnailPath || !existsSync(thumbnailPath)) {
|
|
2502
|
+
return [`${asset.id}: release primary model thumbnail/probe artifact is missing on disk (${asset.thumbnailUrl}).`];
|
|
2503
|
+
}
|
|
2504
|
+
const size = statSync(thumbnailPath).size;
|
|
2505
|
+
if (size < 200)
|
|
2506
|
+
return [`${asset.id}: release primary model thumbnail/probe artifact is too small (${size} bytes).`];
|
|
2507
|
+
return [];
|
|
2508
|
+
}
|
|
2509
|
+
function nonEmpty(value) {
|
|
2510
|
+
return Boolean(value?.trim());
|
|
2511
|
+
}
|
|
1654
2512
|
function isPlaceholderAsset(asset, provenance) {
|
|
1655
2513
|
const value = [
|
|
1656
2514
|
asset.id,
|