@aura3d/cli 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/asset-constants.d.ts +5 -0
  2. package/dist/asset-constants.d.ts.map +1 -0
  3. package/dist/asset-constants.js +5 -0
  4. package/dist/asset-constants.js.map +1 -0
  5. package/dist/asset-core-types.d.ts +206 -0
  6. package/dist/asset-core-types.d.ts.map +1 -0
  7. package/dist/asset-core-types.js +2 -0
  8. package/dist/asset-core-types.js.map +1 -0
  9. package/dist/asset-inspection-types.d.ts +131 -0
  10. package/dist/asset-inspection-types.d.ts.map +1 -0
  11. package/dist/asset-inspection-types.js +2 -0
  12. package/dist/asset-inspection-types.js.map +1 -0
  13. package/dist/asset-manifest.d.ts +8 -0
  14. package/dist/asset-manifest.d.ts.map +1 -0
  15. package/dist/asset-manifest.js +116 -0
  16. package/dist/asset-manifest.js.map +1 -0
  17. package/dist/asset-readiness-types.d.ts +164 -0
  18. package/dist/asset-readiness-types.d.ts.map +1 -0
  19. package/dist/asset-readiness-types.js +2 -0
  20. package/dist/asset-readiness-types.js.map +1 -0
  21. package/dist/asset-source-ast.d.ts +8 -0
  22. package/dist/asset-source-ast.d.ts.map +1 -0
  23. package/dist/asset-source-ast.js +241 -0
  24. package/dist/asset-source-ast.js.map +1 -0
  25. package/dist/asset-source-roles.d.ts +4 -0
  26. package/dist/asset-source-roles.d.ts.map +1 -0
  27. package/dist/asset-source-roles.js +79 -0
  28. package/dist/asset-source-roles.js.map +1 -0
  29. package/dist/asset-source-validation.d.ts +4 -0
  30. package/dist/asset-source-validation.d.ts.map +1 -0
  31. package/dist/asset-source-validation.js +200 -0
  32. package/dist/asset-source-validation.js.map +1 -0
  33. package/dist/character-assembly-types.d.ts +39 -0
  34. package/dist/character-assembly-types.d.ts.map +1 -0
  35. package/dist/character-assembly-types.js +2 -0
  36. package/dist/character-assembly-types.js.map +1 -0
  37. package/dist/cli-help.d.ts +4 -0
  38. package/dist/cli-help.d.ts.map +1 -0
  39. package/dist/cli-help.js +48 -0
  40. package/dist/cli-help.js.map +1 -0
  41. package/dist/cli-options.d.ts +66 -0
  42. package/dist/cli-options.d.ts.map +1 -0
  43. package/dist/cli-options.js +234 -0
  44. package/dist/cli-options.js.map +1 -0
  45. package/dist/cli.js +20 -177
  46. package/dist/cli.js.map +1 -1
  47. package/dist/index.d.ts +12 -402
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +948 -90
  50. package/dist/index.js.map +1 -1
  51. package/dist/pull-bridge/adapters.d.ts +4 -0
  52. package/dist/pull-bridge/adapters.d.ts.map +1 -0
  53. package/dist/pull-bridge/adapters.js +38 -0
  54. package/dist/pull-bridge/adapters.js.map +1 -0
  55. package/dist/pull-bridge/download.d.ts +6 -0
  56. package/dist/pull-bridge/download.d.ts.map +1 -0
  57. package/dist/pull-bridge/download.js +208 -0
  58. package/dist/pull-bridge/download.js.map +1 -0
  59. package/dist/pull-bridge/profiles.d.ts +14 -0
  60. package/dist/pull-bridge/profiles.d.ts.map +1 -0
  61. package/dist/pull-bridge/profiles.js +63 -0
  62. package/dist/pull-bridge/profiles.js.map +1 -0
  63. package/dist/pull-bridge/provenance.d.ts +9 -0
  64. package/dist/pull-bridge/provenance.d.ts.map +1 -0
  65. package/dist/pull-bridge/provenance.js +99 -0
  66. package/dist/pull-bridge/provenance.js.map +1 -0
  67. package/dist/pull-bridge/pullable.d.ts +14 -0
  68. package/dist/pull-bridge/pullable.d.ts.map +1 -0
  69. package/dist/pull-bridge/pullable.js +58 -0
  70. package/dist/pull-bridge/pullable.js.map +1 -0
  71. package/dist/pull-bridge/scoring.d.ts +21 -0
  72. package/dist/pull-bridge/scoring.d.ts.map +1 -0
  73. package/dist/pull-bridge/scoring.js +177 -0
  74. package/dist/pull-bridge/scoring.js.map +1 -0
  75. package/dist/pull-bridge/search.d.ts +39 -0
  76. package/dist/pull-bridge/search.d.ts.map +1 -0
  77. package/dist/pull-bridge/search.js +114 -0
  78. package/dist/pull-bridge/search.js.map +1 -0
  79. package/dist/pull-bridge/types.d.ts +9 -0
  80. package/dist/pull-bridge/types.d.ts.map +1 -0
  81. package/dist/pull-bridge/types.js +2 -0
  82. package/dist/pull-bridge/types.js.map +1 -0
  83. package/dist/pull-bridge/vector3.d.ts +2 -0
  84. package/dist/pull-bridge/vector3.d.ts.map +1 -0
  85. package/dist/pull-bridge/vector3.js +4 -0
  86. package/dist/pull-bridge/vector3.js.map +1 -0
  87. package/dist/pull-bridge.d.ts +18 -100
  88. package/dist/pull-bridge.d.ts.map +1 -1
  89. package/dist/pull-bridge.js +59 -526
  90. package/dist/pull-bridge.js.map +1 -1
  91. package/package.json +3 -2
@@ -8,500 +8,30 @@
8
8
  * Two surfaces:
9
9
  * - `assets search <query>` — rank + print candidates, label what is/ isn't
10
10
  * auto-pullable, and surface marketplace deep-links when nothing is.
11
- * - `assets resolve <query> --name <name>` — pick the top AUTO-PULLABLE
12
- * candidate, download its .glb, and run the normal `addAsset` flow.
11
+ * - `assets resolve <query> --name <name>` — rank AUTO-PULLABLE candidates,
12
+ * download each in order, inspect post-download, and run the normal
13
+ * `addAsset` flow only for a candidate-level match.
13
14
  *
14
15
  * License safety mirrors the index: we NEVER auto-pull an UNVERIFIED or
15
16
  * deep-link-only asset. `selectPullable` is the single decision seam and is
16
17
  * deliberately pure so it can be unit-tested without a network or the CLI.
17
18
  */
18
19
  import { createHash } from "node:crypto";
19
- import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { mkdtempSync, readFileSync } from "node:fs";
20
21
  import { tmpdir } from "node:os";
21
- import { basename, dirname, join } from "node:path";
22
- import { inflateRawSync } from "node:zlib";
22
+ import { join } from "node:path";
23
23
  import * as assetIndex from "@aura3d/asset-index";
24
- import { addAsset } from "./index.js";
25
- const { evaluateAnimationAssetProfile, evaluateGameAssetProfile, FederatedResolver, defaultAdapters, isAnimationAssetProfile, isAutoPullable, } = assetIndex;
26
- function optionalFactory(name) {
27
- const candidate = assetIndex[name];
28
- return typeof candidate === "function" ? candidate : undefined;
29
- }
30
- /** True only when a non-empty env var is present (used to gate keyed sources). */
31
- function hasEnv(name) {
32
- const value = process.env[name];
33
- return typeof value === "string" && value.trim().length > 0;
34
- }
35
- /**
36
- * Assemble the live source adapters for the CLI:
37
- * - the zero-auth verified defaults (Khronos, OS3A, + Poly Haven when present),
38
- * - key-gated sources only when their API key env var is set,
39
- * Each optional factory degrades to "absent" rather than throwing.
40
- */
41
- export function buildSearchAdapters(env = process.env) {
42
- const starterPack = optionalFactory("createAnimationStarterPackAdapter");
43
- const starterAdapters = starterPack ? [starterPack()] : [];
44
- // Primary: the hosted ~850k Aura3D catalog via its `/search` endpoint (hybrid
45
- // keyword + semantic + quality ranking). It already aggregates Objaverse,
46
- // Sketchfab, Poly Pizza, Poly Haven, OS3A, Khronos and the CC0 mirror, so it
47
- // supersedes per-source live federation. Fall back to live sources only if the
48
- // hosted-catalog adapter isn't available in this build.
49
- const auraIndex = optionalFactory("createAuraIndexAdapter");
50
- if (auraIndex)
51
- return [...starterAdapters, auraIndex()];
52
- const adapters = [...starterAdapters, ...defaultAdapters()];
53
- const polyHaven = optionalFactory("createPolyHavenAdapter");
54
- if (polyHaven && !adapters.some((a) => a.id === "polyhaven")) {
55
- adapters.push(polyHaven());
56
- }
57
- // Key-gated sources: include only when the key is present; the adapter itself
58
- // also degrades to [] when the key is missing, but we avoid even constructing
59
- // it without a key so a no-key environment stays fully offline-deterministic.
60
- if (env.SKETCHFAB_API_TOKEN || env.SKETCHFAB_TOKEN) {
61
- const sketchfab = optionalFactory("createSketchfabAdapter");
62
- if (sketchfab)
63
- adapters.push(sketchfab());
64
- }
65
- if (env.POLY_PIZZA_API_KEY || env.POLYPIZZA_API_KEY) {
66
- const polyPizza = optionalFactory("createPolyPizzaAdapter");
67
- if (polyPizza)
68
- adapters.push(polyPizza());
69
- }
70
- return adapters;
71
- }
72
- /** Marketplace deep-link adapter, when the concurrently-authored factory exists. */
73
- export function buildDeepLinkAdapter() {
74
- const factory = optionalFactory("createMarketplaceDeepLinkAdapter");
75
- return factory ? factory() : undefined;
76
- }
77
- /** Translate CLI flags into the index's ResolveConstraints shape. */
78
- export function toResolveConstraints(cli, redistributableOnly) {
79
- const constraints = {};
80
- if (cli.profile === "fighting-character") {
81
- constraints.license = cli.license && cli.license.length > 0 ? cli.license : ["CC0", "CC-BY"];
82
- constraints.animated = true;
83
- constraints.format = "glb";
84
- constraints.maxTriangles = cli.maxTriangles ?? 200_000;
85
- }
86
- else if (isAnimationCliProfile(cli.profile)) {
87
- constraints.license = cli.license && cli.license.length > 0 ? cli.license : ["CC0", "CC-BY"];
88
- constraints.format = "glb";
89
- constraints.maxTriangles = cli.maxTriangles ?? animationProfileMaxTriangles(cli.profile);
90
- if (cli.profile === "animation-character")
91
- constraints.animated = true;
92
- else if (typeof cli.animated === "boolean")
93
- constraints.animated = cli.animated;
94
- }
95
- else {
96
- if (cli.license && cli.license.length > 0)
97
- constraints.license = cli.license;
98
- if (typeof cli.maxTriangles === "number")
99
- constraints.maxTriangles = cli.maxTriangles;
100
- if (typeof cli.animated === "boolean")
101
- constraints.animated = cli.animated;
102
- }
103
- if (redistributableOnly)
104
- constraints.redistributableOnly = true;
105
- return constraints;
106
- }
107
- export function selectPullable(candidates, options = {}) {
108
- const profile = options.profile ?? "general";
109
- const pullable = candidates.find((c) => {
110
- if (!isAutoPullable(c.asset))
111
- return false;
112
- if (profile === "general")
113
- return true;
114
- return evaluateAssetProfile(c.asset, profile).suitable;
115
- });
116
- if (pullable)
117
- return { ok: true, candidate: pullable };
118
- if (candidates.length === 0) {
119
- return {
120
- ok: false,
121
- reason: "No candidates matched the query. Try a broader query or relax constraints.",
122
- };
123
- }
124
- if (profile !== "general") {
125
- const rejectedPullable = candidates.filter((c) => isAutoPullable(c.asset));
126
- if (rejectedPullable.length > 0) {
127
- return {
128
- ok: false,
129
- reason: `No auto-pullable candidate passed the ${profile} profile. ` +
130
- rejectedPullable.slice(0, 5).map((candidate) => {
131
- const evaluation = evaluateAssetProfile(candidate.asset, profile);
132
- const reasons = evaluation.rejectionReasons.length > 0
133
- ? evaluation.rejectionReasons.join("; ")
134
- : "profile did not report a concrete reason";
135
- return `"${candidate.asset.title}" (${candidate.asset.id}): ${reasons}`;
136
- }).join(" | "),
137
- };
138
- }
139
- }
140
- const top = candidates[0].asset;
141
- return {
142
- ok: false,
143
- reason: `No auto-pullable candidate found. The best match "${top.title}" (${top.id}) ` +
144
- `is ${describeWhyNotPullable(top)}. Aura will not auto-pull an asset whose ` +
145
- `license is unverified or that is marketplace deep-link only. ` +
146
- `Review it at ${top.sourcePage ?? top.url} and add it manually once the ` +
147
- `license is confirmed.`,
148
- };
149
- }
150
- function describeWhyNotPullable(asset) {
151
- if (asset.access !== "direct-download")
152
- return "marketplace deep-link only";
153
- if (!asset.license.verified)
154
- return `license UNVERIFIED (raw: "${asset.license.raw}")`;
155
- if (!asset.license.redistributable)
156
- return `license "${asset.license.spdx}" is not redistributable`;
157
- return "not auto-pullable";
158
- }
159
- /** First four bytes of a ZIP local-file header ("PK\x03\x04"). */
160
- const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
161
- /** glTF binary container magic ("glTF"). */
162
- const GLB_MAGIC = Buffer.from([0x67, 0x6c, 0x54, 0x46]);
163
- /**
164
- * Default downloader backed by global fetch.
165
- *
166
- * Objaverse URLs are direct `.glb`; Poly Pizza / Sketchfab artifacts can arrive
167
- * as a ZIP (gltf + bin + textures, or a packaged .glb). We sniff the leading
168
- * bytes: a GLB/gltf body is written through as-is; a `PK` body is unpacked next
169
- * to `destPath` and the assembled .glb/.gltf path is returned. Auth-gated
170
- * Sketchfab `download` endpoints return JSON (a signed-URL envelope, or a 401);
171
- * we surface a clear, non-fatal-to-the-tool error instead of writing garbage.
172
- */
173
- export const defaultDownloadFile = async (url, destPath) => {
174
- const res = await fetch(url, { headers: { accept: "model/gltf-binary,application/zip,*/*" } });
175
- if (!res.ok) {
176
- if (res.status === 401 || res.status === 403) {
177
- throw new Error(`Aura3D resolve failed: ${url} requires authentication (HTTP ${res.status}). ` +
178
- `This source (e.g. Sketchfab) needs an OAuth download token; set SKETCHFAB_API_TOKEN ` +
179
- `or pull a direct-download (Objaverse) candidate instead.`);
180
- }
181
- throw new Error(`Aura3D resolve failed: download of ${url} returned HTTP ${res.status}.`);
182
- }
183
- const buffer = Buffer.from(await res.arrayBuffer());
184
- if (buffer.length === 0) {
185
- throw new Error(`Aura3D resolve failed: download of ${url} was empty.`);
186
- }
187
- // A Sketchfab download endpoint that 200s but returns a JSON signed-URL
188
- // envelope rather than model bytes — follow the envelope when we can, else fail
189
- // clearly. We never feed JSON into the GLB inspector.
190
- const contentType = res.headers.get("content-type") ?? "";
191
- if (contentType.includes("application/json") || buffer[0] === 0x7b /* '{' */) {
192
- const followed = await followSignedDownload(buffer, destPath);
193
- if (followed)
194
- return followed;
195
- throw new Error(`Aura3D resolve failed: ${url} returned a JSON envelope, not model bytes ` +
196
- `(auth-gated download requiring an OAuth token). Pull a direct-download candidate instead.`);
197
- }
198
- if (startsWith(buffer, ZIP_MAGIC)) {
199
- return unpackZipToModel(buffer, destPath);
200
- }
201
- writeFileSync(destPath, buffer);
202
- // A GLB/gltf can still reference EXTERNAL textures/.bin (e.g. the Kenney mirror
203
- // GLBs point at `Textures/*.png`). Fetch those siblings relative to the source
204
- // URL so the add pipeline's dependency check resolves them. Failures here are
205
- // soft: the add step reports any that are genuinely unreachable.
206
- await fetchExternalDependencies(buffer, url, destPath);
207
- return { path: destPath };
208
- };
209
- /** External `uri` refs (images + buffers) in a GLB JSON chunk or a .gltf file. */
210
- function externalUris(modelBytes, destPath) {
211
- let json;
212
- try {
213
- if (startsWith(modelBytes, GLB_MAGIC)) {
214
- // GLB: 12-byte header, then chunk [u32 len][u32 type=JSON][bytes].
215
- if (modelBytes.length < 20)
216
- return [];
217
- const jsonLen = modelBytes.readUInt32LE(12);
218
- json = JSON.parse(modelBytes.toString("utf8", 20, 20 + jsonLen));
219
- }
220
- else if (destPath.toLowerCase().endsWith(".gltf")) {
221
- json = JSON.parse(modelBytes.toString("utf8"));
222
- }
223
- else {
224
- return [];
225
- }
226
- }
227
- catch {
228
- return [];
229
- }
230
- const doc = json;
231
- const uris = [...(doc.images ?? []), ...(doc.buffers ?? [])]
232
- .map((entry) => entry.uri)
233
- .filter((uri) => typeof uri === "string" && uri.length > 0 && !uri.startsWith("data:"));
234
- return [...new Set(uris)];
235
- }
236
- /** Best-effort fetch of a model's external sibling resources next to destPath. */
237
- async function fetchExternalDependencies(modelBytes, sourceUrl, destPath) {
238
- const uris = externalUris(modelBytes, destPath);
239
- if (uris.length === 0)
240
- return;
241
- const dir = dirname(destPath);
242
- await Promise.all(uris.map(async (uri) => {
243
- const decoded = decodeURIComponent(uri);
244
- const target = join(dir, decoded);
245
- let resolvedUrl;
246
- try {
247
- resolvedUrl = new URL(uri, sourceUrl).toString();
248
- }
249
- catch {
250
- return;
251
- }
252
- try {
253
- const res = await fetch(resolvedUrl, { headers: { accept: "*/*" } });
254
- if (!res.ok)
255
- return;
256
- const bytes = Buffer.from(await res.arrayBuffer());
257
- if (bytes.length === 0)
258
- return;
259
- mkdirSync(dirname(target), { recursive: true });
260
- writeFileSync(target, bytes);
261
- }
262
- catch {
263
- // Soft failure: leave the file absent; addAsset surfaces the gap.
264
- }
265
- }));
266
- }
267
- function startsWith(buffer, magic) {
268
- return buffer.length >= magic.length && buffer.subarray(0, magic.length).equals(magic);
269
- }
270
- /**
271
- * Some catalog `download` endpoints return `{ gltf: { url }, ... }`. When the
272
- * sandbox has no creds the URL is usually still un-fetchable, but we attempt the
273
- * obvious direct artifact field so a future credentialed run works end-to-end.
274
- */
275
- async function followSignedDownload(buffer, destPath) {
276
- let parsed;
277
- try {
278
- parsed = JSON.parse(buffer.toString("utf8"));
279
- }
280
- catch {
281
- return undefined;
282
- }
283
- const envelope = parsed;
284
- const directUrl = pickUrl(envelope?.["glb"]) ?? pickUrl(envelope?.["gltf"]) ?? pickUrl(envelope?.["model"]);
285
- if (!directUrl)
286
- return undefined;
287
- const res = await fetch(directUrl, { headers: { accept: "model/gltf-binary,application/zip,*/*" } });
288
- if (!res.ok)
289
- return undefined;
290
- const inner = Buffer.from(await res.arrayBuffer());
291
- if (inner.length === 0)
292
- return undefined;
293
- if (startsWith(inner, ZIP_MAGIC))
294
- return unpackZipToModel(inner, destPath);
295
- writeFileSync(destPath, inner);
296
- return { path: destPath };
297
- }
298
- function pickUrl(value) {
299
- if (typeof value === "string")
300
- return value;
301
- if (value && typeof value === "object") {
302
- const url = value["url"];
303
- if (typeof url === "string")
304
- return url;
305
- }
306
- return undefined;
307
- }
308
- /**
309
- * Minimal, dependency-free ZIP reader: walk the End-Of-Central-Directory and
310
- * central directory, inflate (or copy stored) entries, write them next to
311
- * `destPath`, and return the path to the assembled .glb (preferred) or .gltf.
312
- * Sibling .bin/texture files land in the same dir so the add pipeline's
313
- * dependency copier finds them.
314
- */
315
- function unpackZipToModel(zip, destPath) {
316
- const dir = dirname(destPath);
317
- const entries = readZipEntries(zip);
318
- if (entries.length === 0) {
319
- throw new Error("Aura3D resolve failed: downloaded ZIP contained no files.");
320
- }
321
- let glbPath;
322
- let gltfPath;
323
- for (const entry of entries) {
324
- if (entry.name.endsWith("/"))
325
- continue;
326
- const safeName = basename(entry.name);
327
- if (!safeName)
328
- continue;
329
- const outPath = join(dir, safeName);
330
- writeFileSync(outPath, entry.data);
331
- const lower = safeName.toLowerCase();
332
- if (lower.endsWith(".glb") && (!glbPath || isPlausibleGlb(entry.data)))
333
- glbPath = outPath;
334
- else if (lower.endsWith(".gltf") && !gltfPath)
335
- gltfPath = outPath;
336
- }
337
- const chosen = glbPath ?? gltfPath;
338
- if (!chosen) {
339
- throw new Error("Aura3D resolve failed: ZIP did not contain a .glb or .gltf model.");
340
- }
341
- return { path: chosen };
342
- }
343
- function isPlausibleGlb(data) {
344
- return startsWith(data, GLB_MAGIC);
345
- }
346
- /** Parse central-directory records and inflate each entry's local data. */
347
- function readZipEntries(zip) {
348
- const eocd = findEocd(zip);
349
- if (eocd < 0)
350
- return [];
351
- const total = zip.readUInt16LE(eocd + 10);
352
- let cd = zip.readUInt32LE(eocd + 16);
353
- const entries = [];
354
- for (let i = 0; i < total; i += 1) {
355
- if (cd + 46 > zip.length || zip.readUInt32LE(cd) !== 0x02014b50)
356
- break;
357
- const method = zip.readUInt16LE(cd + 10);
358
- const compSize = zip.readUInt32LE(cd + 20);
359
- const nameLen = zip.readUInt16LE(cd + 28);
360
- const extraLen = zip.readUInt16LE(cd + 30);
361
- const commentLen = zip.readUInt16LE(cd + 32);
362
- const localOffset = zip.readUInt32LE(cd + 42);
363
- const name = zip.toString("utf8", cd + 46, cd + 46 + nameLen);
364
- const data = inflateLocalEntry(zip, localOffset, method, compSize);
365
- if (data)
366
- entries.push({ name, data });
367
- cd += 46 + nameLen + extraLen + commentLen;
368
- }
369
- return entries;
370
- }
371
- function inflateLocalEntry(zip, localOffset, method, compSize) {
372
- if (localOffset + 30 > zip.length || zip.readUInt32LE(localOffset) !== 0x04034b50)
373
- return undefined;
374
- const nameLen = zip.readUInt16LE(localOffset + 26);
375
- const extraLen = zip.readUInt16LE(localOffset + 28);
376
- const dataStart = localOffset + 30 + nameLen + extraLen;
377
- const comp = zip.subarray(dataStart, dataStart + compSize);
378
- if (method === 0)
379
- return Buffer.from(comp); // stored
380
- if (method === 8) {
381
- try {
382
- return inflateRawSync(comp);
383
- }
384
- catch {
385
- return undefined;
386
- }
387
- }
388
- return undefined; // unsupported compression method
389
- }
390
- function findEocd(zip) {
391
- // EOCD signature 0x06054b50, scanning back from the end past any comment.
392
- const min = Math.max(0, zip.length - 0xffff - 22);
393
- for (let i = zip.length - 22; i >= min; i -= 1) {
394
- if (zip.readUInt32LE(i) === 0x06054b50)
395
- return i;
396
- }
397
- return -1;
398
- }
399
- function toLine(candidate, profile = "general") {
400
- const { asset } = candidate;
401
- const line = {
402
- id: asset.id,
403
- source: asset.source,
404
- title: asset.title,
405
- license: asset.license.spdx,
406
- autoPullable: isAutoPullable(asset),
407
- access: asset.access,
408
- };
409
- if (asset.sourcePage)
410
- line.sourcePage = asset.sourcePage;
411
- if (profile !== "general") {
412
- const evaluation = evaluateAssetProfile(asset, profile);
413
- line.profile = {
414
- name: profile,
415
- suitable: evaluation.suitable,
416
- rejectionReasons: evaluation.rejectionReasons,
417
- warnings: evaluation.warnings,
418
- validationHooks: evaluation.validationHooks ?? [],
419
- };
420
- }
421
- return line;
422
- }
423
- function makeDefaultResolver(adapters, limit) {
424
- return new FederatedResolver({ adapters, limit });
425
- }
426
- /** Run a federated search and produce a printable report. */
427
- export async function runSearch(options) {
428
- const env = options.env ?? process.env;
429
- const limit = options.limit ?? 10;
430
- const adapters = buildSearchAdapters(env);
431
- const resolver = options.makeResolver
432
- ? options.makeResolver(adapters)
433
- : makeDefaultResolver(adapters, limit);
434
- const constraints = toResolveConstraints(options.constraints ?? {}, false);
435
- const profile = options.constraints?.profile ?? "general";
436
- const result = await resolver.resolve({
437
- text: options.query,
438
- constraints,
439
- });
440
- const rankedCandidates = rankForProfile(result.candidates, profile);
441
- const candidateLines = rankedCandidates.map((candidate) => toLine(candidate, profile));
442
- const candidates = profile !== "general"
443
- ? candidateLines.filter((candidate) => candidate.profile?.suitable === true)
444
- : candidateLines;
445
- const rejectedCandidates = profile !== "general"
446
- ? candidateLines.filter((candidate) => candidate.profile?.suitable !== true)
447
- : [];
448
- const anyPullable = candidates.some((c) => c.autoPullable);
449
- const anyProfileSuitable = profile === "general" || candidates.some((c) => c.profile?.suitable);
450
- const anyProfilePullable = profile === "general" || candidates.some((c) => c.autoPullable && c.profile?.suitable);
451
- const warnings = [...result.warnings];
452
- if (profile !== "general") {
453
- for (const candidate of candidateLines) {
454
- for (const warning of candidate.profile?.warnings ?? []) {
455
- warnings.push(`${candidate.id}: ${warning}`);
456
- }
457
- }
458
- }
459
- let deepLinks = [];
460
- if (!anyPullable) {
461
- const deepLinkAdapter = buildDeepLinkAdapter();
462
- if (deepLinkAdapter) {
463
- try {
464
- const records = await deepLinkAdapter.search({ text: options.query, constraints: {} }, { fetchJson: assetIndex.defaultFetchJson });
465
- deepLinks = records.map((asset) => toLine({ asset, score: 0 }));
466
- }
467
- catch (err) {
468
- warnings.push(`${deepLinkAdapter.id}: ${err.message}`);
469
- }
470
- }
471
- }
472
- const messages = [];
473
- if (candidateLines.length === 0) {
474
- messages.push(`No candidates found for "${options.query}".`);
475
- }
476
- else {
477
- messages.push(`${candidates.length} candidate(s) for "${options.query}" using ${profile} profile.`);
478
- if (rejectedCandidates.length > 0) {
479
- messages.push(`${rejectedCandidates.length} rejected candidate(s) moved to rejectedCandidates by the ${profile} profile.`);
480
- }
481
- if (!anyPullable) {
482
- messages.push("No auto-pullable candidate. Listed assets need a manual license check before use.");
483
- }
484
- if (profile !== "general" && !anyProfileSuitable) {
485
- messages.push(`No ${profile}-ready candidate. Listed candidates were rejected by the asset profile; inspect rejectionReasons before resolving.`);
486
- }
487
- else if (profile !== "general" && !anyProfilePullable) {
488
- messages.push(`No auto-pullable ${profile}-ready candidate. Resolve will refuse until a downloadable licensed candidate also passes the profile.`);
489
- }
490
- }
491
- if (deepLinks.length > 0) {
492
- messages.push(`${deepLinks.length} marketplace deep-link(s) — manual download (license check required).`);
493
- }
494
- return {
495
- ok: true,
496
- query: options.query,
497
- profile,
498
- candidates,
499
- rejectedCandidates,
500
- deepLinks,
501
- warnings,
502
- messages,
503
- };
504
- }
24
+ import { addAsset, inspectAsset } from "./index.js";
25
+ import { buildSearchAdapters } from "./pull-bridge/adapters.js";
26
+ import { defaultDownloadFile } from "./pull-bridge/download.js";
27
+ import { evaluateAssetProfile, rankForProfile, toResolveConstraints, } from "./pull-bridge/profiles.js";
28
+ import { createPostDownloadCandidateBlockingWarnings, createPreDownloadCandidateBlockingWarnings, createResolveCandidateProvenance, mapCanonicalRoleToCli, } from "./pull-bridge/provenance.js";
29
+ import { selectPullable } from "./pull-bridge/pullable.js";
30
+ import { rankResolveCandidates, scoreResolveCandidate, } from "./pull-bridge/scoring.js";
31
+ import { makeDefaultResolver, runSearch, toLine, } from "./pull-bridge/search.js";
32
+ export { defaultDownloadFile, rankResolveCandidates, runSearch, selectPullable, scoreResolveCandidate, toResolveConstraints, };
33
+ const { isAutoPullable } = assetIndex;
34
+ export { buildDeepLinkAdapter, buildSearchAdapters } from "./pull-bridge/adapters.js";
505
35
  /**
506
36
  * Resolve a query to the top auto-pullable candidate, download its .glb to a
507
37
  * temp path, and run the EXISTING `addAsset` pipeline so it lands as a typed
@@ -521,7 +51,10 @@ export async function runResolve(options) {
521
51
  const constraints = toResolveConstraints(options.constraints ?? {}, true);
522
52
  const profile = options.constraints?.profile ?? "general";
523
53
  const result = await resolver.resolve({ text: options.query, constraints });
524
- const ranked = rankForProfile(result.candidates, profile);
54
+ const ranked = rankResolveCandidates(rankForProfile(result.candidates, profile), {
55
+ query: options.query,
56
+ profile,
57
+ });
525
58
  const selection = selectPullable(ranked, { profile });
526
59
  if (!selection.ok) {
527
60
  throw new Error(`Aura3D resolve refused: ${selection.reason}`);
@@ -540,11 +73,16 @@ export async function runResolve(options) {
540
73
  const download = options.download ?? defaultDownloadFile;
541
74
  const tmpRoot = options.tmpRoot ?? tmpdir();
542
75
  const addFn = options.addAssetFn ?? addAsset;
76
+ const inspectFn = options.inspectAssetFn ?? inspectAsset;
543
77
  const retrievedAt = options.retrievedAt ?? new Date().toISOString();
544
78
  const attemptWarnings = [];
545
79
  for (const candidateChoice of pullable) {
546
80
  const asset = candidateChoice.asset;
547
81
  try {
82
+ const preDownloadWarnings = createPreDownloadCandidateBlockingWarnings(asset);
83
+ if (preDownloadWarnings.length > 0) {
84
+ throw new Error(`pre-download candidate quality blocked: ${preDownloadWarnings.join("; ")}`);
85
+ }
548
86
  const dir = mkdtempSync(join(tmpRoot, "aura3d-resolve-"));
549
87
  const ext = asset.format === "gltf" ? "gltf" : "glb";
550
88
  const tempFile = join(dir, `${options.name}.${ext}`);
@@ -556,28 +94,58 @@ export async function runResolve(options) {
556
94
  // timestamp (injectable for determinism) so provenance records exactly what
557
95
  // was fetched. (#19/#26)
558
96
  const sha256 = `sha256-${createHash("sha256").update(readFileSync(resolvedFile)).digest("hex")}`;
97
+ const inspection = inspectFn({
98
+ file: resolvedFile,
99
+ ...(options.projectDir ? { projectDir: options.projectDir } : {}),
100
+ animation: true,
101
+ humanoid: true,
102
+ skeleton: true,
103
+ morphs: true,
104
+ license: true,
105
+ });
106
+ const postDownloadWarnings = createPostDownloadCandidateBlockingWarnings(asset, inspection, profile);
107
+ if (postDownloadWarnings.length > 0) {
108
+ throw new Error(`post-download inspection blocked candidate: ${postDownloadWarnings.join("; ")}`);
109
+ }
110
+ const candidateScore = scoreResolveCandidate(candidateChoice, {
111
+ query: options.query,
112
+ profile,
113
+ });
114
+ const resolveCandidate = createResolveCandidateProvenance(candidateChoice, options.query, candidateScore, inspection);
559
115
  const add = addFn({
560
116
  file: resolvedFile,
561
117
  name: options.name,
562
118
  ...(options.projectDir ? { projectDir: options.projectDir } : {}),
119
+ sourcePage: asset.sourcePage,
120
+ downloadUrl: asset.downloadUrl ?? asset.url,
563
121
  sourceUrl: asset.sourcePage ?? asset.url,
564
122
  license: asset.license.spdx,
565
- sourceFamily: asset.source,
566
- attribution: asset.attribution,
567
- author: asset.attribution,
123
+ licenseName: (asset.licenseName ?? asset.license.raw) || asset.license.spdx,
124
+ licenseUrl: asset.licenseUrl ?? asset.license.sourcePage,
125
+ licenseRaw: asset.license.raw,
126
+ sourceFamily: asset.sourceFamily ?? asset.source,
127
+ attribution: asset.attribution ?? asset.author,
128
+ author: asset.author ?? asset.attribution,
568
129
  sha256,
130
+ resolveCandidate,
569
131
  retrievedAt,
132
+ quality: "candidate",
133
+ role: mapCanonicalRoleToCli(asset.intendedRole),
134
+ suitabilityReason: asset.roleSuitability ?? "Resolved catalog candidate; release validation and rendered-probe proof are still required.",
570
135
  });
571
136
  const typedRef = `model(assets.${options.name})`;
572
137
  const messages = [...attemptWarnings, ...add.messages];
573
- messages.push(`Pulled ${asset.id} (${asset.license.spdx}) from ${asset.source}.`);
138
+ messages.push(`Resolved candidate ${asset.id} (${asset.license.spdx}) from ${asset.source}.`);
574
139
  messages.push(`Provenance: sha256 ${sha256}, retrieved ${retrievedAt}, source ${asset.sourcePage ?? asset.url}.`);
140
+ messages.push(`Candidate score ${candidateScore.total}: ${candidateScore.reasons.join("; ") || "no positive score reasons"}. ` +
141
+ `Release validation is still required before public examples.`);
575
142
  if (asset.license.attributionRequired) {
576
143
  const credit = asset.attribution ?? asset.sourcePage ?? asset.source;
577
144
  messages.push(`Attribution required (${asset.license.spdx}): credit "${credit}". ` +
578
145
  `See ${asset.sourcePage ?? asset.url}.`);
579
146
  }
580
147
  messages.push(`Use the typed ref: ${typedRef}`);
148
+ messages.push(`Next: run aura3d assets validate --release --source <entrypoint> and attach retained rendered-probe proof before claiming release readiness.`);
581
149
  return {
582
150
  ok: add.ok,
583
151
  profile,
@@ -595,39 +163,4 @@ export async function runResolve(options) {
595
163
  throw new Error(`Aura3D resolve failed: every auto-pullable candidate failed to download or assemble. ` +
596
164
  attemptWarnings.join(" | "));
597
165
  }
598
- function rankForProfile(candidates, profile) {
599
- if (profile === "general")
600
- return candidates;
601
- return [...candidates].sort((a, b) => {
602
- const aEval = evaluateAssetProfile(a.asset, profile);
603
- const bEval = evaluateAssetProfile(b.asset, profile);
604
- return (Number(bEval.suitable) - Number(aEval.suitable) ||
605
- (b.score + bEval.scoreBonus) - (a.score + aEval.scoreBonus) ||
606
- a.asset.id.localeCompare(b.asset.id));
607
- });
608
- }
609
- function isAnimationCliProfile(profile) {
610
- return typeof profile === "string" && isAnimationAssetProfile(profile);
611
- }
612
- function evaluateAssetProfile(asset, profile) {
613
- // CLI search/resolve runs against the hosted catalog *before* downloading, so
614
- // animation profiles evaluate in pre-download mode: absent rig/animation/triangle
615
- // metadata down-ranks and defers to a post-download validation hook instead of
616
- // hard-rejecting every catalog candidate. (#20/#23)
617
- return profile === "fighting-character"
618
- ? evaluateGameAssetProfile(asset, "fighting-character")
619
- : evaluateAnimationAssetProfile(asset, profile, { preDownload: true });
620
- }
621
- function animationProfileMaxTriangles(profile) {
622
- switch (profile) {
623
- case "animation-character":
624
- return 160_000;
625
- case "animation-prop":
626
- return 100_000;
627
- case "animation-set":
628
- return 350_000;
629
- case "animation-environment":
630
- return 250_000;
631
- }
632
- }
633
166
  //# sourceMappingURL=pull-bridge.js.map