@aura3d/engine 1.0.3 → 1.0.6

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 (210) hide show
  1. package/README.md +338 -14
  2. package/dist/animation/AnimationClipEvents.d.ts +57 -0
  3. package/dist/animation/AnimationClipEvents.d.ts.map +1 -0
  4. package/dist/animation/AnimationClipEvents.js +171 -0
  5. package/dist/animation/AnimationClipEvents.js.map +1 -0
  6. package/dist/animation/AnimationClipRegistry.d.ts +76 -0
  7. package/dist/animation/AnimationClipRegistry.d.ts.map +1 -0
  8. package/dist/animation/AnimationClipRegistry.js +130 -0
  9. package/dist/animation/AnimationClipRegistry.js.map +1 -0
  10. package/dist/animation/AnimationController.d.ts +168 -0
  11. package/dist/animation/AnimationController.d.ts.map +1 -0
  12. package/dist/animation/AnimationController.js +619 -0
  13. package/dist/animation/AnimationController.js.map +1 -0
  14. package/dist/animation/AnimationStateGraph.d.ts +2 -0
  15. package/dist/animation/AnimationStateGraph.d.ts.map +1 -0
  16. package/dist/animation/AnimationStateGraph.js +2 -0
  17. package/dist/animation/AnimationStateGraph.js.map +1 -0
  18. package/dist/animation/AnimationStateMachine.d.ts +16 -0
  19. package/dist/animation/AnimationStateMachine.d.ts.map +1 -1
  20. package/dist/animation/AnimationStateMachine.js +69 -7
  21. package/dist/animation/AnimationStateMachine.js.map +1 -1
  22. package/dist/animation/HumanoidRetargeting.d.ts +76 -0
  23. package/dist/animation/HumanoidRetargeting.d.ts.map +1 -0
  24. package/dist/animation/HumanoidRetargeting.js +331 -0
  25. package/dist/animation/HumanoidRetargeting.js.map +1 -0
  26. package/dist/animation/browser-index.d.ts +18 -0
  27. package/dist/animation/browser-index.d.ts.map +1 -1
  28. package/dist/animation/browser-index.js +13 -0
  29. package/dist/animation/browser-index.js.map +1 -1
  30. package/dist/animation/index.d.ts +17 -1
  31. package/dist/animation/index.d.ts.map +1 -1
  32. package/dist/animation/index.js +12 -1
  33. package/dist/animation/index.js.map +1 -1
  34. package/dist/animation/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -1
  35. package/dist/animation/threejs-compatibility/AnimationDiagnostics.js +3 -5
  36. package/dist/animation/threejs-compatibility/AnimationDiagnostics.js.map +1 -1
  37. package/dist/assets/GLTFAnimationRuntime.js +1 -1
  38. package/dist/assets/GLTFLoader.js +1 -1
  39. package/dist/aura3d-cli/cli.js +225 -8
  40. package/dist/aura3d-cli/cli.js.map +1 -1
  41. package/dist/aura3d-cli/index.d.ts +283 -3
  42. package/dist/aura3d-cli/index.d.ts.map +1 -1
  43. package/dist/aura3d-cli/index.js +1028 -4
  44. package/dist/aura3d-cli/index.js.map +1 -1
  45. package/dist/aura3d-cli/pull-bridge.d.ts +108 -0
  46. package/dist/aura3d-cli/pull-bridge.d.ts.map +1 -0
  47. package/dist/aura3d-cli/pull-bridge.js +333 -0
  48. package/dist/aura3d-cli/pull-bridge.js.map +1 -0
  49. package/dist/create-aura3d/index.d.ts +1 -1
  50. package/dist/create-aura3d/index.d.ts.map +1 -1
  51. package/dist/create-aura3d/index.js +9 -2
  52. package/dist/create-aura3d/index.js.map +1 -1
  53. package/dist/editor-runtime/ProjectSerializer.d.ts +74 -1
  54. package/dist/editor-runtime/ProjectSerializer.d.ts.map +1 -1
  55. package/dist/editor-runtime/ProjectSerializer.js +123 -6
  56. package/dist/editor-runtime/ProjectSerializer.js.map +1 -1
  57. package/dist/editor-runtime/TimelineModel.d.ts +18 -0
  58. package/dist/editor-runtime/TimelineModel.d.ts.map +1 -1
  59. package/dist/editor-runtime/TimelineModel.js +67 -3
  60. package/dist/editor-runtime/TimelineModel.js.map +1 -1
  61. package/dist/editor-runtime/TimelineRuntimeBridge.d.ts +98 -0
  62. package/dist/editor-runtime/TimelineRuntimeBridge.d.ts.map +1 -0
  63. package/dist/editor-runtime/TimelineRuntimeBridge.js +186 -0
  64. package/dist/editor-runtime/TimelineRuntimeBridge.js.map +1 -0
  65. package/dist/editor-runtime/index.d.ts +3 -1
  66. package/dist/editor-runtime/index.d.ts.map +1 -1
  67. package/dist/editor-runtime/index.js +1 -0
  68. package/dist/editor-runtime/index.js.map +1 -1
  69. package/dist/engine/agent-api/AnimationController.d.ts +607 -0
  70. package/dist/engine/agent-api/AnimationController.d.ts.map +1 -0
  71. package/dist/engine/agent-api/AnimationController.js +2192 -0
  72. package/dist/engine/agent-api/AnimationController.js.map +1 -0
  73. package/dist/engine/agent-api/AssetEvidence.d.ts +88 -0
  74. package/dist/engine/agent-api/AssetEvidence.d.ts.map +1 -0
  75. package/dist/engine/agent-api/AssetEvidence.js +157 -0
  76. package/dist/engine/agent-api/AssetEvidence.js.map +1 -0
  77. package/dist/engine/agent-api/AuraAppHandle.d.ts +55 -0
  78. package/dist/engine/agent-api/AuraAppHandle.d.ts.map +1 -0
  79. package/dist/engine/agent-api/AuraAppHandle.js +15 -0
  80. package/dist/engine/agent-api/AuraAppHandle.js.map +1 -0
  81. package/dist/engine/agent-api/AuraVoiceBridge.d.ts +96 -0
  82. package/dist/engine/agent-api/AuraVoiceBridge.d.ts.map +1 -0
  83. package/dist/engine/agent-api/AuraVoiceBridge.js +370 -0
  84. package/dist/engine/agent-api/AuraVoiceBridge.js.map +1 -0
  85. package/dist/engine/agent-api/CartoonDirector.d.ts +95 -0
  86. package/dist/engine/agent-api/CartoonDirector.d.ts.map +1 -0
  87. package/dist/engine/agent-api/CartoonDirector.js +342 -0
  88. package/dist/engine/agent-api/CartoonDirector.js.map +1 -0
  89. package/dist/engine/agent-api/CartoonPerformance.d.ts +149 -0
  90. package/dist/engine/agent-api/CartoonPerformance.d.ts.map +1 -0
  91. package/dist/engine/agent-api/CartoonPerformance.js +317 -0
  92. package/dist/engine/agent-api/CartoonPerformance.js.map +1 -0
  93. package/dist/engine/agent-api/CartoonRenderQueue.d.ts +132 -0
  94. package/dist/engine/agent-api/CartoonRenderQueue.d.ts.map +1 -0
  95. package/dist/engine/agent-api/CartoonRenderQueue.js +385 -0
  96. package/dist/engine/agent-api/CartoonRenderQueue.js.map +1 -0
  97. package/dist/engine/agent-api/CharacterAssembly.d.ts +126 -0
  98. package/dist/engine/agent-api/CharacterAssembly.d.ts.map +1 -0
  99. package/dist/engine/agent-api/CharacterAssembly.js +280 -0
  100. package/dist/engine/agent-api/CharacterAssembly.js.map +1 -0
  101. package/dist/engine/agent-api/DialoguePerformance.d.ts +150 -0
  102. package/dist/engine/agent-api/DialoguePerformance.d.ts.map +1 -0
  103. package/dist/engine/agent-api/DialoguePerformance.js +335 -0
  104. package/dist/engine/agent-api/DialoguePerformance.js.map +1 -0
  105. package/dist/engine/agent-api/FrameLoop.d.ts +70 -0
  106. package/dist/engine/agent-api/FrameLoop.d.ts.map +1 -0
  107. package/dist/engine/agent-api/FrameLoop.js +165 -0
  108. package/dist/engine/agent-api/FrameLoop.js.map +1 -0
  109. package/dist/engine/agent-api/GameAppRuntime.d.ts +62 -0
  110. package/dist/engine/agent-api/GameAppRuntime.d.ts.map +1 -0
  111. package/dist/engine/agent-api/GameAppRuntime.js +189 -0
  112. package/dist/engine/agent-api/GameAppRuntime.js.map +1 -0
  113. package/dist/engine/agent-api/GameAssetValidation.d.ts +279 -0
  114. package/dist/engine/agent-api/GameAssetValidation.d.ts.map +1 -0
  115. package/dist/engine/agent-api/GameAssetValidation.js +719 -0
  116. package/dist/engine/agent-api/GameAssetValidation.js.map +1 -0
  117. package/dist/engine/agent-api/GameEvidence.d.ts +148 -0
  118. package/dist/engine/agent-api/GameEvidence.d.ts.map +1 -0
  119. package/dist/engine/agent-api/GameEvidence.js +269 -0
  120. package/dist/engine/agent-api/GameEvidence.js.map +1 -0
  121. package/dist/engine/agent-api/GameRuntime.d.ts +931 -0
  122. package/dist/engine/agent-api/GameRuntime.d.ts.map +1 -0
  123. package/dist/engine/agent-api/GameRuntime.js +2229 -0
  124. package/dist/engine/agent-api/GameRuntime.js.map +1 -0
  125. package/dist/engine/agent-api/GameSceneBridge.d.ts +54 -0
  126. package/dist/engine/agent-api/GameSceneBridge.d.ts.map +1 -0
  127. package/dist/engine/agent-api/GameSceneBridge.js +110 -0
  128. package/dist/engine/agent-api/GameSceneBridge.js.map +1 -0
  129. package/dist/engine/agent-api/PromptAnimationContract.d.ts +278 -0
  130. package/dist/engine/agent-api/PromptAnimationContract.d.ts.map +1 -0
  131. package/dist/engine/agent-api/PromptAnimationContract.js +238 -0
  132. package/dist/engine/agent-api/PromptAnimationContract.js.map +1 -0
  133. package/dist/engine/agent-api/PromptAnimationEvidence.d.ts +183 -0
  134. package/dist/engine/agent-api/PromptAnimationEvidence.d.ts.map +1 -0
  135. package/dist/engine/agent-api/PromptAnimationEvidence.js +454 -0
  136. package/dist/engine/agent-api/PromptAnimationEvidence.js.map +1 -0
  137. package/dist/engine/agent-api/RuntimeNodeHandle.d.ts +100 -0
  138. package/dist/engine/agent-api/RuntimeNodeHandle.d.ts.map +1 -0
  139. package/dist/engine/agent-api/RuntimeNodeHandle.js +36 -0
  140. package/dist/engine/agent-api/RuntimeNodeHandle.js.map +1 -0
  141. package/dist/engine/agent-api/ShotTimeline.d.ts +179 -0
  142. package/dist/engine/agent-api/ShotTimeline.d.ts.map +1 -0
  143. package/dist/engine/agent-api/ShotTimeline.js +264 -0
  144. package/dist/engine/agent-api/ShotTimeline.js.map +1 -0
  145. package/dist/engine/agent-api/VisemeController.d.ts +89 -0
  146. package/dist/engine/agent-api/VisemeController.d.ts.map +1 -0
  147. package/dist/engine/agent-api/VisemeController.js +207 -0
  148. package/dist/engine/agent-api/VisemeController.js.map +1 -0
  149. package/dist/engine/agent-api/game-kits/fighting.d.ts +123 -0
  150. package/dist/engine/agent-api/game-kits/fighting.d.ts.map +1 -0
  151. package/dist/engine/agent-api/game-kits/fighting.js +483 -0
  152. package/dist/engine/agent-api/game-kits/fighting.js.map +1 -0
  153. package/dist/engine/agent-api/game-kits/index.d.ts +15 -0
  154. package/dist/engine/agent-api/game-kits/index.d.ts.map +1 -0
  155. package/dist/engine/agent-api/game-kits/index.js +6 -0
  156. package/dist/engine/agent-api/game-kits/index.js.map +1 -0
  157. package/dist/engine/agent-api/humanoid-walk-runtime.d.ts +1 -0
  158. package/dist/engine/agent-api/humanoid-walk-runtime.d.ts.map +1 -1
  159. package/dist/engine/agent-api/index.d.ts +495 -1
  160. package/dist/engine/agent-api/index.d.ts.map +1 -1
  161. package/dist/engine/agent-api/index.js +752 -6
  162. package/dist/engine/agent-api/index.js.map +1 -1
  163. package/dist/engine/agent-api/product-viewer-runtime.d.ts.map +1 -1
  164. package/dist/index.d.ts +1 -0
  165. package/dist/index.js +1 -0
  166. package/dist/physics/CollisionVolumes.d.ts +57 -0
  167. package/dist/physics/CollisionVolumes.d.ts.map +1 -0
  168. package/dist/physics/CollisionVolumes.js +159 -0
  169. package/dist/physics/CollisionVolumes.js.map +1 -0
  170. package/dist/physics/HitboxWorld.d.ts +250 -0
  171. package/dist/physics/HitboxWorld.d.ts.map +1 -0
  172. package/dist/physics/HitboxWorld.js +771 -0
  173. package/dist/physics/HitboxWorld.js.map +1 -0
  174. package/dist/physics/KinematicBody.d.ts +157 -0
  175. package/dist/physics/KinematicBody.d.ts.map +1 -0
  176. package/dist/physics/KinematicBody.js +405 -0
  177. package/dist/physics/KinematicBody.js.map +1 -0
  178. package/dist/physics/KinematicWorld.d.ts +58 -0
  179. package/dist/physics/KinematicWorld.d.ts.map +1 -0
  180. package/dist/physics/KinematicWorld.js +246 -0
  181. package/dist/physics/KinematicWorld.js.map +1 -0
  182. package/dist/physics/index.d.ts +4 -0
  183. package/dist/physics/index.d.ts.map +1 -1
  184. package/dist/physics/index.js +4 -0
  185. package/dist/physics/index.js.map +1 -1
  186. package/dist/rendering/ForwardPass.js +2 -2
  187. package/dist/rendering/ShaderLibrary.js +2 -2
  188. package/dist/rendering/SkinnedLitMaterial.js +3 -3
  189. package/dist/rendering/SkinnedUnlitMaterial.js +3 -3
  190. package/dist/scene/Renderable.js +2 -2
  191. package/dist/scripting/VisualGraph.d.ts +2 -1
  192. package/dist/scripting/VisualGraph.d.ts.map +1 -1
  193. package/dist/scripting/VisualGraph.js +118 -1
  194. package/dist/scripting/VisualGraph.js.map +1 -1
  195. package/dist/scripting/VisualGraphContext.d.ts +123 -0
  196. package/dist/scripting/VisualGraphContext.d.ts.map +1 -0
  197. package/dist/scripting/VisualGraphContext.js +2 -0
  198. package/dist/scripting/VisualGraphContext.js.map +1 -0
  199. package/dist/scripting/VisualGraphExecutor.d.ts +6 -1
  200. package/dist/scripting/VisualGraphExecutor.d.ts.map +1 -1
  201. package/dist/scripting/VisualGraphExecutor.js +364 -7
  202. package/dist/scripting/VisualGraphExecutor.js.map +1 -1
  203. package/dist/scripting/VisualNodeCatalog.d.ts +1 -1
  204. package/dist/scripting/VisualNodeCatalog.d.ts.map +1 -1
  205. package/dist/scripting/VisualNodeCatalog.js +61 -1
  206. package/dist/scripting/VisualNodeCatalog.js.map +1 -1
  207. package/dist/scripting/index.d.ts +1 -0
  208. package/dist/scripting/index.d.ts.map +1 -1
  209. package/dist/scripting/index.js.map +1 -1
  210. package/package.json +203 -118
@@ -38,10 +38,19 @@ export function addAsset(options) {
38
38
  hash: `sha256-${hash}`,
39
39
  sizeBytes: statSync(sourcePath).size,
40
40
  bounds: inspection.bounds,
41
+ boundsMetadata: inspection.boundsMetadata,
41
42
  materials: inspection.materials,
43
+ materialMetadata: inspection.materialMetadata,
42
44
  animations: inspection.animations,
45
+ animationMetadata: inspection.animation,
46
+ humanoid: inspection.humanoid,
47
+ skeleton: inspection.skeleton,
48
+ morphTargets: inspection.morphTargets,
49
+ provenance: createAssetProvenance(projectDir, sourcePath, options, inspection.provenance),
43
50
  textures: inspection.textures,
44
51
  dependencies: inspection.dependencies,
52
+ orientation: inspection.orientation,
53
+ nodeNames: inspection.nodeNames,
45
54
  thumbnailUrl,
46
55
  warnings: createAssetWarnings(sourcePath, inspection)
47
56
  };
@@ -91,14 +100,55 @@ export function scanAssets(options) {
91
100
  messages: ["No supported assets found."]
92
101
  };
93
102
  }
103
+ export function inspectAsset(options) {
104
+ const projectDir = resolve(options.projectDir ?? process.cwd());
105
+ const sourcePath = resolve(projectDir, options.file);
106
+ if (!existsSync(sourcePath)) {
107
+ throw new Error(`Aura3D assets inspect failed: "${options.file}" does not exist.`);
108
+ }
109
+ const format = extname(sourcePath).slice(1).toLowerCase();
110
+ const inspection = inspectAssetFile(sourcePath, format);
111
+ const warnings = createAssetWarnings(sourcePath, inspection);
112
+ return {
113
+ ok: warnings.length === 0,
114
+ schema: "aura3d.asset-inspection/1.0",
115
+ file: normalizeRelativePath(relative(projectDir, sourcePath)),
116
+ format,
117
+ sizeBytes: statSync(sourcePath).size,
118
+ bounds: inspection.bounds,
119
+ boundsMetadata: inspection.boundsMetadata,
120
+ materials: inspection.materials,
121
+ materialMetadata: inspection.materialMetadata,
122
+ animations: inspection.animations,
123
+ ...(options.animation ? { animation: inspection.animation } : {}),
124
+ ...(options.humanoid ? { humanoid: inspection.humanoid } : {}),
125
+ ...(options.skeleton ? { skeleton: inspection.skeleton } : {}),
126
+ ...(options.morphs ? { morphTargets: inspection.morphTargets } : {}),
127
+ ...(options.license ? { provenance: createAssetProvenance(projectDir, sourcePath, {}, inspection.provenance) } : {}),
128
+ textures: inspection.textures,
129
+ orientation: inspection.orientation,
130
+ nodeNames: inspection.nodeNames,
131
+ dependencies: inspection.dependencies,
132
+ warnings,
133
+ messages: warnings.length === 0 ? ["Asset inspection completed."] : warnings
134
+ };
135
+ }
94
136
  export function validateAssets(options = {}) {
95
137
  const projectDir = resolve(options.projectDir ?? process.cwd());
96
138
  const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
97
139
  const manifestMissing = !existsSync(manifestPath);
98
- const manifest = readAssetManifest(projectDir);
140
+ const sourceManifest = readAssetManifest(projectDir);
141
+ const manifest = filterAssetManifest(sourceManifest, options.assetIds);
142
+ const externalProvenance = readExternalProvenance(projectDir, options.provenanceFile);
99
143
  const failures = manifestMissing
100
144
  ? [`Missing ${DEFAULT_AURA_ASSET_MANIFEST}. Suggested fix: run aura3d assets add ./asset.glb --name product or aura3d assets scan ./assets.`]
101
145
  : [];
146
+ const missingAssetIds = findMissingAssetIds(sourceManifest, options.assetIds);
147
+ for (const id of missingAssetIds)
148
+ failures.push(`Requested asset "${id}" was not found in ${DEFAULT_AURA_ASSET_MANIFEST}.`);
149
+ if (options.provenanceFile && !existsSync(resolve(projectDir, options.provenanceFile))) {
150
+ failures.push(`Missing asset provenance evidence file: ${options.provenanceFile}`);
151
+ }
102
152
  const warnings = [];
103
153
  for (const asset of manifest.assets) {
104
154
  const outputPath = resolve(projectDir, asset.outputPath);
@@ -109,6 +159,13 @@ export function validateAssets(options = {}) {
109
159
  const actualHash = `sha256-${hashFile(outputPath)}`;
110
160
  if (actualHash !== asset.hash)
111
161
  failures.push(`Hash mismatch for "${asset.id}": expected ${asset.hash}, found ${actualHash}`);
162
+ const provenance = resolveAssetProvenance(asset, externalProvenance);
163
+ if (options.noPlaceholders && isPlaceholderAsset(asset, provenance)) {
164
+ failures.push(`Placeholder asset is not allowed in strict release validation: "${asset.id}". Replace it with a real typed asset and provenance.`);
165
+ }
166
+ if (options.requireLicense && !hasUsableLicenseEvidence(provenance)) {
167
+ failures.push(`Missing license/provenance evidence for "${asset.id}". Add it with assets add --license ... --source-url ... or pass --provenance <evidence.json>.`);
168
+ }
112
169
  warnings.push(...asset.warnings.map((warning) => `${asset.id}: ${warning}`));
113
170
  if (asset.format === "gltf") {
114
171
  for (const dependency of asset.dependencies ?? asset.textures) {
@@ -145,6 +202,22 @@ export function writeTypedAssets(projectDir, manifest = readAssetManifest(projec
145
202
  const metadata = {
146
203
  materials: asset.materials,
147
204
  animations: asset.animations,
205
+ animationClips: asset.animations,
206
+ animationMetadata: asset.animationMetadata ?? createReadinessAnimationMetadata(asset.animations),
207
+ humanoid: asset.humanoid?.humanoid ?? false,
208
+ humanoidStatus: asset.humanoid?.status ?? "unknown",
209
+ humanoidConfidence: asset.humanoid?.confidence ?? "low",
210
+ skeleton: asset.skeleton,
211
+ morphTargets: asset.morphTargets,
212
+ provenance: asset.provenance,
213
+ sourcePath: asset.source,
214
+ outputPath: asset.outputPath,
215
+ license: asset.provenance?.license,
216
+ author: asset.provenance?.author,
217
+ boundsMetadata: asset.boundsMetadata,
218
+ materialMetadata: asset.materialMetadata,
219
+ orientation: asset.orientation,
220
+ nodeNames: asset.nodeNames ?? [],
148
221
  textures: asset.textures,
149
222
  dependencies: asset.dependencies ?? [],
150
223
  thumbnailUrl: asset.thumbnailUrl
@@ -228,6 +301,97 @@ export function checkDeploy(options = {}) {
228
301
  messages: failures.length === 0 ? ["Deploy check passed."] : failures
229
302
  };
230
303
  }
304
+ export function validateGameAssets(options = {}) {
305
+ return validateAssetReadiness("game", options);
306
+ }
307
+ export function validateCartoonAssets(options = {}) {
308
+ return validateAssetReadiness("cartoon", options);
309
+ }
310
+ export function createCharacterAssemblyPlan(options) {
311
+ const projectDir = resolve(options.projectDir ?? process.cwd());
312
+ const manifest = readAssetManifest(projectDir);
313
+ const failures = [];
314
+ const warnings = [];
315
+ const output = normalizeRelativePath(options.output ?? `src/aura-character-${sanitizeAssetId(options.name)}.assembly.json`);
316
+ const bodyAsset = manifest.assets.find((asset) => asset.id === options.body);
317
+ if (!bodyAsset) {
318
+ failures.push(`Missing body asset "${options.body}". Add it first with aura3d assets add ./body.glb --name ${options.body}.`);
319
+ }
320
+ else if (bodyAsset.type !== "model") {
321
+ failures.push(`Body asset "${options.body}" must be a model asset, found ${bodyAsset.type}.`);
322
+ }
323
+ else if (bodyAsset.humanoid && !bodyAsset.humanoid.humanoid) {
324
+ warnings.push(`Body asset "${options.body}" has humanoid status "${bodyAsset.humanoid.status}"; character assembly can still compose parts, but acting and retargeting may be limited.`);
325
+ }
326
+ const resolvePart = (part) => {
327
+ const asset = manifest.assets.find((entry) => entry.id === part.asset);
328
+ if (!asset) {
329
+ failures.push(`Missing ${part.slot} part asset "${part.asset}".`);
330
+ return undefined;
331
+ }
332
+ if (asset.type !== "model")
333
+ warnings.push(`${part.slot}: "${part.asset}" is ${asset.type}; character assembly expects model parts for rig/attachment safety.`);
334
+ return {
335
+ slot: part.slot,
336
+ asset: part.asset,
337
+ url: asset.url,
338
+ type: asset.type,
339
+ format: asset.format,
340
+ animations: asset.animations,
341
+ humanoid: asset.humanoid,
342
+ attachTo: part.attachTo ?? defaultAttachPoint(part.slot)
343
+ };
344
+ };
345
+ const parts = (options.parts ?? []).map(resolvePart).filter((part) => Boolean(part));
346
+ const body = bodyAsset
347
+ ? {
348
+ slot: "body",
349
+ asset: bodyAsset.id,
350
+ url: bodyAsset.url,
351
+ type: bodyAsset.type,
352
+ format: bodyAsset.format,
353
+ animations: bodyAsset.animations,
354
+ humanoid: bodyAsset.humanoid,
355
+ attachTo: "root"
356
+ }
357
+ : {
358
+ slot: "body",
359
+ asset: options.body,
360
+ url: "",
361
+ type: "model",
362
+ format: "missing",
363
+ animations: [],
364
+ attachTo: "root"
365
+ };
366
+ const plan = {
367
+ schema: "aura3d.character-assembly/1.0",
368
+ name: options.name,
369
+ output,
370
+ scale: options.scale ?? 1,
371
+ body,
372
+ parts,
373
+ rules: {
374
+ normalizeScale: true,
375
+ facePositiveZ: true,
376
+ preserveTypedAssetReferences: true,
377
+ requireNamedAttachments: true
378
+ }
379
+ };
380
+ mkdirSync(dirname(resolve(projectDir, output)), { recursive: true });
381
+ writeFileSync(resolve(projectDir, output), `${JSON.stringify(plan, null, 2)}\n`);
382
+ return {
383
+ ok: failures.length === 0,
384
+ schema: "aura3d.character-assembly/1.0",
385
+ name: options.name,
386
+ output,
387
+ body,
388
+ parts,
389
+ validation: { failures, warnings },
390
+ messages: failures.length === 0
391
+ ? [`Wrote ${output}. Import typed assets from src/aura-assets.ts and compose with model(assets.${options.body}).`]
392
+ : failures
393
+ };
394
+ }
231
395
  export function initAgentFiles(options) {
232
396
  const projectDir = resolve(options.projectDir ?? process.cwd());
233
397
  const targets = options.agent === "all" ? ["generic", "claude", "cursor", "copilot"] : [options.agent];
@@ -244,6 +408,481 @@ export function initAgentFiles(options) {
244
408
  }
245
409
  return written;
246
410
  }
411
+ function validateAssetReadiness(profile, options) {
412
+ const projectDir = resolve(options.projectDir ?? process.cwd());
413
+ const gameProfile = profile === "game" ? options.gameProfile : undefined;
414
+ const sourceManifest = readAssetManifest(projectDir);
415
+ const manifest = filterAssetManifest(sourceManifest, options.assetIds);
416
+ const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
417
+ const evidencePath = options.output ? resolve(projectDir, options.output) : undefined;
418
+ const validation = validateAssets({
419
+ projectDir,
420
+ noPlaceholders: options.noPlaceholders,
421
+ requireLicense: options.requireLicense,
422
+ provenanceFile: options.provenanceFile,
423
+ assetIds: options.assetIds
424
+ });
425
+ const externalProvenance = readExternalProvenance(projectDir, options.provenanceFile);
426
+ const failures = [...validation.failures];
427
+ const warnings = [...validation.warnings];
428
+ const modelAssets = manifest.assets.filter((asset) => asset.type === "model");
429
+ const animatedModels = modelAssets.filter((asset) => asset.animations.length > 0);
430
+ const animationClips = manifest.assets.reduce((total, asset) => total + asset.animations.length, 0);
431
+ const humanoidModels = modelAssets.filter((asset) => asset.humanoid?.humanoid).length;
432
+ const artifacts = createReadinessArtifacts(projectDir, manifest, evidencePath);
433
+ const assets = manifest.assets.map((asset) => {
434
+ const provenance = resolveAssetProvenance(asset, externalProvenance);
435
+ const placeholderFree = !isPlaceholderAsset(asset, provenance);
436
+ const licenseVerified = hasUsableLicenseEvidence(provenance);
437
+ const assetWarnings = [...asset.warnings];
438
+ const readinessIssues = createAssetReadinessIssues(profile, asset);
439
+ const profileIssues = gameProfile === "fighting-character"
440
+ ? createFightingCharacterReadinessIssues(asset, provenance, licenseVerified)
441
+ : { failures: [], warnings: [] };
442
+ if (asset.type === "model" && !asset.bounds)
443
+ assetWarnings.push("Missing bounds; camera framing, collision proxies, and thumbnail composition will be weaker.");
444
+ if (asset.type === "model" && asset.materials.length === 0)
445
+ assetWarnings.push("No material names detected; authored visual diagnostics will be limited.");
446
+ if (asset.type === "model" && asset.sizeBytes > 50 * 1024 * 1024)
447
+ assetWarnings.push("Large model over 50MB; consider mesh/texture optimization before browser deployment.");
448
+ if (asset.type === "model" && asset.animations.length > 0 && asset.humanoid?.status === "unknown")
449
+ assetWarnings.push("Animated model has unknown humanoid status; inspect with --humanoid before using it as an acted character.");
450
+ assetWarnings.push(...readinessIssues.warnings);
451
+ assetWarnings.push(...profileIssues.warnings);
452
+ pushUnique(failures, [...readinessIssues.failures, ...profileIssues.failures]);
453
+ pushUnique(warnings, [...readinessIssues.warnings, ...profileIssues.warnings]);
454
+ const gameReady = asset.type === "model" && Boolean(asset.bounds) && asset.materials.length > 0 && asset.sizeBytes <= 50 * 1024 * 1024 && readinessIssues.failures.length === 0 && profileIssues.failures.length === 0;
455
+ const cartoonReady = asset.type === "model"
456
+ ? Boolean(asset.bounds) && (asset.animations.length > 0 || /prop|set|stage|background|environment/i.test(asset.id))
457
+ : asset.type === "audio" || asset.type === "texture";
458
+ const artifactPaths = artifacts.assetFiles.find((entry) => entry.id === asset.id) ?? createReadinessAssetArtifacts(projectDir, manifest, asset);
459
+ return {
460
+ id: asset.id,
461
+ type: asset.type,
462
+ format: asset.format,
463
+ source: asset.source,
464
+ outputPath: asset.outputPath,
465
+ url: asset.url,
466
+ sizeBytes: asset.sizeBytes,
467
+ bounds: asset.bounds,
468
+ boundsMetadata: asset.boundsMetadata,
469
+ animations: asset.animations,
470
+ animation: createReadinessAnimationMetadata(asset.animations),
471
+ animationMetadata: asset.animationMetadata,
472
+ humanoid: asset.humanoid,
473
+ skeleton: asset.skeleton,
474
+ morphTargets: asset.morphTargets,
475
+ provenance,
476
+ placeholderFree,
477
+ licenseVerified,
478
+ materials: asset.materials,
479
+ materialMetadata: asset.materialMetadata,
480
+ textures: asset.textures,
481
+ orientation: asset.orientation,
482
+ nodeNames: asset.nodeNames,
483
+ artifactPaths,
484
+ gameReady,
485
+ cartoonReady,
486
+ warnings: assetWarnings
487
+ };
488
+ });
489
+ if (profile === "game") {
490
+ if (modelAssets.length === 0)
491
+ failures.push("Game readiness requires at least one typed model asset. Add a GLB/GLTF with aura3d assets add ./fighter.glb --name fighter.");
492
+ if (animatedModels.length === 0)
493
+ warnings.push("No animated model clips detected. Static scenes can ship, but playable character showcases should include idle/walk/attack/hurt clips.");
494
+ if (animatedModels.length > 0 && humanoidModels === 0)
495
+ warnings.push("No humanoid model metadata detected. Character-heavy game routes should confirm humanoid status with assets inspect --humanoid and typed asset metadata.");
496
+ for (const asset of assets) {
497
+ if (asset.type !== "model")
498
+ continue;
499
+ if (!asset.gameReady)
500
+ warnings.push(`${asset.id}: not game-ready yet; expected bounds, named materials, and browser-sized payload.`);
501
+ }
502
+ if (gameProfile === "fighting-character") {
503
+ if (modelAssets.length < 1)
504
+ failures.push("fighting-character profile requires at least one typed fighter model.");
505
+ if (animatedModels.length < modelAssets.length)
506
+ failures.push("fighting-character profile requires every selected model asset to include embedded animation clips.");
507
+ if (humanoidModels < modelAssets.length)
508
+ failures.push("fighting-character profile requires every selected model asset to include humanoid/skeleton metadata.");
509
+ }
510
+ }
511
+ else {
512
+ if (modelAssets.length === 0)
513
+ failures.push("Cartoon readiness requires at least one typed model/set/prop GLB or GLTF.");
514
+ if (animatedModels.length === 0)
515
+ warnings.push("No animated character clips detected. Prompt-to-episode output can use transform animation, but character acting needs skeletal or pose clips.");
516
+ if (animatedModels.length > 0 && humanoidModels === 0)
517
+ warnings.push("No humanoid model metadata detected. Acting-heavy cartoon routes should confirm character rigs with assets inspect --humanoid.");
518
+ const audioAssets = manifest.assets.filter((asset) => asset.type === "audio");
519
+ if (audioAssets.length === 0)
520
+ warnings.push("No audio assets detected. AuraVoice bridge can still reference external narration manifests, but local episode proof is stronger with audio registered.");
521
+ }
522
+ const ok = failures.length === 0;
523
+ const baseMessage = ok
524
+ ? `${profile === "game" ? "Game" : "Cartoon"} asset readiness report completed.`
525
+ : failures;
526
+ const messages = [
527
+ ...(Array.isArray(baseMessage) ? baseMessage : [baseMessage]),
528
+ ...(evidencePath ? [`Wrote asset readiness evidence: ${normalizeRelativePath(relative(projectDir, evidencePath))}`] : [])
529
+ ];
530
+ const report = {
531
+ schema: "aura3d.asset-readiness/1.0",
532
+ profile,
533
+ ...(gameProfile ? { gameProfile } : {}),
534
+ ok,
535
+ status: ok ? "passed" : "failed",
536
+ validator: createReadinessValidatorEvidence(profile),
537
+ checkedAt: new Date().toISOString(),
538
+ manifestPath,
539
+ artifacts,
540
+ contracts: createReadinessValidationContracts(profile),
541
+ summary: {
542
+ totalAssets: manifest.assets.length,
543
+ modelAssets: modelAssets.length,
544
+ animatedModels: animatedModels.length,
545
+ textureAssets: manifest.assets.filter((asset) => asset.type === "texture").length,
546
+ audioAssets: manifest.assets.filter((asset) => asset.type === "audio").length,
547
+ environmentAssets: manifest.assets.filter((asset) => asset.type === "environment").length,
548
+ animationClips,
549
+ humanoidModels
550
+ },
551
+ assets,
552
+ failures,
553
+ warnings,
554
+ messages
555
+ };
556
+ if (evidencePath) {
557
+ mkdirSync(dirname(evidencePath), { recursive: true });
558
+ writeFileSync(evidencePath, `${JSON.stringify(report, null, 2)}\n`);
559
+ }
560
+ return report;
561
+ }
562
+ function filterAssetManifest(manifest, assetIds) {
563
+ const normalized = normalizeAssetIdFilter(assetIds);
564
+ if (normalized.length === 0)
565
+ return manifest;
566
+ const allowed = new Set(normalized);
567
+ return {
568
+ ...manifest,
569
+ assets: manifest.assets.filter((asset) => allowed.has(asset.id))
570
+ };
571
+ }
572
+ function findMissingAssetIds(manifest, assetIds) {
573
+ const normalized = normalizeAssetIdFilter(assetIds);
574
+ if (normalized.length === 0)
575
+ return [];
576
+ const existing = new Set(manifest.assets.map((asset) => asset.id));
577
+ return normalized.filter((id) => !existing.has(id));
578
+ }
579
+ function normalizeAssetIdFilter(assetIds) {
580
+ return [...new Set((assetIds ?? []).map((id) => id.trim()).filter(Boolean))];
581
+ }
582
+ function createReadinessValidatorEvidence(profile) {
583
+ return profile === "game"
584
+ ? {
585
+ id: "aura-clash-game-assets",
586
+ command: "assets validate-game",
587
+ label: "AuraClash game asset validator"
588
+ }
589
+ : {
590
+ id: "aura-voice-cartoon-assets",
591
+ command: "assets validate-cartoon",
592
+ label: "AuraVoice cartoon asset validator"
593
+ };
594
+ }
595
+ function createReadinessValidationContracts(profile) {
596
+ if (profile === "game") {
597
+ return [
598
+ {
599
+ id: "quaternius-game-ready-fighter-validation-contract",
600
+ label: "Quaternius-derived game-ready fighter validation contract",
601
+ profile: "game",
602
+ sourceFamily: "Quaternius",
603
+ intendedUse: "fighter",
604
+ sourceOnly: true,
605
+ requiredChecks: [
606
+ "typed Aura model asset entry generated by assets add",
607
+ "Quaternius provenance or source-family metadata",
608
+ "GLB/GLTF model with browser-sized payload",
609
+ "bounds with grounded pivot and fighter-scale dimensions",
610
+ "forward-facing +z or z orientation before runtime mirroring",
611
+ "humanoid skeleton metadata suitable for retarget diagnostics",
612
+ "readable visible materials and texture budget",
613
+ "thumbnail or first-frame artifact path",
614
+ "non-empty named fighting animation clips",
615
+ "no floating hair-only assembly without a body/head anchor"
616
+ ],
617
+ requiredAnimationClips: ["idle", "walk", "lightPunch"],
618
+ evidenceBoundary: "This CLI contract is source-only. It does not prove a Quaternius fighter passed validation until assets validate-game output and retained runtime/browser evidence are archived."
619
+ }
620
+ ];
621
+ }
622
+ return [
623
+ {
624
+ id: "auravoice-cartoon-character-asset-validation-contract",
625
+ label: "AuraVoice cartoon asset validation contract",
626
+ profile: "cartoon",
627
+ sourceFamily: "AuraVoice",
628
+ intendedUse: "cartoon-character",
629
+ sourceOnly: true,
630
+ requiredChecks: [
631
+ "typed Aura model, texture, audio, or environment asset entry",
632
+ "bounds for model/set composition",
633
+ "animation or transform-ready character metadata",
634
+ "audio or external AuraVoice manifest references for stronger episode proof"
635
+ ],
636
+ evidenceBoundary: "This CLI contract is source-only. It does not prove cartoon route readiness until validate-cartoon output, rendered frames, timing proof, and AuraVoice evidence are archived."
637
+ }
638
+ ];
639
+ }
640
+ function createReadinessArtifacts(projectDir, manifest, evidencePath) {
641
+ const artifacts = {
642
+ manifestPath: resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
643
+ typedAssetsPath: resolve(projectDir, manifest.typegen),
644
+ outputDir: resolve(projectDir, manifest.outputDir),
645
+ assetBasePath: manifest.assetBasePath,
646
+ assetFiles: manifest.assets.map((asset) => createReadinessAssetArtifacts(projectDir, manifest, asset))
647
+ };
648
+ if (evidencePath)
649
+ artifacts.evidencePath = evidencePath;
650
+ return artifacts;
651
+ }
652
+ function createReadinessAssetArtifacts(projectDir, manifest, asset) {
653
+ const artifact = {
654
+ id: asset.id,
655
+ sourcePath: resolve(projectDir, asset.source),
656
+ outputPath: resolve(projectDir, asset.outputPath),
657
+ publicUrl: asset.url,
658
+ dependencyPaths: (asset.dependencies ?? []).map((dependency) => resolve(dirname(resolve(projectDir, asset.outputPath)), dependency))
659
+ };
660
+ if (asset.thumbnailUrl) {
661
+ artifact.thumbnailUrl = asset.thumbnailUrl;
662
+ const thumbnailPath = resolvePublicArtifactPath(projectDir, manifest, asset.thumbnailUrl);
663
+ if (thumbnailPath)
664
+ artifact.thumbnailPath = thumbnailPath;
665
+ }
666
+ return artifact;
667
+ }
668
+ function createReadinessAnimationMetadata(animations) {
669
+ return {
670
+ clipCount: animations.length,
671
+ clips: animations.map((name, index) => ({ index, name }))
672
+ };
673
+ }
674
+ function createAssetReadinessIssues(profile, asset) {
675
+ if (asset.type !== "model")
676
+ return { failures: [], warnings: [] };
677
+ const failures = [];
678
+ const warnings = [];
679
+ const prefix = `${asset.id}:`;
680
+ const characterLike = isCharacterLikeAsset(asset);
681
+ if (profile === "game" && characterLike) {
682
+ const missing = missingRequiredGameClips(asset.animations);
683
+ if (missing.length > 0) {
684
+ failures.push(`${prefix} missing required game animation clip${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}.`);
685
+ }
686
+ }
687
+ const emptyClips = (asset.animationMetadata?.clips ?? []).filter((clip) => clip.channelCount === 0 || clip.samplerCount === 0);
688
+ for (const clip of emptyClips) {
689
+ failures.push(`${prefix} animation clip "${clip.name}" is empty; expected at least one channel and sampler.`);
690
+ }
691
+ const bounds = asset.boundsMetadata;
692
+ if (bounds && bounds.maxDimension > 50) {
693
+ failures.push(`${prefix} oversized bounds detected; largest dimension is ${bounds.maxDimension}m, expected at most 50m for browser game assets.`);
694
+ }
695
+ else if (bounds && characterLike && bounds.maxDimension > 4) {
696
+ warnings.push(`${prefix} character-sized model is unusually large (${bounds.maxDimension}m); confirm scale before using it in gameplay.`);
697
+ }
698
+ if (bounds && characterLike && !bounds.grounded) {
699
+ warnings.push(`${prefix} bounds are not grounded at the pivot; min.y is ${bounds.min[1]}m.`);
700
+ }
701
+ const orientation = asset.orientation;
702
+ if (profile === "game" && characterLike && orientation?.forwardAxis && !["+z", "z"].includes(orientation.forwardAxis.toLowerCase())) {
703
+ failures.push(`${prefix} wrong facing direction "${orientation.forwardAxis}"; fighting-game characters are expected to face +z before runtime mirroring.`);
704
+ }
705
+ const invisibleMaterials = (asset.materialMetadata ?? []).filter((material) => !material.visible || !material.readable);
706
+ for (const material of invisibleMaterials) {
707
+ failures.push(`${prefix} invisible or unreadable material "${material.name}" detected${material.reasons.length ? ` (${material.reasons.join("; ")})` : ""}.`);
708
+ }
709
+ if (profile === "game" && hasFloatingHairRisk(asset)) {
710
+ failures.push(`${prefix} floating hair risk detected; hair-only geometry must be assembled onto a body/head with assets assemble-character before game validation.`);
711
+ }
712
+ return { failures, warnings };
713
+ }
714
+ function createFightingCharacterReadinessIssues(asset, provenance, licenseVerified) {
715
+ const failures = [];
716
+ const warnings = [];
717
+ const prefix = `${asset.id}:`;
718
+ if (asset.type !== "model") {
719
+ failures.push(`${prefix} fighting-character profile requires a model asset, found ${asset.type}.`);
720
+ return { failures, warnings };
721
+ }
722
+ if (asset.format !== "glb" && asset.format !== "gltf") {
723
+ failures.push(`${prefix} fighting-character profile requires GLB/GLTF model input, found ${asset.format}.`);
724
+ }
725
+ if (!licenseVerified) {
726
+ failures.push(`${prefix} fighting-character profile requires verified redistributable license/provenance evidence.`);
727
+ }
728
+ if (!provenance?.sourceUrl && !provenance?.sourceFamily) {
729
+ failures.push(`${prefix} fighting-character profile requires catalog/source provenance for release evidence.`);
730
+ }
731
+ if (asset.animations.length === 0) {
732
+ failures.push(`${prefix} fighting-character profile requires embedded animation clips.`);
733
+ }
734
+ const missing = missingRequiredGameClips(asset.animations);
735
+ if (missing.length > 0) {
736
+ failures.push(`${prefix} fighting-character profile missing required animation clip${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}.`);
737
+ }
738
+ const skeletonJointCount = asset.skeleton?.jointCount ?? 0;
739
+ if (!asset.humanoid?.humanoid && skeletonJointCount < 6) {
740
+ failures.push(`${prefix} fighting-character profile requires humanoid metadata or at least 6 skeleton joints; found ${skeletonJointCount}.`);
741
+ }
742
+ const metadataRisk = findFightingCharacterMetadataRisk(asset);
743
+ if (metadataRisk) {
744
+ failures.push(`${prefix} fighting-character profile rejects ${metadataRisk.kind} metadata "${metadataRisk.term}"; use complete, original, license-safe humanoid fighter assets.`);
745
+ }
746
+ if (!asset.boundsMetadata) {
747
+ failures.push(`${prefix} fighting-character profile requires bounds metadata for scale, ground, and lane checks.`);
748
+ }
749
+ else {
750
+ const bounds = asset.boundsMetadata;
751
+ const height = bounds.size[1];
752
+ if (bounds.maxDimension > 4.5) {
753
+ failures.push(`${prefix} fighting-character profile bounds too large (${bounds.maxDimension}m max); expected character-scale <= 4.5m.`);
754
+ }
755
+ if (height < 0.75) {
756
+ failures.push(`${prefix} fighting-character profile height ${height}m is too small for a readable humanoid fighter.`);
757
+ }
758
+ if (!bounds.grounded) {
759
+ warnings.push(`${prefix} fighting-character profile bounds are not grounded at pivot; min.y is ${bounds.min[1]}m.`);
760
+ }
761
+ }
762
+ if (asset.materials.length === 0) {
763
+ failures.push(`${prefix} fighting-character profile requires at least one readable material.`);
764
+ }
765
+ if (asset.sizeBytes > 50 * 1024 * 1024) {
766
+ failures.push(`${prefix} fighting-character profile payload is ${asset.sizeBytes} bytes; expected <= 52428800 for browser gameplay.`);
767
+ }
768
+ return { failures, warnings };
769
+ }
770
+ function isCharacterLikeAsset(asset) {
771
+ if (asset.humanoid?.humanoid)
772
+ return true;
773
+ return /fighter|player|opponent|enemy|hero|character|avatar|humanoid|npc|body|mara/i.test(asset.id);
774
+ }
775
+ function findFightingCharacterMetadataRisk(asset) {
776
+ const text = [
777
+ asset.id,
778
+ asset.source,
779
+ asset.provenance?.sourceUrl ?? "",
780
+ asset.provenance?.sourceFamily ?? "",
781
+ asset.provenance?.author ?? "",
782
+ ...(asset.nodeNames ?? []),
783
+ ...asset.materials,
784
+ ].join(" ").toLowerCase();
785
+ const ipRiskTerms = [
786
+ "fan art",
787
+ "fanart",
788
+ "copyright",
789
+ "copyrighted",
790
+ "ripped",
791
+ "pokemon",
792
+ "mario",
793
+ "sonic",
794
+ "naruto",
795
+ "dragon ball",
796
+ "fortnite",
797
+ "marvel",
798
+ "dc comics",
799
+ "star wars",
800
+ "disney",
801
+ ];
802
+ const ipRisk = ipRiskTerms.find((term) => text.includes(term));
803
+ if (ipRisk)
804
+ return { kind: "IP-risk", term: ipRisk };
805
+ const nonCharacterTerms = [
806
+ "aircraft",
807
+ "airplane",
808
+ "vehicle",
809
+ "building",
810
+ "architecture",
811
+ "environment",
812
+ "terrain",
813
+ "prop",
814
+ "furniture",
815
+ "sculpt",
816
+ "sculpture",
817
+ "statue",
818
+ "bust",
819
+ "figurine",
820
+ "miniature",
821
+ "photogrammetry",
822
+ "pedestal",
823
+ "spider",
824
+ "animal",
825
+ "quadruped",
826
+ "creature",
827
+ "insect",
828
+ "dragon",
829
+ "dinosaur",
830
+ "horse",
831
+ "dog",
832
+ "cat",
833
+ "bird",
834
+ "fish",
835
+ ];
836
+ const nonCharacter = nonCharacterTerms.find((term) => text.includes(term));
837
+ if (nonCharacter)
838
+ return { kind: "non-character", term: nonCharacter };
839
+ return undefined;
840
+ }
841
+ function missingRequiredGameClips(animations) {
842
+ const normalized = animations.map((name) => name.toLowerCase().replace(/[^a-z0-9]/g, ""));
843
+ const hasNamed = (patterns) => normalized.some((name) => patterns.some((pattern) => pattern.test(name)));
844
+ const missing = [];
845
+ if (!hasNamed([/idle/, /stand/]))
846
+ missing.push("idle");
847
+ if (!hasNamed([/walk/, /locomotion/, /move/]))
848
+ missing.push("walk");
849
+ if (!hasNamed([/lightpunch/, /lightattack/, /light/, /jab/, /punch/, /attack/]))
850
+ missing.push("lightPunch");
851
+ return missing;
852
+ }
853
+ function hasFloatingHairRisk(asset) {
854
+ const names = [asset.id, ...(asset.nodeNames ?? [])].join(" ").toLowerCase();
855
+ if (!names.includes("hair"))
856
+ return false;
857
+ const hasBodyAnchor = /body|torso|spine|chest|head|neck|skull|face|hips|pelvis/.test(names);
858
+ return !hasBodyAnchor;
859
+ }
860
+ function pushUnique(target, values) {
861
+ for (const value of values) {
862
+ if (!target.includes(value))
863
+ target.push(value);
864
+ }
865
+ }
866
+ function resolvePublicArtifactPath(projectDir, manifest, url) {
867
+ if (/^https?:\/\//i.test(url))
868
+ return undefined;
869
+ if (url.startsWith(manifest.assetBasePath)) {
870
+ return resolve(projectDir, manifest.outputDir, url.slice(manifest.assetBasePath.length));
871
+ }
872
+ return resolve(projectDir, url.replace(/^\//, "public/"));
873
+ }
874
+ function defaultAttachPoint(slot) {
875
+ const normalized = slot.toLowerCase();
876
+ if (normalized.includes("hair") || normalized.includes("hat") || normalized.includes("face"))
877
+ return "head";
878
+ if (normalized.includes("hand") || normalized.includes("weapon") || normalized.includes("prop"))
879
+ return "rightHand";
880
+ if (normalized.includes("shoe") || normalized.includes("boot"))
881
+ return "feet";
882
+ if (normalized.includes("cape") || normalized.includes("back"))
883
+ return "spine";
884
+ return "root";
885
+ }
247
886
  export function readAssetManifest(projectDir) {
248
887
  const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
249
888
  if (!existsSync(manifestPath)) {
@@ -270,8 +909,15 @@ function inspectAssetFile(path, format) {
270
909
  return inspectGlb(readFileSync(path), dirname(path));
271
910
  return {
272
911
  materials: [],
912
+ materialMetadata: [],
273
913
  animations: [],
914
+ animation: emptyAnimationInspection(),
915
+ humanoid: unknownHumanoidInspection("Humanoid detection is only available for GLB/glTF model assets."),
916
+ skeleton: emptySkeletonInspection("Skeleton detection is only available for GLB/glTF model assets."),
917
+ morphTargets: emptyMorphTargetInspection("Morph target detection is only available for GLB/glTF model assets."),
274
918
  textures: [],
919
+ orientation: unknownOrientationInspection(),
920
+ nodeNames: [],
275
921
  dependencies: [],
276
922
  bounds: undefined
277
923
  };
@@ -300,18 +946,226 @@ function inspectGltf(json, baseDir) {
300
946
  throw new Error(`Aura3D assets add failed: referenced asset file missing: ${missing.join(", ")}. Suggested fix: keep external .bin and texture files beside the .gltf or export as .glb.`);
301
947
  }
302
948
  }
949
+ const boundsMetadata = extractBoundsDetails(json);
303
950
  return {
304
- bounds: extractBounds(json),
951
+ bounds: boundsMetadata?.size,
952
+ boundsMetadata,
305
953
  materials: (json.materials ?? []).map((material, index) => material.name ?? `material-${index}`),
954
+ materialMetadata: inspectGltfMaterials(json),
306
955
  animations: (json.animations ?? []).map((animation, index) => animation.name ?? `clip-${index}`),
956
+ animation: inspectGltfAnimations(json),
957
+ humanoid: inspectGltfHumanoid(json),
958
+ skeleton: inspectGltfSkeleton(json),
959
+ morphTargets: inspectGltfMorphTargets(json),
960
+ provenance: inspectGltfProvenance(json),
307
961
  textures: (json.images ?? []).map((image, index) => image.uri ?? image.name ?? `image-${index}`),
962
+ orientation: inspectGltfOrientation(json),
963
+ nodeNames: (json.nodes ?? []).map((node, index) => node.name ?? `node-${index}`),
308
964
  dependencies
309
965
  };
310
966
  }
967
+ function emptyAnimationInspection() {
968
+ return {
969
+ clipCount: 0,
970
+ clips: [],
971
+ messages: ["No embedded animation clips detected."]
972
+ };
973
+ }
974
+ function inspectGltfAnimations(json) {
975
+ const clips = (json.animations ?? []).map((animation, index) => {
976
+ const channels = animation.channels ?? [];
977
+ const targetPaths = uniqueStrings(channels.map((channel) => channel.target?.path).filter(isString));
978
+ const targetNodes = uniqueStrings(channels.map((channel) => {
979
+ const nodeIndex = channel.target?.node;
980
+ return typeof nodeIndex === "number" ? json.nodes?.[nodeIndex]?.name ?? `node-${nodeIndex}` : undefined;
981
+ }).filter(isString));
982
+ return {
983
+ index,
984
+ name: animation.name ?? `clip-${index}`,
985
+ channelCount: channels.length,
986
+ samplerCount: animation.samplers?.length ?? 0,
987
+ targetPaths,
988
+ targetNodes
989
+ };
990
+ });
991
+ return {
992
+ clipCount: clips.length,
993
+ clips,
994
+ messages: clips.length === 0
995
+ ? ["No embedded animation clips detected."]
996
+ : [`Detected ${clips.length} embedded animation clip${clips.length === 1 ? "" : "s"}.`]
997
+ };
998
+ }
999
+ function inspectGltfHumanoid(json) {
1000
+ const skinCount = json.skins?.length ?? 0;
1001
+ const jointIndexes = uniqueNumbers((json.skins ?? []).flatMap((skin) => skin.joints ?? []));
1002
+ const jointNames = uniqueStrings(jointIndexes.map((index) => json.nodes?.[index]?.name ?? `joint-${index}`));
1003
+ const nodeNames = uniqueStrings((json.nodes ?? []).map((node, index) => node.name ?? `node-${index}`));
1004
+ const candidates = jointNames.length > 0 ? jointNames : nodeNames;
1005
+ const requiredBones = ["hips", "spine", "head", "leftArm", "rightArm", "leftLeg", "rightLeg"];
1006
+ const matchedBones = requiredBones.filter((bone) => candidates.some((name) => matchesHumanoidBone(name, bone)));
1007
+ const missingBones = requiredBones.filter((bone) => !matchedBones.includes(bone));
1008
+ const hasSkin = jointIndexes.length > 0;
1009
+ const hasTorso = matchedBones.includes("hips") && matchedBones.includes("spine") && matchedBones.includes("head");
1010
+ const hasArms = matchedBones.includes("leftArm") && matchedBones.includes("rightArm");
1011
+ const hasLegs = matchedBones.includes("leftLeg") && matchedBones.includes("rightLeg");
1012
+ const humanoid = (hasSkin && hasTorso && hasArms && hasLegs) || (!hasSkin && hasTorso && matchedBones.length >= 5);
1013
+ const status = humanoid
1014
+ ? "humanoid"
1015
+ : hasSkin || matchedBones.length > 0
1016
+ ? "unknown"
1017
+ : "non-humanoid";
1018
+ const confidence = humanoid && hasSkin
1019
+ ? "high"
1020
+ : humanoid || (hasSkin && matchedBones.length >= 5)
1021
+ ? "medium"
1022
+ : "low";
1023
+ return {
1024
+ humanoid,
1025
+ status,
1026
+ confidence,
1027
+ skinCount,
1028
+ jointCount: jointIndexes.length,
1029
+ matchedBones,
1030
+ missingBones,
1031
+ messages: humanoid
1032
+ ? [`Humanoid signals detected from ${hasSkin ? "skinned joints" : "node names"}.`]
1033
+ : status === "unknown"
1034
+ ? [`Humanoid status is unknown; missing bone groups: ${missingBones.join(", ")}.`]
1035
+ : ["No humanoid skeleton signals detected."]
1036
+ };
1037
+ }
1038
+ function inspectGltfSkeleton(json) {
1039
+ const skins = (json.skins ?? []).map((skin, index) => {
1040
+ const joints = (skin.joints ?? []).map((jointIndex) => json.nodes?.[jointIndex]?.name ?? `joint-${jointIndex}`);
1041
+ const skeletonIndex = skin.skeleton;
1042
+ return {
1043
+ index,
1044
+ name: skin.name ?? `skin-${index}`,
1045
+ jointCount: joints.length,
1046
+ joints,
1047
+ ...(typeof skeletonIndex === "number" ? { skeleton: json.nodes?.[skeletonIndex]?.name ?? `node-${skeletonIndex}` } : {})
1048
+ };
1049
+ });
1050
+ const jointCount = uniqueStrings(skins.flatMap((skin) => skin.joints)).length;
1051
+ return {
1052
+ skinCount: skins.length,
1053
+ jointCount,
1054
+ skins,
1055
+ messages: skins.length === 0
1056
+ ? ["No skin/skeleton metadata detected."]
1057
+ : [`Detected ${skins.length} skin${skins.length === 1 ? "" : "s"} with ${jointCount} unique joint${jointCount === 1 ? "" : "s"}.`]
1058
+ };
1059
+ }
1060
+ function emptySkeletonInspection(message) {
1061
+ return {
1062
+ skinCount: 0,
1063
+ jointCount: 0,
1064
+ skins: [],
1065
+ messages: [message]
1066
+ };
1067
+ }
1068
+ function inspectGltfMorphTargets(json) {
1069
+ const meshes = (json.meshes ?? []).map((mesh, index) => {
1070
+ const meshExtras = objectValue(mesh.extras);
1071
+ const namedTargets = stringArrayValue(meshExtras?.targetNames ?? meshExtras?.morphTargetNames);
1072
+ const targetCount = Math.max(namedTargets.length, mesh.weights?.length ?? 0, ...(mesh.primitives ?? []).map((primitive) => primitive.targets?.length ?? 0));
1073
+ if (targetCount === 0)
1074
+ return undefined;
1075
+ const targetNames = targetCount > 0
1076
+ ? Array.from({ length: targetCount }, (_, targetIndex) => namedTargets[targetIndex] ?? `target-${targetIndex}`)
1077
+ : [];
1078
+ return {
1079
+ index,
1080
+ name: mesh.name ?? `mesh-${index}`,
1081
+ targetNames
1082
+ };
1083
+ }).filter((mesh) => Boolean(mesh));
1084
+ const targetNames = uniqueStrings(meshes.flatMap((mesh) => mesh.targetNames));
1085
+ return {
1086
+ targetCount: targetNames.length,
1087
+ targetNames,
1088
+ meshes,
1089
+ messages: targetNames.length === 0
1090
+ ? ["No morph target metadata detected."]
1091
+ : [`Detected ${targetNames.length} morph target${targetNames.length === 1 ? "" : "s"}.`]
1092
+ };
1093
+ }
1094
+ function emptyMorphTargetInspection(message) {
1095
+ return {
1096
+ targetCount: 0,
1097
+ targetNames: [],
1098
+ meshes: [],
1099
+ messages: [message]
1100
+ };
1101
+ }
1102
+ function inspectGltfProvenance(json) {
1103
+ const assetExtras = objectValue(json.asset?.extras);
1104
+ const auraExtras = objectValue(assetExtras?.aura3d) ?? assetExtras;
1105
+ const provenance = objectValue(auraExtras?.provenance ?? auraExtras?.license ?? auraExtras?.source);
1106
+ const sourceUrl = stringValue(provenance?.sourceUrl ?? provenance?.url ?? auraExtras?.sourceUrl);
1107
+ const license = stringValue(provenance?.license ?? provenance?.spdx ?? auraExtras?.license);
1108
+ const author = stringValue(provenance?.author ?? provenance?.creator ?? auraExtras?.author);
1109
+ const sourceFamily = stringValue(provenance?.sourceFamily ?? provenance?.source ?? auraExtras?.sourceFamily);
1110
+ const attribution = stringValue(provenance?.attribution ?? auraExtras?.attribution);
1111
+ const evidence = stringArrayValue(provenance?.evidence ?? auraExtras?.evidence);
1112
+ if (!sourceUrl && !license && !author && !sourceFamily && !attribution && evidence.length === 0)
1113
+ return undefined;
1114
+ return {
1115
+ ...(sourceUrl ? { sourceUrl } : {}),
1116
+ ...(license ? { license } : {}),
1117
+ ...(author ? { author } : {}),
1118
+ ...(sourceFamily ? { sourceFamily } : {}),
1119
+ ...(attribution ? { attribution } : {}),
1120
+ ...(evidence.length > 0 ? { evidence } : {})
1121
+ };
1122
+ }
1123
+ function unknownHumanoidInspection(message) {
1124
+ return {
1125
+ humanoid: false,
1126
+ status: "unknown",
1127
+ confidence: "low",
1128
+ skinCount: 0,
1129
+ jointCount: 0,
1130
+ matchedBones: [],
1131
+ missingBones: ["hips", "spine", "head", "leftArm", "rightArm", "leftLeg", "rightLeg"],
1132
+ messages: [message]
1133
+ };
1134
+ }
1135
+ function matchesHumanoidBone(name, bone) {
1136
+ const raw = name.toLowerCase();
1137
+ const compact = raw.replace(/[^a-z0-9]/g, "");
1138
+ const left = compact.includes("left") || /(^|[^a-z0-9])l([^a-z0-9]|$)/.test(raw) || /[^a-z0-9]l$/.test(raw);
1139
+ const right = compact.includes("right") || /(^|[^a-z0-9])r([^a-z0-9]|$)/.test(raw) || /[^a-z0-9]r$/.test(raw);
1140
+ const arm = compact.includes("arm") || compact.includes("forearm") || compact.includes("shoulder") || compact.includes("hand");
1141
+ const leg = compact.includes("leg") || compact.includes("thigh") || compact.includes("foot") || compact.includes("toe");
1142
+ if (bone === "hips")
1143
+ return compact.includes("hip") || compact.includes("pelvis");
1144
+ if (bone === "spine")
1145
+ return compact.includes("spine") || compact.includes("chest") || compact.includes("torso");
1146
+ if (bone === "head")
1147
+ return compact.includes("head") || compact.includes("neck");
1148
+ if (bone === "leftArm")
1149
+ return left && arm;
1150
+ if (bone === "rightArm")
1151
+ return right && arm;
1152
+ if (bone === "leftLeg")
1153
+ return left && leg;
1154
+ return right && leg;
1155
+ }
311
1156
  function isExternalUri(uri) {
312
1157
  return typeof uri === "string" && uri.length > 0 && !uri.startsWith("data:");
313
1158
  }
314
- function extractBounds(json) {
1159
+ function isString(value) {
1160
+ return typeof value === "string";
1161
+ }
1162
+ function uniqueStrings(values) {
1163
+ return [...new Set(values)];
1164
+ }
1165
+ function uniqueNumbers(values) {
1166
+ return [...new Set(values)];
1167
+ }
1168
+ function extractBoundsDetails(json) {
315
1169
  let min;
316
1170
  let max;
317
1171
  for (const accessor of json.accessors ?? []) {
@@ -320,7 +1174,89 @@ function extractBounds(json) {
320
1174
  min = min ? [Math.min(min[0], accessor.min[0]), Math.min(min[1], accessor.min[1]), Math.min(min[2], accessor.min[2])] : [accessor.min[0], accessor.min[1], accessor.min[2]];
321
1175
  max = max ? [Math.max(max[0], accessor.max[0]), Math.max(max[1], accessor.max[1]), Math.max(max[2], accessor.max[2])] : [accessor.max[0], accessor.max[1], accessor.max[2]];
322
1176
  }
323
- return min && max ? [round(max[0] - min[0]), round(max[1] - min[1]), round(max[2] - min[2])] : undefined;
1177
+ if (!min || !max)
1178
+ return undefined;
1179
+ const size = [round(max[0] - min[0]), round(max[1] - min[1]), round(max[2] - min[2])];
1180
+ const center = [round((min[0] + max[0]) / 2), round((min[1] + max[1]) / 2), round((min[2] + max[2]) / 2)];
1181
+ const roundedMin = [round(min[0]), round(min[1]), round(min[2])];
1182
+ const roundedMax = [round(max[0]), round(max[1]), round(max[2])];
1183
+ return {
1184
+ min: roundedMin,
1185
+ max: roundedMax,
1186
+ size,
1187
+ center,
1188
+ maxDimension: Math.max(...size),
1189
+ grounded: Math.abs(roundedMin[1]) <= 0.08
1190
+ };
1191
+ }
1192
+ function inspectGltfMaterials(json) {
1193
+ return (json.materials ?? []).map((material, index) => {
1194
+ const name = material.name ?? `material-${index}`;
1195
+ const extras = objectValue(material.extras);
1196
+ const explicitVisible = booleanValue(extras?.visible);
1197
+ const explicitReadable = booleanValue(extras?.readable);
1198
+ const opacity = round(numberValue(material.pbrMetallicRoughness?.baseColorFactor?.[3]) ?? 1);
1199
+ const alphaMode = material.alphaMode;
1200
+ const alphaCutoff = numberValue(material.alphaCutoff);
1201
+ const reasons = [];
1202
+ if (opacity <= 0)
1203
+ reasons.push("baseColorFactor alpha is 0");
1204
+ if (alphaMode === "MASK" && (alphaCutoff ?? 0.5) >= 1)
1205
+ reasons.push("alpha mask cutoff hides fully transparent surfaces");
1206
+ if (explicitVisible === false)
1207
+ reasons.push("material extras mark visible=false");
1208
+ if (explicitReadable === false)
1209
+ reasons.push("material extras mark readable=false");
1210
+ const visible = explicitVisible ?? (opacity > 0 && !(alphaMode === "MASK" && (alphaCutoff ?? 0.5) >= 1));
1211
+ const readable = explicitReadable ?? visible;
1212
+ return {
1213
+ name,
1214
+ visible,
1215
+ readable,
1216
+ opacity,
1217
+ ...(alphaMode ? { alphaMode } : {}),
1218
+ reasons
1219
+ };
1220
+ });
1221
+ }
1222
+ function inspectGltfOrientation(json) {
1223
+ const assetExtras = objectValue(json.asset?.extras);
1224
+ const auraExtras = objectValue(assetExtras?.aura3d) ?? assetExtras;
1225
+ const orientation = objectValue(auraExtras?.orientation) ?? auraExtras;
1226
+ const forwardAxis = stringValue(orientation?.forwardAxis ?? orientation?.forward ?? orientation?.facing);
1227
+ const upAxis = stringValue(orientation?.upAxis ?? orientation?.up);
1228
+ if (!forwardAxis && !upAxis)
1229
+ return unknownOrientationInspection();
1230
+ return {
1231
+ source: "gltf-extras",
1232
+ ...(forwardAxis ? { forwardAxis } : {}),
1233
+ ...(upAxis ? { upAxis } : {}),
1234
+ messages: [`Orientation metadata detected${forwardAxis ? ` with forwardAxis=${forwardAxis}` : ""}${upAxis ? ` and upAxis=${upAxis}` : ""}.`]
1235
+ };
1236
+ }
1237
+ function unknownOrientationInspection() {
1238
+ return {
1239
+ source: "unknown",
1240
+ messages: ["No orientation metadata detected; facing direction cannot be proven."]
1241
+ };
1242
+ }
1243
+ function objectValue(value) {
1244
+ return typeof value === "object" && value !== null ? value : undefined;
1245
+ }
1246
+ function stringValue(value) {
1247
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
1248
+ }
1249
+ function stringArrayValue(value) {
1250
+ if (!Array.isArray(value))
1251
+ return [];
1252
+ return value.map((entry) => stringValue(entry)).filter((entry) => Boolean(entry));
1253
+ }
1254
+ function booleanValue(value) {
1255
+ return typeof value === "boolean" ? value : undefined;
1256
+ }
1257
+ function numberValue(value) {
1258
+ const number = Number(value);
1259
+ return Number.isFinite(number) ? number : undefined;
324
1260
  }
325
1261
  function createAssetWarnings(path, inspection) {
326
1262
  const warnings = [];
@@ -331,8 +1267,96 @@ function createAssetWarnings(path, inspection) {
331
1267
  warnings.push("bounds could not be extracted");
332
1268
  if (inspection.textures.length === 0 && ["glb", "gltf"].includes(extname(path).slice(1).toLowerCase()))
333
1269
  warnings.push("no texture references detected");
1270
+ if (inspection.orientation.source === "unknown" && ["glb", "gltf"].includes(extname(path).slice(1).toLowerCase()))
1271
+ warnings.push("orientation metadata missing; facing direction cannot be validated until GLTF extras declare aura3d.orientation.forwardAxis");
1272
+ if (inspection.materialMetadata.some((material) => !material.visible || !material.readable))
1273
+ warnings.push("one or more materials are invisible or unreadable");
334
1274
  return warnings;
335
1275
  }
1276
+ function createAssetProvenance(projectDir, sourcePath, options, detected) {
1277
+ return {
1278
+ sourcePath: normalizeRelativePath(relative(projectDir, sourcePath)),
1279
+ ...(options.sourceUrl ?? detected?.sourceUrl ? { sourceUrl: options.sourceUrl ?? detected?.sourceUrl } : {}),
1280
+ ...(options.license ?? detected?.license ? { license: options.license ?? detected?.license } : {}),
1281
+ ...(options.author ?? detected?.author ? { author: options.author ?? detected?.author } : {}),
1282
+ ...(options.sourceFamily ?? detected?.sourceFamily ? { sourceFamily: options.sourceFamily ?? detected?.sourceFamily } : {}),
1283
+ ...(options.attribution ?? detected?.attribution ? { attribution: options.attribution ?? detected?.attribution } : {}),
1284
+ ...(detected?.evidence && detected.evidence.length > 0 ? { evidence: detected.evidence } : {}),
1285
+ checkedAt: new Date().toISOString()
1286
+ };
1287
+ }
1288
+ function readExternalProvenance(projectDir, provenanceFile) {
1289
+ if (!provenanceFile)
1290
+ return new Map();
1291
+ const path = resolve(projectDir, provenanceFile);
1292
+ if (!existsSync(path))
1293
+ return new Map();
1294
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
1295
+ const root = objectValue(parsed);
1296
+ if (!root)
1297
+ return new Map();
1298
+ const checkedAt = stringValue(root.updatedAt ?? root.verifiedAt ?? root.checkedAt) ?? new Date().toISOString();
1299
+ const records = [
1300
+ ...arrayObjectValue(root.launchGlbs),
1301
+ ...arrayObjectValue(root.assets),
1302
+ ...arrayObjectValue(root.assetEvidence)
1303
+ ];
1304
+ const byId = new Map();
1305
+ for (const record of records) {
1306
+ const typedAsset = stringValue(record.typedAsset);
1307
+ const id = stringValue(record.assetKey ?? record.id) ?? typedAsset?.replace(/^assets\./, "");
1308
+ if (!id)
1309
+ continue;
1310
+ const nestedProvenance = objectValue(record.provenance);
1311
+ const sourcePath = stringValue(record.sourcePath ?? record.source ?? nestedProvenance?.builderOutput) ?? id;
1312
+ const license = stringValue(record.license ?? record.licenseNote ?? nestedProvenance?.license);
1313
+ const sourceUrl = stringValue(record.sourceUrl ?? record.publicUrl ?? record.officialPage ?? nestedProvenance?.sourceUrl);
1314
+ const sourceFamily = stringValue(record.sourceFamily ?? nestedProvenance?.sourceFamily ?? nestedProvenance?.sourcePack);
1315
+ const author = stringValue(record.author ?? nestedProvenance?.author);
1316
+ const attribution = stringValue(record.attribution ?? record.credit ?? nestedProvenance?.attribution);
1317
+ const evidence = [
1318
+ ...stringArrayValue(record.evidence),
1319
+ ...stringArrayValue(record.intendedRouteUsage),
1320
+ ...stringArrayValue(nestedProvenance?.evidence)
1321
+ ];
1322
+ byId.set(id, {
1323
+ sourcePath,
1324
+ ...(sourceUrl ? { sourceUrl } : {}),
1325
+ ...(license ? { license } : {}),
1326
+ ...(author ? { author } : {}),
1327
+ ...(sourceFamily ? { sourceFamily } : {}),
1328
+ ...(attribution ? { attribution } : {}),
1329
+ ...(evidence.length > 0 ? { evidence } : {}),
1330
+ checkedAt
1331
+ });
1332
+ }
1333
+ return byId;
1334
+ }
1335
+ function arrayObjectValue(value) {
1336
+ if (!Array.isArray(value))
1337
+ return [];
1338
+ return value.map((entry) => objectValue(entry)).filter((entry) => Boolean(entry));
1339
+ }
1340
+ function resolveAssetProvenance(asset, externalProvenance) {
1341
+ return asset.provenance ?? externalProvenance.get(asset.id);
1342
+ }
1343
+ function hasUsableLicenseEvidence(provenance) {
1344
+ const license = provenance?.license?.trim();
1345
+ if (!license)
1346
+ return false;
1347
+ return !/(unverified|unknown|candidate|needs[-\s]?confirmation|todo|placeholder)/i.test(license);
1348
+ }
1349
+ function isPlaceholderAsset(asset, provenance) {
1350
+ const value = [
1351
+ asset.id,
1352
+ asset.source,
1353
+ asset.outputPath,
1354
+ asset.url,
1355
+ provenance?.sourcePath,
1356
+ provenance?.sourceUrl
1357
+ ].filter(Boolean).join(" ");
1358
+ return /(^|[-_./\s])(placeholder|dummy|mock|todo|replace-me|sample-placeholder)([-_./\s]|$)/i.test(value);
1359
+ }
336
1360
  function copyAssetDependencies(projectDir, sourcePath, outputDir, dependencies) {
337
1361
  const sourceDir = dirname(sourcePath);
338
1362
  for (const dependency of dependencies) {