@aura3d/cli 1.4.3 → 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/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: inspection.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
- ...(detected?.evidence && detected.evidence.length > 0 ? { evidence: detected.evidence } : {}),
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
- 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));
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") && !hasMeaningfulWorldExtent(dimensions)) {
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;