@aura3d/cli 1.4.2 → 1.4.4
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/README.md +5 -3
- package/dist/asset-core-types.d.ts +43 -0
- package/dist/asset-core-types.d.ts.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +7 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-options.d.ts +3 -1
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +23 -0
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +40 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +384 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/dist/cartoon-asset-profiles.d.ts +0 -15
- package/dist/cartoon-asset-profiles.d.ts.map +0 -1
- package/dist/cartoon-asset-profiles.js +0 -46
- package/dist/cartoon-asset-profiles.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -140,7 +140,9 @@ export function addAsset(options) {
|
|
|
140
140
|
provenance: createAssetProvenance(projectDir, sourcePath, options, mergeDetectedProvenance(existing?.provenance, inspection.provenance)),
|
|
141
141
|
textures: inspection.textures,
|
|
142
142
|
dependencies: inspection.dependencies,
|
|
143
|
-
orientation:
|
|
143
|
+
orientation: options.orientation ?? (existing?.orientation?.source === "manifest-override" && existing.orientation.assetHash === `sha256-${hash}`
|
|
144
|
+
? existing.orientation
|
|
145
|
+
: inspection.orientation),
|
|
144
146
|
nodeNames: inspection.nodeNames,
|
|
145
147
|
thumbnailUrl,
|
|
146
148
|
quality: options.quality ?? existing?.quality ?? "ungraded",
|
|
@@ -150,6 +152,15 @@ export function addAsset(options) {
|
|
|
150
152
|
...(options.gameGeometry ?? existing?.gameGeometry ? { gameGeometry: options.gameGeometry ?? existing?.gameGeometry } : {}),
|
|
151
153
|
warnings: createAssetWarnings(sourcePath, inspection)
|
|
152
154
|
};
|
|
155
|
+
if (options.orientation) {
|
|
156
|
+
if (options.orientation.source !== "manifest-override") {
|
|
157
|
+
throw new Error("Aura3D assets add failed: --orientation-json must contain a manifest-override orientation.");
|
|
158
|
+
}
|
|
159
|
+
const orientationFailures = createManifestOrientationOverrideWarnings(entry);
|
|
160
|
+
if (orientationFailures.length > 0) {
|
|
161
|
+
throw new Error(`Aura3D assets add failed: invalid orientation override: ${orientationFailures.join("; ")}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
153
164
|
const manifest = sortManifest({
|
|
154
165
|
schema: "aura3d.assets/1.0",
|
|
155
166
|
assetBasePath: publicPath,
|
|
@@ -173,6 +184,328 @@ export function addAsset(options) {
|
|
|
173
184
|
]
|
|
174
185
|
};
|
|
175
186
|
}
|
|
187
|
+
export function bindGameRouteEvidence(options) {
|
|
188
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
189
|
+
const manifest = readAssetManifest(projectDir);
|
|
190
|
+
const assetIds = [...new Set(options.assetIds.map((id) => id.trim()).filter(Boolean))];
|
|
191
|
+
const blockers = [];
|
|
192
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.routeId))
|
|
193
|
+
blockers.push(`game-route-evidence:unsafe-route-id:${options.routeId}`);
|
|
194
|
+
if (assetIds.length !== 2)
|
|
195
|
+
blockers.push(`game-route-evidence:asset-pair-required:${assetIds.length}`);
|
|
196
|
+
const assets = assetIds.map((id) => manifest.assets.find((asset) => asset.id === id));
|
|
197
|
+
for (let index = 0; index < assetIds.length; index += 1) {
|
|
198
|
+
if (!assets[index])
|
|
199
|
+
blockers.push(`game-route-evidence:asset-missing:${assetIds[index]}`);
|
|
200
|
+
}
|
|
201
|
+
const screenshotBytes = readProjectEvidenceBytes(projectDir, options.routePrimaryScreenshot, "screenshot", blockers);
|
|
202
|
+
const screenshotSha256 = screenshotBytes
|
|
203
|
+
? `sha256-${createHash("sha256").update(screenshotBytes).digest("hex")}`
|
|
204
|
+
: undefined;
|
|
205
|
+
const geometry = readProjectEvidenceJson(projectDir, options.geometryReport, "geometry-report", blockers);
|
|
206
|
+
const composition = readProjectEvidenceJson(projectDir, options.compositionReport, "composition-report", blockers);
|
|
207
|
+
const reviewFile = readProjectEvidenceJson(projectDir, options.visualReview, "visual-review", blockers);
|
|
208
|
+
const geometryRecord = asRecord(geometry);
|
|
209
|
+
const compositionRecord = asRecord(composition);
|
|
210
|
+
const reviewRecord = asRecord(reviewFile);
|
|
211
|
+
if (geometryRecord) {
|
|
212
|
+
if (geometryRecord.routeId !== options.routeId)
|
|
213
|
+
blockers.push(`game-route-evidence:geometry-route:${String(geometryRecord.routeId)}`);
|
|
214
|
+
if (geometryRecord.pass !== true)
|
|
215
|
+
blockers.push(`game-route-evidence:geometry-not-passing:${String(geometryRecord.pass)}`);
|
|
216
|
+
}
|
|
217
|
+
const geometryValue = asRecord(options.category === "racing" ? geometryRecord?.topology : geometryRecord?.surfaceMap);
|
|
218
|
+
const expectedGeometrySchema = options.category === "racing"
|
|
219
|
+
? "aura3d-racing-track-topology/1.0"
|
|
220
|
+
: "aura3d-platformer-playable-surfaces/1.0";
|
|
221
|
+
if (geometryRecord && geometryRecord.schema !== expectedGeometrySchema)
|
|
222
|
+
blockers.push(`game-route-evidence:geometry-schema:${String(geometryRecord.schema)}`);
|
|
223
|
+
const primaryGeometryAssetId = typeof geometryValue?.assetId === "string" ? geometryValue.assetId : undefined;
|
|
224
|
+
if (!primaryGeometryAssetId || !assetIds.includes(primaryGeometryAssetId)) {
|
|
225
|
+
blockers.push(`game-route-evidence:geometry-primary-asset:${String(primaryGeometryAssetId)}`);
|
|
226
|
+
}
|
|
227
|
+
if (geometryValue) {
|
|
228
|
+
const overlay = asRecord(geometryValue.evidence)?.routeOverlay;
|
|
229
|
+
if (overlay !== options.routePrimaryScreenshot)
|
|
230
|
+
blockers.push(`game-route-evidence:geometry-screenshot:${String(overlay)}`);
|
|
231
|
+
}
|
|
232
|
+
if (compositionRecord) {
|
|
233
|
+
if (compositionRecord.schema !== "aura3d-showcase-asset-pair-composition/1.0")
|
|
234
|
+
blockers.push(`game-route-evidence:composition-schema:${String(compositionRecord.schema)}`);
|
|
235
|
+
if (compositionRecord.routeId !== options.routeId)
|
|
236
|
+
blockers.push(`game-route-evidence:composition-route:${String(compositionRecord.routeId)}`);
|
|
237
|
+
if (compositionRecord.category !== options.category)
|
|
238
|
+
blockers.push(`game-route-evidence:composition-category:${String(compositionRecord.category)}`);
|
|
239
|
+
if (compositionRecord.pass !== true || compositionRecord.verdict !== "pass")
|
|
240
|
+
blockers.push(`game-route-evidence:composition-not-passing:${String(compositionRecord.verdict)}`);
|
|
241
|
+
const screenshot = asRecord(compositionRecord.screenshot);
|
|
242
|
+
if (screenshot?.path !== options.routePrimaryScreenshot)
|
|
243
|
+
blockers.push(`game-route-evidence:composition-screenshot:${String(screenshot?.path)}`);
|
|
244
|
+
if (screenshotSha256 && screenshot?.sha256 !== screenshotSha256)
|
|
245
|
+
blockers.push("game-route-evidence:composition-screenshot-hash-stale");
|
|
246
|
+
const reportGeometry = asRecord(compositionRecord.geometry);
|
|
247
|
+
if (reportGeometry?.report !== options.geometryReport)
|
|
248
|
+
blockers.push(`game-route-evidence:composition-geometry-report:${String(reportGeometry?.report)}`);
|
|
249
|
+
const compositionAssets = Array.isArray(compositionRecord.assets) ? compositionRecord.assets.map(asRecord).filter(Boolean) : [];
|
|
250
|
+
for (const asset of assets) {
|
|
251
|
+
if (!asset)
|
|
252
|
+
continue;
|
|
253
|
+
const evidenceAsset = compositionAssets.find((candidate) => candidate?.id === asset.id);
|
|
254
|
+
if (!evidenceAsset)
|
|
255
|
+
blockers.push(`game-route-evidence:composition-asset-missing:${asset.id}`);
|
|
256
|
+
else if (evidenceAsset.manifestHash !== asset.hash || evidenceAsset.evidenceHash !== asset.hash)
|
|
257
|
+
blockers.push(`game-route-evidence:composition-asset-hash-stale:${asset.id}`);
|
|
258
|
+
}
|
|
259
|
+
const requiredChecks = ["binding-overlap", "contact", "camera-readability", "scale-contract", "debug-guide-absence"];
|
|
260
|
+
const checks = Array.isArray(compositionRecord.checks) ? compositionRecord.checks.map(asRecord).filter(Boolean) : [];
|
|
261
|
+
for (const id of requiredChecks) {
|
|
262
|
+
if (checks.find((check) => check?.id === id)?.verdict !== "pass")
|
|
263
|
+
blockers.push(`game-route-evidence:composition-check:${id}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const routes = Array.isArray(reviewRecord?.routes) ? reviewRecord.routes.map(asRecord).filter(Boolean) : [];
|
|
267
|
+
const review = routes.find((candidate) => candidate?.id === options.routeId);
|
|
268
|
+
if (!review)
|
|
269
|
+
blockers.push(`game-route-evidence:visual-review-route-missing:${options.routeId}`);
|
|
270
|
+
else {
|
|
271
|
+
if (review.verdict !== "pass")
|
|
272
|
+
blockers.push(`game-route-evidence:visual-review-verdict:${String(review.verdict)}`);
|
|
273
|
+
const screenshots = Array.isArray(review.screenshotEvidence) ? review.screenshotEvidence : [];
|
|
274
|
+
if (!screenshots.includes(options.routePrimaryScreenshot))
|
|
275
|
+
blockers.push("game-route-evidence:visual-review-screenshot-missing");
|
|
276
|
+
}
|
|
277
|
+
for (const asset of assets) {
|
|
278
|
+
if (!asset)
|
|
279
|
+
continue;
|
|
280
|
+
const expected = asset.id === primaryGeometryAssetId
|
|
281
|
+
? (options.category === "racing" ? "certified-racing-track" : "certified-platformer-world")
|
|
282
|
+
: (options.category === "racing" ? "certified-racing-vehicle" : "certified-platformer-character");
|
|
283
|
+
if (asset.gameGeometry?.certification !== expected && asset.gameGeometry?.certification !== "certified-generated-game-world") {
|
|
284
|
+
blockers.push(`game-route-evidence:asset-certification:${asset.id}:${String(asset.gameGeometry?.certification)}`);
|
|
285
|
+
}
|
|
286
|
+
if (asset.gameGeometry?.evidence?.manifestHash !== asset.hash)
|
|
287
|
+
blockers.push(`game-route-evidence:asset-manifest-hash-stale:${asset.id}`);
|
|
288
|
+
}
|
|
289
|
+
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
290
|
+
if (blockers.length > 0 || !screenshotSha256) {
|
|
291
|
+
return { ok: false, wroteManifest: false, manifestPath, routeId: options.routeId, assetIds, blockers };
|
|
292
|
+
}
|
|
293
|
+
const next = sortManifest({
|
|
294
|
+
...manifest,
|
|
295
|
+
assets: manifest.assets.map((asset) => assetIds.includes(asset.id)
|
|
296
|
+
? {
|
|
297
|
+
...asset,
|
|
298
|
+
gameGeometry: {
|
|
299
|
+
...asset.gameGeometry,
|
|
300
|
+
evidence: {
|
|
301
|
+
manifestHash: asset.hash,
|
|
302
|
+
routePrimaryScreenshot: options.routePrimaryScreenshot,
|
|
303
|
+
routePrimaryScreenshotSha256: screenshotSha256,
|
|
304
|
+
geometryReport: options.geometryReport,
|
|
305
|
+
visualReview: "pass",
|
|
306
|
+
assetPairPass: true,
|
|
307
|
+
blockers: []
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
: asset)
|
|
312
|
+
});
|
|
313
|
+
writeAssetManifest(projectDir, next);
|
|
314
|
+
const typegenPath = writeTypedAssets(projectDir, next);
|
|
315
|
+
return { ok: true, wroteManifest: true, manifestPath, typegenPath, routeId: options.routeId, assetIds, blockers: [] };
|
|
316
|
+
}
|
|
317
|
+
function asRecord(value) {
|
|
318
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
319
|
+
}
|
|
320
|
+
function readProjectEvidenceBytes(projectDir, path, label, blockers) {
|
|
321
|
+
if (!path || path.includes("\\0") || path.startsWith("/") || path.startsWith("\\")) {
|
|
322
|
+
blockers.push(`game-route-evidence:${label}-unsafe-path:${path}`);
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
const absolute = resolve(projectDir, path);
|
|
326
|
+
const rel = relative(projectDir, absolute);
|
|
327
|
+
if (rel.startsWith("..") || rel.startsWith("/")) {
|
|
328
|
+
blockers.push(`game-route-evidence:${label}-unsafe-path:${path}`);
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
if (!existsSync(absolute)) {
|
|
332
|
+
blockers.push(`game-route-evidence:${label}-missing:${path}`);
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
return readFileSync(absolute);
|
|
336
|
+
}
|
|
337
|
+
function readProjectEvidenceJson(projectDir, path, label, blockers) {
|
|
338
|
+
const bytes = readProjectEvidenceBytes(projectDir, path, label, blockers);
|
|
339
|
+
if (!bytes)
|
|
340
|
+
return undefined;
|
|
341
|
+
try {
|
|
342
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
blockers.push(`game-route-evidence:${label}-invalid-json:${path}`);
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
export async function certifyGameGeometry(options) {
|
|
350
|
+
const { probeShowcaseGameGeometry } = await import("create-aura3d");
|
|
351
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
352
|
+
const manifest = readAssetManifest(projectDir);
|
|
353
|
+
const batchIds = options.assetIds?.map((id) => id.trim()).filter(Boolean);
|
|
354
|
+
const mode = batchIds ? "screen" : "certify";
|
|
355
|
+
const assetIds = batchIds ?? (options.assetId ? [options.assetId.trim()] : []);
|
|
356
|
+
if (assetIds.length === 0) {
|
|
357
|
+
throw new Error("Game geometry certification requires --asset <id> or read-only --assets <csv> screening.");
|
|
358
|
+
}
|
|
359
|
+
if (mode === "certify" && assetIds.length !== 1) {
|
|
360
|
+
throw new Error("Game geometry certification writes exactly one asset at a time; use --assets for read-only screening.");
|
|
361
|
+
}
|
|
362
|
+
const evaluated = assetIds.map((assetId) => {
|
|
363
|
+
const asset = manifest.assets.find((entry) => entry.id === assetId);
|
|
364
|
+
if (!asset) {
|
|
365
|
+
return {
|
|
366
|
+
row: {
|
|
367
|
+
assetId,
|
|
368
|
+
category: options.category,
|
|
369
|
+
pass: false,
|
|
370
|
+
reasons: [],
|
|
371
|
+
blockers: [`asset-extraction:asset-not-found:${assetId}`]
|
|
372
|
+
},
|
|
373
|
+
gameGeometry: undefined
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const visualCertification = options.category === "racing" && asset.role === "vehicle"
|
|
377
|
+
? { role: "vehicle", certification: "certified-racing-vehicle", label: "racing-vehicle" }
|
|
378
|
+
: options.category === "platformer" && asset.role === "character"
|
|
379
|
+
? { role: "character", certification: "certified-platformer-character", label: "platformer-character" }
|
|
380
|
+
: undefined;
|
|
381
|
+
if (visualCertification) {
|
|
382
|
+
const blockers = retainedGameSubjectCertificationBlockers(projectDir, manifest, asset, visualCertification.role, visualCertification.label);
|
|
383
|
+
const probe = asset.renderedProbe;
|
|
384
|
+
return {
|
|
385
|
+
row: {
|
|
386
|
+
assetId,
|
|
387
|
+
category: options.category,
|
|
388
|
+
pass: blockers.length === 0,
|
|
389
|
+
reasons: blockers.length === 0 && probe
|
|
390
|
+
? [
|
|
391
|
+
`hash-bound retained ${visualCertification.role} probe:${probe.url}`,
|
|
392
|
+
`foreground:${probe.foregroundBounds?.width ?? 0}x${probe.foregroundBounds?.height ?? 0}`
|
|
393
|
+
]
|
|
394
|
+
: [],
|
|
395
|
+
blockers
|
|
396
|
+
},
|
|
397
|
+
gameGeometry: blockers.length === 0 && probe
|
|
398
|
+
? {
|
|
399
|
+
certification: visualCertification.certification,
|
|
400
|
+
evidence: {
|
|
401
|
+
manifestHash: asset.hash,
|
|
402
|
+
routePrimaryScreenshot: probe.url,
|
|
403
|
+
routePrimaryScreenshotSha256: probe.sha256,
|
|
404
|
+
visualReview: "pass",
|
|
405
|
+
blockers: []
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
: undefined
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const probe = probeShowcaseGameGeometry(assetId, options.category, { projectDir });
|
|
412
|
+
const gameGeometry = probe.extraction.ok
|
|
413
|
+
? {
|
|
414
|
+
certification: options.category === "racing" ? "certified-racing-track" : "certified-platformer-world",
|
|
415
|
+
evidence: {
|
|
416
|
+
manifestHash: asset.hash,
|
|
417
|
+
blockers: []
|
|
418
|
+
},
|
|
419
|
+
...(options.category === "racing"
|
|
420
|
+
? { racingTopology: probe.extraction.value }
|
|
421
|
+
: { playableSurfaceMap: probe.extraction.value })
|
|
422
|
+
}
|
|
423
|
+
: undefined;
|
|
424
|
+
return {
|
|
425
|
+
row: {
|
|
426
|
+
assetId,
|
|
427
|
+
category: options.category,
|
|
428
|
+
pass: probe.extraction.ok,
|
|
429
|
+
reasons: probe.extraction.reasons,
|
|
430
|
+
blockers: probe.extraction.ok ? [] : probe.extraction.blockers
|
|
431
|
+
},
|
|
432
|
+
gameGeometry
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
const rows = evaluated.map(({ row }) => row);
|
|
436
|
+
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
437
|
+
if (mode === "screen" || !rows[0]?.pass) {
|
|
438
|
+
return {
|
|
439
|
+
ok: rows.every((row) => row.pass),
|
|
440
|
+
mode,
|
|
441
|
+
wroteManifest: false,
|
|
442
|
+
manifestPath,
|
|
443
|
+
rows
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const assetId = rows[0]?.assetId;
|
|
447
|
+
const gameGeometry = evaluated[0]?.gameGeometry;
|
|
448
|
+
if (!assetId || !gameGeometry) {
|
|
449
|
+
return { ok: false, mode, wroteManifest: false, manifestPath, rows };
|
|
450
|
+
}
|
|
451
|
+
const next = sortManifest({
|
|
452
|
+
...manifest,
|
|
453
|
+
assets: manifest.assets.map((entry) => entry.id === assetId ? { ...entry, gameGeometry } : entry)
|
|
454
|
+
});
|
|
455
|
+
writeAssetManifest(projectDir, next);
|
|
456
|
+
const typegenPath = writeTypedAssets(projectDir, next);
|
|
457
|
+
return {
|
|
458
|
+
ok: true,
|
|
459
|
+
mode,
|
|
460
|
+
wroteManifest: true,
|
|
461
|
+
manifestPath,
|
|
462
|
+
typegenPath,
|
|
463
|
+
rows
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function retainedGameSubjectCertificationBlockers(projectDir, manifest, asset, role, label) {
|
|
467
|
+
const blockers = [];
|
|
468
|
+
if (asset.role !== role) {
|
|
469
|
+
blockers.push(`asset-certification:${label}-role-required:${asset.id}`);
|
|
470
|
+
return blockers;
|
|
471
|
+
}
|
|
472
|
+
if (asset.quality !== "release") {
|
|
473
|
+
blockers.push(`asset-certification:${label}-release-quality-required:${asset.id}`);
|
|
474
|
+
}
|
|
475
|
+
const probe = asset.renderedProbe;
|
|
476
|
+
if (!probe) {
|
|
477
|
+
blockers.push(`asset-certification:${label}-probe-missing:${asset.id}`);
|
|
478
|
+
return blockers;
|
|
479
|
+
}
|
|
480
|
+
if (probe.assetHash !== asset.hash) {
|
|
481
|
+
blockers.push(`asset-certification:${label}-probe-asset-hash-stale:${asset.id}`);
|
|
482
|
+
}
|
|
483
|
+
const probePath = resolvePublicArtifactPath(projectDir, manifest, probe.url);
|
|
484
|
+
if (!probePath || !existsSync(probePath)) {
|
|
485
|
+
blockers.push(`asset-certification:${label}-probe-artifact-missing:${asset.id}`);
|
|
486
|
+
return blockers;
|
|
487
|
+
}
|
|
488
|
+
const probeBytes = readFileSync(probePath);
|
|
489
|
+
const actualSha256 = `sha256-${createHash("sha256").update(probeBytes).digest("hex")}`;
|
|
490
|
+
if (probe.sha256 !== actualSha256) {
|
|
491
|
+
blockers.push(`asset-certification:${label}-probe-image-hash-stale:${asset.id}`);
|
|
492
|
+
}
|
|
493
|
+
const probeMetrics = decodeRenderedProbePng(probeBytes);
|
|
494
|
+
if (!probeMetrics.ok) {
|
|
495
|
+
blockers.push(`asset-certification:${label}-probe-not-png:${asset.id}`);
|
|
496
|
+
return blockers;
|
|
497
|
+
}
|
|
498
|
+
if (probe.width !== probeMetrics.width || probe.height !== probeMetrics.height) {
|
|
499
|
+
blockers.push(`asset-certification:${label}-probe-dimensions-stale:${asset.id}`);
|
|
500
|
+
}
|
|
501
|
+
if (probe.nonBlankPixels !== probeMetrics.nonBlankPixels || probe.colorBuckets !== probeMetrics.colorBuckets) {
|
|
502
|
+
blockers.push(`asset-certification:${label}-probe-pixels-stale:${asset.id}`);
|
|
503
|
+
}
|
|
504
|
+
if (createRoleAwareRenderedProbeWarnings(asset, role).length > 0) {
|
|
505
|
+
blockers.push(`asset-certification:${label}-probe-readability-failed:${asset.id}`);
|
|
506
|
+
}
|
|
507
|
+
return blockers;
|
|
508
|
+
}
|
|
176
509
|
export function scanAssets(options) {
|
|
177
510
|
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
178
511
|
const directory = resolve(projectDir, options.directory);
|
|
@@ -1782,6 +2115,7 @@ function createAssetWarnings(path, inspection) {
|
|
|
1782
2115
|
return warnings;
|
|
1783
2116
|
}
|
|
1784
2117
|
function createAssetProvenance(projectDir, sourcePath, options, detected) {
|
|
2118
|
+
const evidence = [...new Set([...(options.provenanceEvidence ?? []), ...(detected?.evidence ?? [])].map((entry) => entry.trim()).filter(Boolean))];
|
|
1785
2119
|
return {
|
|
1786
2120
|
sourcePath: normalizeRelativePath(relative(projectDir, sourcePath)),
|
|
1787
2121
|
...(options.sourcePage ?? detected?.sourcePage ? { sourcePage: options.sourcePage ?? detected?.sourcePage } : {}),
|
|
@@ -1797,7 +2131,7 @@ function createAssetProvenance(projectDir, sourcePath, options, detected) {
|
|
|
1797
2131
|
...(options.sha256 ?? detected?.sha256 ? { sha256: options.sha256 ?? detected?.sha256 } : {}),
|
|
1798
2132
|
...(options.retrievedAt ?? detected?.retrievedAt ? { retrievedAt: options.retrievedAt ?? detected?.retrievedAt } : {}),
|
|
1799
2133
|
...(options.resolveCandidate ?? detected?.resolveCandidate ? { resolveCandidate: options.resolveCandidate ?? detected?.resolveCandidate } : {}),
|
|
1800
|
-
...(
|
|
2134
|
+
...(evidence.length > 0 ? { evidence } : {}),
|
|
1801
2135
|
checkedAt: new Date().toISOString()
|
|
1802
2136
|
};
|
|
1803
2137
|
}
|
|
@@ -1971,7 +2305,7 @@ function createReleaseAssetQualityWarnings(asset) {
|
|
|
1971
2305
|
if (asset.materialMetadata?.some((material) => !material.visible || !material.readable)) {
|
|
1972
2306
|
warnings.push(`${asset.id}: release primary model has invisible or unreadable material metadata.`);
|
|
1973
2307
|
}
|
|
1974
|
-
if (["glb", "gltf"].includes(asset.format) && asset.textures.length === 0) {
|
|
2308
|
+
if (["glb", "gltf"].includes(asset.format) && asset.textures.length === 0 && !hasHashBoundFlatColorMaterialEvidence(asset)) {
|
|
1975
2309
|
warnings.push(`${asset.id}: release primary model has no texture references; use only with explicit material/readability evidence.`);
|
|
1976
2310
|
}
|
|
1977
2311
|
return warnings;
|
|
@@ -2130,10 +2464,30 @@ function hasValidManifestOrientationOverride(asset) {
|
|
|
2130
2464
|
asset.orientation?.source === "manifest-override";
|
|
2131
2465
|
}
|
|
2132
2466
|
function releaseStoredAssetWarnings(asset) {
|
|
2133
|
-
|
|
2134
|
-
if (
|
|
2135
|
-
|
|
2136
|
-
|
|
2467
|
+
let warnings = asset.warnings ?? [];
|
|
2468
|
+
if (hasValidManifestOrientationOverride(asset)) {
|
|
2469
|
+
warnings = warnings.filter((warning) => !/orientation metadata missing; facing direction cannot be validated/i.test(warning));
|
|
2470
|
+
}
|
|
2471
|
+
if (hasHashBoundFlatColorMaterialEvidence(asset)) {
|
|
2472
|
+
warnings = warnings.filter((warning) => !/^no texture references detected$/i.test(warning.trim()));
|
|
2473
|
+
}
|
|
2474
|
+
return warnings;
|
|
2475
|
+
}
|
|
2476
|
+
function hasHashBoundFlatColorMaterialEvidence(asset) {
|
|
2477
|
+
if (asset.materials.length === 0)
|
|
2478
|
+
return false;
|
|
2479
|
+
const materialMetadata = asset.materialMetadata ?? [];
|
|
2480
|
+
if (materialMetadata.length < asset.materials.length)
|
|
2481
|
+
return false;
|
|
2482
|
+
if (materialMetadata.some((material) => !nonEmpty(material.name) || material.visible !== true || material.readable !== true ||
|
|
2483
|
+
typeof material.opacity !== "number" || material.opacity <= 0))
|
|
2484
|
+
return false;
|
|
2485
|
+
const probe = asset.renderedProbe;
|
|
2486
|
+
return nonEmpty(probe?.url) &&
|
|
2487
|
+
nonEmpty(probe?.sha256) &&
|
|
2488
|
+
nonEmpty(probe?.assetHash) &&
|
|
2489
|
+
probe?.assetHash === asset.hash &&
|
|
2490
|
+
["browser-screenshot", "aura-probe-render"].includes(String(probe?.kind));
|
|
2137
2491
|
}
|
|
2138
2492
|
function createRenderedProbeForegroundWarnings(assetId, foregroundBounds, probeMetrics) {
|
|
2139
2493
|
if (!foregroundBounds)
|
|
@@ -2192,7 +2546,9 @@ function createRoleAwareReleaseQualityWarnings(projectDir, manifest, asset) {
|
|
|
2192
2546
|
if (isHugeForReleaseRole(role, dimensions) && !hasHashBoundNormalizationEvidence(asset)) {
|
|
2193
2547
|
warnings.push(`${asset.id}: role-aware release ${role} validation found huge bounds without explicit normalization evidence.`);
|
|
2194
2548
|
}
|
|
2195
|
-
if ((role === "track" || role === "world" || role === "environment") &&
|
|
2549
|
+
if ((role === "track" || role === "world" || role === "environment") &&
|
|
2550
|
+
!hasMeaningfulWorldExtent(dimensions) &&
|
|
2551
|
+
!hasCurrentMeshExtractedGameplayExtent(asset, role)) {
|
|
2196
2552
|
warnings.push(`${asset.id}: role-aware release ${role} validation needs a gameplay-scale footprint/extent.`);
|
|
2197
2553
|
}
|
|
2198
2554
|
}
|
|
@@ -2258,6 +2614,26 @@ function hasMeaningfulWorldExtent(size) {
|
|
|
2258
2614
|
const horizontalArea = Math.abs(size[0] * size[2]);
|
|
2259
2615
|
return Math.max(size[0], size[2]) >= 10 && horizontalArea >= 100;
|
|
2260
2616
|
}
|
|
2617
|
+
function hasCurrentMeshExtractedGameplayExtent(asset, role) {
|
|
2618
|
+
const geometry = asset.gameGeometry;
|
|
2619
|
+
if (!geometry || geometry.evidence?.manifestHash !== asset.hash || (geometry.evidence.blockers?.length ?? 0) > 0)
|
|
2620
|
+
return false;
|
|
2621
|
+
if (role === "track" && geometry.certification === "certified-racing-track") {
|
|
2622
|
+
const topology = geometry.racingTopology;
|
|
2623
|
+
return topology?.source === "asset-mesh-extracted" &&
|
|
2624
|
+
topology.assetHash === asset.hash &&
|
|
2625
|
+
typeof topology.lapLengthMeters === "number" &&
|
|
2626
|
+
topology.lapLengthMeters >= 10;
|
|
2627
|
+
}
|
|
2628
|
+
if ((role === "world" || role === "environment") && geometry.certification === "certified-platformer-world") {
|
|
2629
|
+
const surfaceMap = geometry.playableSurfaceMap;
|
|
2630
|
+
return surfaceMap?.source === "asset-mesh-extracted" &&
|
|
2631
|
+
surfaceMap.assetHash === asset.hash &&
|
|
2632
|
+
typeof surfaceMap.levelLength === "number" &&
|
|
2633
|
+
surfaceMap.levelLength >= 10;
|
|
2634
|
+
}
|
|
2635
|
+
return false;
|
|
2636
|
+
}
|
|
2261
2637
|
function hasHashBoundNormalizationEvidence(asset) {
|
|
2262
2638
|
const probe = asset.renderedProbe;
|
|
2263
2639
|
const foregroundBounds = probe?.foregroundBounds;
|