@aura3d/cli 1.3.3 → 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.
Files changed (91) hide show
  1. package/dist/asset-constants.d.ts +5 -0
  2. package/dist/asset-constants.d.ts.map +1 -0
  3. package/dist/asset-constants.js +5 -0
  4. package/dist/asset-constants.js.map +1 -0
  5. package/dist/asset-core-types.d.ts +206 -0
  6. package/dist/asset-core-types.d.ts.map +1 -0
  7. package/dist/asset-core-types.js +2 -0
  8. package/dist/asset-core-types.js.map +1 -0
  9. package/dist/asset-inspection-types.d.ts +131 -0
  10. package/dist/asset-inspection-types.d.ts.map +1 -0
  11. package/dist/asset-inspection-types.js +2 -0
  12. package/dist/asset-inspection-types.js.map +1 -0
  13. package/dist/asset-manifest.d.ts +8 -0
  14. package/dist/asset-manifest.d.ts.map +1 -0
  15. package/dist/asset-manifest.js +116 -0
  16. package/dist/asset-manifest.js.map +1 -0
  17. package/dist/asset-readiness-types.d.ts +164 -0
  18. package/dist/asset-readiness-types.d.ts.map +1 -0
  19. package/dist/asset-readiness-types.js +2 -0
  20. package/dist/asset-readiness-types.js.map +1 -0
  21. package/dist/asset-source-ast.d.ts +8 -0
  22. package/dist/asset-source-ast.d.ts.map +1 -0
  23. package/dist/asset-source-ast.js +241 -0
  24. package/dist/asset-source-ast.js.map +1 -0
  25. package/dist/asset-source-roles.d.ts +4 -0
  26. package/dist/asset-source-roles.d.ts.map +1 -0
  27. package/dist/asset-source-roles.js +79 -0
  28. package/dist/asset-source-roles.js.map +1 -0
  29. package/dist/asset-source-validation.d.ts +4 -0
  30. package/dist/asset-source-validation.d.ts.map +1 -0
  31. package/dist/asset-source-validation.js +200 -0
  32. package/dist/asset-source-validation.js.map +1 -0
  33. package/dist/character-assembly-types.d.ts +39 -0
  34. package/dist/character-assembly-types.d.ts.map +1 -0
  35. package/dist/character-assembly-types.js +2 -0
  36. package/dist/character-assembly-types.js.map +1 -0
  37. package/dist/cli-help.d.ts +4 -0
  38. package/dist/cli-help.d.ts.map +1 -0
  39. package/dist/cli-help.js +48 -0
  40. package/dist/cli-help.js.map +1 -0
  41. package/dist/cli-options.d.ts +66 -0
  42. package/dist/cli-options.d.ts.map +1 -0
  43. package/dist/cli-options.js +234 -0
  44. package/dist/cli-options.js.map +1 -0
  45. package/dist/cli.js +20 -177
  46. package/dist/cli.js.map +1 -1
  47. package/dist/index.d.ts +12 -402
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +948 -90
  50. package/dist/index.js.map +1 -1
  51. package/dist/pull-bridge/adapters.d.ts +4 -0
  52. package/dist/pull-bridge/adapters.d.ts.map +1 -0
  53. package/dist/pull-bridge/adapters.js +38 -0
  54. package/dist/pull-bridge/adapters.js.map +1 -0
  55. package/dist/pull-bridge/download.d.ts +6 -0
  56. package/dist/pull-bridge/download.d.ts.map +1 -0
  57. package/dist/pull-bridge/download.js +208 -0
  58. package/dist/pull-bridge/download.js.map +1 -0
  59. package/dist/pull-bridge/profiles.d.ts +14 -0
  60. package/dist/pull-bridge/profiles.d.ts.map +1 -0
  61. package/dist/pull-bridge/profiles.js +63 -0
  62. package/dist/pull-bridge/profiles.js.map +1 -0
  63. package/dist/pull-bridge/provenance.d.ts +9 -0
  64. package/dist/pull-bridge/provenance.d.ts.map +1 -0
  65. package/dist/pull-bridge/provenance.js +99 -0
  66. package/dist/pull-bridge/provenance.js.map +1 -0
  67. package/dist/pull-bridge/pullable.d.ts +14 -0
  68. package/dist/pull-bridge/pullable.d.ts.map +1 -0
  69. package/dist/pull-bridge/pullable.js +58 -0
  70. package/dist/pull-bridge/pullable.js.map +1 -0
  71. package/dist/pull-bridge/scoring.d.ts +21 -0
  72. package/dist/pull-bridge/scoring.d.ts.map +1 -0
  73. package/dist/pull-bridge/scoring.js +177 -0
  74. package/dist/pull-bridge/scoring.js.map +1 -0
  75. package/dist/pull-bridge/search.d.ts +39 -0
  76. package/dist/pull-bridge/search.d.ts.map +1 -0
  77. package/dist/pull-bridge/search.js +114 -0
  78. package/dist/pull-bridge/search.js.map +1 -0
  79. package/dist/pull-bridge/types.d.ts +9 -0
  80. package/dist/pull-bridge/types.d.ts.map +1 -0
  81. package/dist/pull-bridge/types.js +2 -0
  82. package/dist/pull-bridge/types.js.map +1 -0
  83. package/dist/pull-bridge/vector3.d.ts +2 -0
  84. package/dist/pull-bridge/vector3.d.ts.map +1 -0
  85. package/dist/pull-bridge/vector3.js +4 -0
  86. package/dist/pull-bridge/vector3.js.map +1 -0
  87. package/dist/pull-bridge.d.ts +18 -100
  88. package/dist/pull-bridge.d.ts.map +1 -1
  89. package/dist/pull-bridge.js +59 -526
  90. package/dist/pull-bridge.js.map +1 -1
  91. 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 const DEFAULT_AURA_ASSET_MANIFEST = "aura.assets.json";
7
- export const DEFAULT_AURA_ASSET_OUTPUT_DIR = "public/aura-assets";
8
- export const DEFAULT_AURA_ASSET_PUBLIC_PATH = "/aura-assets/";
9
- export const DEFAULT_AURA_ASSET_TYPEGEN = "src/aura-assets.ts";
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 (options.noPlaceholders && isPlaceholderAsset(asset, provenance)) {
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 (options.requireLicense && !hasUsableLicenseEvidence(provenance)) {
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
- warnings.push(...asset.warnings.map((warning) => `${asset.id}: ${warning}`));
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 manifest = readAssetManifest(projectDir);
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({ projectDir });
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 ? ["Deploy check passed."] : failures
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
- const normalized = normalizeAssetIdFilter(assetIds);
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, sourceUrl, sourceFamily, evidence,
1570
- * sha256) that the new pass did not re-detect. Freshly detected values win over
1571
- * the prior entry; explicit `addAsset` options still win over both (applied in
1572
- * {@link createAssetProvenance}). (#26)
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,