@qloo/qloo-harness 0.1.18

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.
@@ -0,0 +1,1380 @@
1
+ import { createRequire as __qlooCreateRequire } from "node:module";
2
+ const require = __qlooCreateRequire(import.meta.url);
3
+
4
+ // packages/qloo-tool-schema/dist/upstream.lock.json
5
+ var upstream_lock_default = {
6
+ lockVersion: 1,
7
+ source: {
8
+ documentationUrl: "https://docs.qloo.com",
9
+ registryUrl: "https://dash.readme.com/api/v1/api-registry/4svkj1svmrti5tfx",
10
+ registryId: "4svkj1svmrti5tfx",
11
+ retrievedAt: "2026-08-20",
12
+ apiTitle: "Qloo API",
13
+ apiVersion: "2.0",
14
+ openapiVersion: "3.1.0",
15
+ sha256: "da8d4b830f2a646815d25afcaa653f81f1ad939707e2e2560d2d0b9f103076f1"
16
+ },
17
+ expectations: {
18
+ productionServer: "https://api.qloo.com",
19
+ authentication: {
20
+ scheme: "ApiKey",
21
+ type: "apiKey",
22
+ location: "header",
23
+ header: "X-Api-Key"
24
+ },
25
+ getPaths: [
26
+ "/",
27
+ "/entities",
28
+ "/search",
29
+ "/v2/analysis/compare",
30
+ "/v2/audiences",
31
+ "/v2/audiences/types",
32
+ "/v2/insights",
33
+ "/v2/tags",
34
+ "/v2/tags/types",
35
+ "/v2/trending"
36
+ ]
37
+ },
38
+ documentedExceptions: [
39
+ {
40
+ value: "urn:demographics",
41
+ scope: "/v2/insights filter.type",
42
+ status: "documented-but-not-enumerated",
43
+ documentationUrl: "https://docs.qloo.com/reference/demographics-use-case",
44
+ treatment: "Preserve as an explicit documentation exception; do not synthesize an endpoint payload schema from it."
45
+ },
46
+ {
47
+ value: "urn:tag",
48
+ scope: "/v2/insights filter.type",
49
+ status: "documented-but-not-enumerated",
50
+ documentationUrl: "https://docs.qloo.com/reference/taste-analysis",
51
+ treatment: "The pinned specification mentions this value in diversify.by documentation but omits it from the filter.type enum; preserve the discrepancy for review."
52
+ }
53
+ ]
54
+ };
55
+
56
+ // packages/qloo-tool-schema/dist/src/catalog.js
57
+ var QLOO_TOOL_CATALOG_VERSION = "1.0.0";
58
+ var QLOO_TOOL_RESULT_SCHEMA_VERSION = "1.0-preview.1";
59
+ var QLOO_WORKFLOW_OPERATION_IDS = [
60
+ "recommend",
61
+ "rank",
62
+ "describe",
63
+ "where_popular",
64
+ "compare_audiences",
65
+ "entity_tags",
66
+ "audience_demographics",
67
+ "trends",
68
+ "find_tags"
69
+ ];
70
+ var QLOO_CAPABILITIES_TOOL_NAME = "qloo_capabilities";
71
+ var QLOO_INSIGHTS_ENTITY_TYPES = [
72
+ { id: "artist", urn: "urn:entity:artist" },
73
+ { id: "book", urn: "urn:entity:book" },
74
+ { id: "brand", urn: "urn:entity:brand" },
75
+ { id: "movie", urn: "urn:entity:movie" },
76
+ { id: "person", urn: "urn:entity:person" },
77
+ { id: "place", urn: "urn:entity:place" },
78
+ { id: "podcast", urn: "urn:entity:podcast" },
79
+ { id: "tv_show", urn: "urn:entity:tv_show" },
80
+ { id: "videogame", urn: "urn:entity:videogame" }
81
+ ];
82
+ var QLOO_SEARCH_ENTITY_TYPES = [
83
+ ...QLOO_INSIGHTS_ENTITY_TYPES,
84
+ { id: "locality", urn: "urn:entity:locality" },
85
+ { id: "actor", urn: "urn:entity:actor" },
86
+ { id: "album", urn: "urn:entity:album" },
87
+ { id: "author", urn: "urn:entity:author" },
88
+ { id: "director", urn: "urn:entity:director" }
89
+ ];
90
+ function buildEntityTypeAliases(entityTypes) {
91
+ const aliases = /* @__PURE__ */ new Map();
92
+ const urnById = new Map(entityTypes.map(({ id, urn }) => [id, urn]));
93
+ for (const entityType2 of entityTypes) {
94
+ aliases.set(entityType2.id, entityType2.urn);
95
+ aliases.set(entityType2.urn, entityType2.urn);
96
+ aliases.set(`${entityType2.id}s`, entityType2.urn);
97
+ }
98
+ const addAlias = (alias, id) => {
99
+ const urn = urnById.get(id);
100
+ if (urn)
101
+ aliases.set(alias, urn);
102
+ };
103
+ addAlias("people", "person");
104
+ addAlias("localities", "locality");
105
+ addAlias("tv", "tv_show");
106
+ addAlias("show", "tv_show");
107
+ addAlias("shows", "tv_show");
108
+ addAlias("tv show", "tv_show");
109
+ addAlias("tv shows", "tv_show");
110
+ addAlias("game", "videogame");
111
+ addAlias("games", "videogame");
112
+ addAlias("video game", "videogame");
113
+ addAlias("video games", "videogame");
114
+ return aliases;
115
+ }
116
+ var insightsEntityTypeByAlias = buildEntityTypeAliases(QLOO_INSIGHTS_ENTITY_TYPES);
117
+ var searchEntityTypeByAlias = buildEntityTypeAliases(QLOO_SEARCH_ENTITY_TYPES);
118
+ var operationSummaries = {
119
+ recommend: "Find entities for a taste profile, audience, or collection of signals.",
120
+ rank: "Rank a supplied set of options for signals, location, or audience.",
121
+ describe: "Resolve and describe one Qloo entity.",
122
+ where_popular: "Find geographic areas where an entity has comparatively strong affinity.",
123
+ compare_audiences: "Contrast two signal groups and identify shared or differentiating affinities.",
124
+ entity_tags: "Retrieve concepts and tags that characterize entities.",
125
+ audience_demographics: "Retrieve aggregate audience segments associated with an entity.",
126
+ trends: "Retrieve interest or trend information over a time range.",
127
+ find_tags: "Search the Qloo tag ontology by natural-language concept."
128
+ };
129
+ var QLOO_WORKFLOW_OPERATIONS = Object.freeze(QLOO_WORKFLOW_OPERATION_IDS.map((id) => Object.freeze({
130
+ id,
131
+ toolName: `qloo_${id}`,
132
+ summary: operationSummaries[id],
133
+ since: QLOO_TOOL_CATALOG_VERSION
134
+ })));
135
+ var upstreamLock = upstream_lock_default;
136
+ var upstreamGetPaths = Object.freeze([
137
+ ...upstreamLock.expectations.getPaths
138
+ ]);
139
+ var documentedExceptions = Object.freeze(upstreamLock.documentedExceptions.map((exception) => Object.freeze({ ...exception })));
140
+ var QLOO_CAPABILITIES_TOOL = Object.freeze({
141
+ name: QLOO_CAPABILITIES_TOOL_NAME,
142
+ description: "Return the versioned Qloo workflow catalog and pinned upstream API contract metadata without making a network request.",
143
+ inputSchema: Object.freeze({
144
+ $schema: "https://json-schema.org/draft/2020-12/schema",
145
+ type: "object",
146
+ additionalProperties: false,
147
+ properties: Object.freeze({
148
+ operation_ids: Object.freeze({
149
+ type: "array",
150
+ description: "Optional canonical operation IDs to include. Omit for the complete catalog.",
151
+ uniqueItems: true,
152
+ items: Object.freeze({
153
+ type: "string",
154
+ enum: QLOO_WORKFLOW_OPERATION_IDS
155
+ })
156
+ })
157
+ })
158
+ })
159
+ });
160
+ var operationIds = new Set(QLOO_WORKFLOW_OPERATION_IDS);
161
+
162
+ // packages/qloo-tool-schema/dist/src/workflow-contracts.js
163
+ import { Type } from "typebox";
164
+ import { Value } from "typebox/value";
165
+ var QLOO_TOOL_DEFAULT_LIMIT = 10;
166
+ var QLOO_TOOL_MAX_RESULTS = 20;
167
+ var QLOO_TAG_BOOLEAN_OPERATORS = ["union", "intersection"];
168
+ var insightsEntityTypeIds = QLOO_INSIGHTS_ENTITY_TYPES.map(({ id }) => id);
169
+ var searchEntityTypeIds = QLOO_SEARCH_ENTITY_TYPES.map(({ id }) => id);
170
+ var QLOO_INSIGHTS_ENTITY_TYPE_SCHEMA = Type.String({
171
+ enum: insightsEntityTypeIds,
172
+ description: `Qloo Insights entity type: ${insightsEntityTypeIds.join(", ")}.`
173
+ });
174
+ var QLOO_SEARCH_ENTITY_TYPE_SCHEMA = Type.String({
175
+ enum: searchEntityTypeIds,
176
+ description: `Qloo search entity type: ${searchEntityTypeIds.join(", ")}.`
177
+ });
178
+ var QLOO_TAG_BOOLEAN_OPERATOR_SCHEMA = Type.String({
179
+ enum: QLOO_TAG_BOOLEAN_OPERATORS,
180
+ description: "Combine tag values with union (any) or intersection (all)."
181
+ });
182
+ var QLOO_LIMIT_SCHEMA = Type.Optional(Type.Integer({
183
+ minimum: 1,
184
+ maximum: QLOO_TOOL_MAX_RESULTS,
185
+ default: QLOO_TOOL_DEFAULT_LIMIT,
186
+ description: `Maximum results to return (1-${QLOO_TOOL_MAX_RESULTS}).`
187
+ }));
188
+ var QLOO_ENTITY_INPUTS_SCHEMA = Type.Array(Type.String({ minLength: 1 }), {
189
+ minItems: 1,
190
+ maxItems: 10,
191
+ description: "Entity names or Qloo entity UUIDs. Keep one combined taste profile in one call."
192
+ });
193
+ var QLOO_TAG_INPUTS_SCHEMA = Type.Array(Type.String({ minLength: 1 }), {
194
+ minItems: 1,
195
+ maxItems: 10,
196
+ description: "Natural-language tag concepts or stable Qloo tag URNs."
197
+ });
198
+ var QLOO_DEMOGRAPHIC_SCHEMA = Type.String({
199
+ minLength: 1,
200
+ description: "Audience demographic in natural language, such as 'young women', 'men 25-34', or 'millennials'. It is converted to documented Qloo demographic signals.",
201
+ examples: ["young women", "men 25-34", "millennials"]
202
+ });
203
+ var QLOO_REQUEST_PREVIEW_SCHEMA = Type.Object({
204
+ method: Type.Literal("GET"),
205
+ path: Type.String({ minLength: 1 }),
206
+ query: Type.Unknown()
207
+ }, { additionalProperties: false });
208
+ var QLOO_RESULT_ENVELOPE_SCHEMA = Type.Object({
209
+ schema_version: Type.Literal(QLOO_TOOL_RESULT_SCHEMA_VERSION),
210
+ operation: Type.String(),
211
+ status: Type.Union([
212
+ Type.Literal("ok"),
213
+ Type.Literal("empty"),
214
+ Type.Literal("needs_input"),
215
+ Type.Literal("partial"),
216
+ Type.Literal("degraded"),
217
+ Type.Literal("error")
218
+ ]),
219
+ summary: Type.Optional(Type.String()),
220
+ interpretation: Type.Optional(Type.Unknown()),
221
+ results: Type.Optional(Type.Unknown()),
222
+ result_count: Type.Optional(Type.Integer({ minimum: 0 })),
223
+ warnings: Type.Optional(Type.Array(Type.String())),
224
+ query_intent: Type.Optional(Type.Unknown()),
225
+ error: Type.Optional(Type.Object({
226
+ code: Type.String(),
227
+ layer: Type.String(),
228
+ retryable: Type.Boolean(),
229
+ recovery: Type.String()
230
+ }, { additionalProperties: false })),
231
+ execution: Type.Object({
232
+ transport: Type.String({ enum: ["direct", "cli", "mcp"] }),
233
+ transport_name: Type.String(),
234
+ correlation_id: Type.String(),
235
+ duration_ms: Type.Number({ minimum: 0 })
236
+ }, { additionalProperties: false }),
237
+ provenance: Type.Optional(Type.Object({
238
+ source: Type.String(),
239
+ endpoint: Type.String(),
240
+ documentation: Type.String(),
241
+ transport: Type.Optional(Type.String()),
242
+ requests: Type.Optional(Type.Array(QLOO_REQUEST_PREVIEW_SCHEMA, {
243
+ maxItems: 20
244
+ }))
245
+ }, { additionalProperties: false }))
246
+ }, { additionalProperties: true });
247
+ var QLOO_WORKFLOW_INPUT_SCHEMAS = {
248
+ recommend: Type.Object({
249
+ target_type: QLOO_INSIGHTS_ENTITY_TYPE_SCHEMA,
250
+ signals: Type.Optional(QLOO_ENTITY_INPUTS_SCHEMA),
251
+ signal_tags: Type.Optional(QLOO_TAG_INPUTS_SCHEMA),
252
+ signal_tags_operator: Type.Optional(QLOO_TAG_BOOLEAN_OPERATOR_SCHEMA),
253
+ signal_location: Type.Optional(Type.String({ minLength: 1 })),
254
+ demographic: Type.Optional(QLOO_DEMOGRAPHIC_SCHEMA),
255
+ filter_location: Type.Optional(Type.String({ minLength: 1 })),
256
+ include_tags: Type.Optional(QLOO_TAG_INPUTS_SCHEMA),
257
+ include_tags_operator: Type.Optional(QLOO_TAG_BOOLEAN_OPERATOR_SCHEMA),
258
+ exclude_tags: Type.Optional(QLOO_TAG_INPUTS_SCHEMA),
259
+ exclude_tags_operator: Type.Optional(QLOO_TAG_BOOLEAN_OPERATOR_SCHEMA),
260
+ explain: Type.Optional(Type.Boolean({ default: true })),
261
+ limit: QLOO_LIMIT_SCHEMA
262
+ }, { additionalProperties: false }),
263
+ rank: Type.Object({
264
+ options: QLOO_ENTITY_INPUTS_SCHEMA,
265
+ option_type: QLOO_INSIGHTS_ENTITY_TYPE_SCHEMA,
266
+ signals: Type.Optional(QLOO_ENTITY_INPUTS_SCHEMA),
267
+ signal_location: Type.Optional(Type.String({ minLength: 1 })),
268
+ demographic: Type.Optional(QLOO_DEMOGRAPHIC_SCHEMA),
269
+ include_tags: Type.Optional(QLOO_TAG_INPUTS_SCHEMA),
270
+ exclude_tags: Type.Optional(QLOO_TAG_INPUTS_SCHEMA)
271
+ }, { additionalProperties: false }),
272
+ describe: Type.Object({
273
+ entity: Type.String({ minLength: 1 }),
274
+ type: Type.Optional(QLOO_SEARCH_ENTITY_TYPE_SCHEMA)
275
+ }, { additionalProperties: false }),
276
+ where_popular: Type.Object({
277
+ entity: Type.String({ minLength: 1 }),
278
+ entity_type: Type.Optional(QLOO_SEARCH_ENTITY_TYPE_SCHEMA),
279
+ within: Type.String({
280
+ minLength: 1,
281
+ description: "Required geographic area that constrains the heatmap."
282
+ }),
283
+ limit: QLOO_LIMIT_SCHEMA
284
+ }, { additionalProperties: false }),
285
+ compare_audiences: Type.Object({
286
+ group_a: QLOO_ENTITY_INPUTS_SCHEMA,
287
+ group_b: QLOO_ENTITY_INPUTS_SCHEMA,
288
+ target_type: Type.Optional(QLOO_INSIGHTS_ENTITY_TYPE_SCHEMA),
289
+ limit: QLOO_LIMIT_SCHEMA
290
+ }, { additionalProperties: false }),
291
+ entity_tags: Type.Object({
292
+ entities: QLOO_ENTITY_INPUTS_SCHEMA,
293
+ entity_type: Type.Optional(QLOO_SEARCH_ENTITY_TYPE_SCHEMA),
294
+ limit: QLOO_LIMIT_SCHEMA
295
+ }, { additionalProperties: false }),
296
+ audience_demographics: Type.Object({
297
+ entity: Type.String({ minLength: 1 }),
298
+ entity_type: Type.Optional(QLOO_SEARCH_ENTITY_TYPE_SCHEMA),
299
+ limit: QLOO_LIMIT_SCHEMA
300
+ }, { additionalProperties: false }),
301
+ trends: Type.Object({
302
+ entities: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, maxItems: 5 }),
303
+ entity_type: QLOO_INSIGHTS_ENTITY_TYPE_SCHEMA,
304
+ start_date: Type.String({
305
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$",
306
+ description: "Inclusive start date in YYYY-MM-DD format."
307
+ }),
308
+ end_date: Type.String({
309
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$",
310
+ description: "Inclusive end date in YYYY-MM-DD format."
311
+ }),
312
+ limit: QLOO_LIMIT_SCHEMA
313
+ }, { additionalProperties: false }),
314
+ find_tags: Type.Object({
315
+ query: Type.String({ minLength: 1 }),
316
+ semantic: Type.Optional(Type.Boolean({ default: true })),
317
+ limit: QLOO_LIMIT_SCHEMA
318
+ }, { additionalProperties: false })
319
+ };
320
+ var sharedWorkflowBehavior = Object.freeze({
321
+ readOnly: true,
322
+ idempotent: true,
323
+ mutating: false,
324
+ destructive: false,
325
+ openWorld: true,
326
+ concurrency: "safe_for_independent_inputs",
327
+ requiredCredential: "qloo_api",
328
+ defaultResultBudget: QLOO_TOOL_DEFAULT_LIMIT,
329
+ maximumResultBudget: QLOO_TOOL_MAX_RESULTS,
330
+ safeRetryClasses: Object.freeze(["dns", "connect", "timeout", "rate_limit", "upstream_5xx"]),
331
+ supportedTransports: Object.freeze(["direct", "mcp"])
332
+ });
333
+ var mediumLatencyOperations = /* @__PURE__ */ new Set([
334
+ "recommend",
335
+ "rank",
336
+ "compare_audiences",
337
+ "entity_tags",
338
+ "audience_demographics",
339
+ "trends"
340
+ ]);
341
+ var QLOO_WORKFLOW_BEHAVIORS = Object.freeze(Object.fromEntries(Object.keys(QLOO_WORKFLOW_INPUT_SCHEMAS).map((operation) => [
342
+ operation,
343
+ Object.freeze({
344
+ ...sharedWorkflowBehavior,
345
+ expectedLatency: mediumLatencyOperations.has(operation) ? "medium" : "low"
346
+ })
347
+ ])));
348
+ var QLOO_WORKFLOW_METADATA = {
349
+ recommend: {
350
+ toolName: "qloo_recommend",
351
+ label: "Qloo recommendations",
352
+ description: "Find Qloo entities related to one combined entity, tag, location, and demographic taste profile. Signals influence affinity; filters constrain output.",
353
+ promptSnippet: "Recommend entities from a combined Qloo taste profile",
354
+ promptGuidelines: [
355
+ "Pass every related taste and audience signal together in one qloo_recommend call.",
356
+ "Use signal_location for audience location and filter_location to constrain result geography.",
357
+ "Include demographic and location in the same call when both describe the audience."
358
+ ],
359
+ documentation: ["https://docs.qloo.com/reference/insights-api-deep-dive"]
360
+ },
361
+ rank: {
362
+ toolName: "qloo_rank",
363
+ label: "Qloo rank",
364
+ description: "Rank one supplied option set against one shared entity, location, demographic, and tag profile. Scores from separate calls are not comparable.",
365
+ promptSnippet: "Rank a caller-supplied shortlist with Qloo",
366
+ promptGuidelines: ["Pass every option in one qloo_rank call so scores remain comparable."],
367
+ documentation: ["https://docs.qloo.com/reference/insights-api-deep-dive"]
368
+ },
369
+ describe: {
370
+ toolName: "qloo_describe",
371
+ label: "Qloo entity",
372
+ description: "Resolve one named entity or Qloo UUID and return only metadata present in Qloo.",
373
+ promptSnippet: "Resolve and describe one Qloo entity",
374
+ documentation: ["https://docs.qloo.com/reference/get-search", "https://docs.qloo.com/reference/get-entities"]
375
+ },
376
+ where_popular: {
377
+ toolName: "qloo_where_popular",
378
+ label: "Qloo geographic affinity",
379
+ description: "Find geographic areas where one entity has strong query-relative affinity. This is distinct from filtering recommendations to a place.",
380
+ promptSnippet: "Find where an entity is comparatively popular",
381
+ documentation: ["https://docs.qloo.com/reference/heatmaps-use-case"]
382
+ },
383
+ compare_audiences: {
384
+ toolName: "qloo_compare_audiences",
385
+ label: "Qloo audience comparison",
386
+ description: "Compare two complete groups of entity signals in one Qloo analysis request.",
387
+ promptSnippet: "Compare two Qloo taste-signal groups",
388
+ documentation: ["https://docs.qloo.com/reference/analysis-compare"]
389
+ },
390
+ entity_tags: {
391
+ toolName: "qloo_entity_tags",
392
+ label: "Qloo entity tags",
393
+ description: "Retrieve Qloo concepts that jointly characterize one or more entities using the documented urn:tag Insights workflow.",
394
+ promptSnippet: "Find concepts that characterize Qloo entities",
395
+ documentation: ["https://docs.qloo.com/reference/taste-analysis"]
396
+ },
397
+ audience_demographics: {
398
+ toolName: "qloo_audience_demographics",
399
+ label: "Qloo audience demographics",
400
+ description: "Retrieve aggregate demographic distributions associated with one entity through the documented urn:demographics Insights workflow.",
401
+ promptSnippet: "Inspect aggregate demographics associated with a Qloo entity",
402
+ documentation: ["https://docs.qloo.com/reference/demographics-use-case"]
403
+ },
404
+ trends: {
405
+ toolName: "qloo_trends",
406
+ label: "Qloo trends",
407
+ description: "Retrieve Qloo time-series popularity metrics for one to five resolved entities over an ISO date range.",
408
+ promptSnippet: "Track Qloo entity popularity metrics over time",
409
+ documentation: ["https://docs.qloo.com/reference/get-trending"]
410
+ },
411
+ find_tags: {
412
+ toolName: "qloo_find_tags",
413
+ label: "Qloo tag search",
414
+ description: "Search Qloo's tag ontology by natural-language concept and return stable tag URNs.",
415
+ promptSnippet: "Search the Qloo tag ontology",
416
+ documentation: ["https://docs.qloo.com/reference/get-tags-1"]
417
+ }
418
+ };
419
+ var QLOO_WORKFLOW_CONTRACTS = Object.freeze(Object.fromEntries(Object.entries(QLOO_WORKFLOW_METADATA).map(([id, metadata]) => [
420
+ id,
421
+ Object.freeze({
422
+ id,
423
+ ...metadata,
424
+ behavior: QLOO_WORKFLOW_BEHAVIORS[id],
425
+ inputSchema: QLOO_WORKFLOW_INPUT_SCHEMAS[id],
426
+ resultSchema: QLOO_RESULT_ENVELOPE_SCHEMA
427
+ })
428
+ ])));
429
+
430
+ // packages/qloo-tool-schema/dist/src/resolution-contracts.js
431
+ import { Type as Type2 } from "typebox";
432
+ var QLOO_RESOLUTION_CONTRACT_VERSION = "1.0-preview.1";
433
+ var QLOO_RESOLUTION_PROVIDER_DESCRIPTOR_SCHEMA = Type2.Object({
434
+ contract_version: Type2.Literal(QLOO_RESOLUTION_CONTRACT_VERSION),
435
+ provider_id: Type2.String({ minLength: 1 }),
436
+ name: Type2.String({ minLength: 1 }),
437
+ entity_strategy: Type2.String({ minLength: 1 }),
438
+ tag_strategy: Type2.String({ minLength: 1 }),
439
+ remote: Type2.Boolean(),
440
+ uses_model: Type2.Boolean(),
441
+ fallback: Type2.Literal("none")
442
+ }, { additionalProperties: false });
443
+ var QLOO_RESOLUTION_CANDIDATE_SCHEMA = Type2.Object({
444
+ id: Type2.String({ minLength: 1 }),
445
+ name: Type2.String({ minLength: 1 }),
446
+ type: Type2.Optional(Type2.String({ minLength: 1 })),
447
+ rank: Type2.Optional(Type2.Integer({ minimum: 1 })),
448
+ score: Type2.Optional(Type2.Number()),
449
+ popularity: Type2.Optional(Type2.Number()),
450
+ parent_types: Type2.Optional(Type2.Array(Type2.String({ minLength: 1 }), {
451
+ maxItems: 20,
452
+ uniqueItems: true
453
+ })),
454
+ description: Type2.Optional(Type2.String({ minLength: 1, maxLength: 500 })),
455
+ release_year: Type2.Optional(Type2.Integer()),
456
+ address: Type2.Optional(Type2.Unknown())
457
+ }, { additionalProperties: false });
458
+ var QLOO_RESOLUTION_PROVENANCE_SCHEMA = Type2.Object({
459
+ contract_version: Type2.Literal(QLOO_RESOLUTION_CONTRACT_VERSION),
460
+ provider_id: Type2.String({ minLength: 1 }),
461
+ provider_name: Type2.String({ minLength: 1 }),
462
+ endpoint: Type2.String({ minLength: 1 }),
463
+ strategy: Type2.String({ minLength: 1 }),
464
+ remote: Type2.Boolean(),
465
+ uses_model: Type2.Boolean(),
466
+ provider_version: Type2.Optional(Type2.String({ minLength: 1 })),
467
+ cache_status: Type2.Optional(Type2.String({ minLength: 1 }))
468
+ }, { additionalProperties: false });
469
+ var QLOO_RESOLUTION_OUTCOME_COMMON_SCHEMA = {
470
+ input: Type2.String({ minLength: 1 }),
471
+ provenance: QLOO_RESOLUTION_PROVENANCE_SCHEMA,
472
+ warnings: Type2.Optional(Type2.Array(Type2.String()))
473
+ };
474
+ var QLOO_RESOLUTION_OUTCOME_SCHEMA = Type2.Union([
475
+ Type2.Object({
476
+ ...QLOO_RESOLUTION_OUTCOME_COMMON_SCHEMA,
477
+ status: Type2.Literal("resolved"),
478
+ match: Type2.Union([
479
+ Type2.Literal("identifier"),
480
+ Type2.Literal("exact"),
481
+ Type2.Literal("semantic")
482
+ ]),
483
+ selected: QLOO_RESOLUTION_CANDIDATE_SCHEMA,
484
+ alternatives: Type2.Optional(Type2.Array(QLOO_RESOLUTION_CANDIDATE_SCHEMA))
485
+ }, { additionalProperties: false }),
486
+ Type2.Object({
487
+ ...QLOO_RESOLUTION_OUTCOME_COMMON_SCHEMA,
488
+ status: Type2.Literal("ambiguous"),
489
+ candidates: Type2.Array(QLOO_RESOLUTION_CANDIDATE_SCHEMA)
490
+ }, { additionalProperties: false }),
491
+ Type2.Object({
492
+ ...QLOO_RESOLUTION_OUTCOME_COMMON_SCHEMA,
493
+ status: Type2.Literal("not_found"),
494
+ candidates: Type2.Optional(Type2.Array(QLOO_RESOLUTION_CANDIDATE_SCHEMA))
495
+ }, { additionalProperties: false })
496
+ ]);
497
+
498
+ // apps/qloo-harness/dist/resolution-provider.js
499
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
500
+ var TAG_URN_PREFIX = "urn:tag:";
501
+ var DEFAULT_RESOLVER_TIMEOUT_MS = 15e3;
502
+ var DEFAULT_MAX_RESOLVER_RESPONSE_BYTES = 1 * 1024 * 1024;
503
+ var DEFAULT_RESOLVER_HEALTH_TIMEOUT_MS = 5e3;
504
+ var DEFAULT_MAX_RESOLVER_HEALTH_RESPONSE_BYTES = 64 * 1024;
505
+ var DEFAULT_MINIMUM_SEMANTIC_SCORE = 0.7;
506
+ var DEFAULT_MINIMUM_SEMANTIC_SCORE_MARGIN = 0.05;
507
+ var DEFAULT_RESOLUTION_CACHE_TTL_MS = 15 * 60 * 1e3;
508
+ var DEFAULT_RESOLUTION_CACHE_MAX_ENTRIES = 256;
509
+ var TASTE_RESOLVER_HEALTH_UPSTREAMS = [
510
+ "gemma",
511
+ "postgres",
512
+ "qloo_search",
513
+ "redis"
514
+ ];
515
+ var QLOO_RESOLUTION_PROVIDER_ENVIRONMENT_VARIABLE = "QLOO_RESOLUTION_PROVIDER";
516
+ var TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE = "TASTE_RESOLVER_URL";
517
+ var TASTE_RESOLVER_API_KEY_ENVIRONMENT_VARIABLE = "TASTE_RESOLVER_API_KEY";
518
+ var QLOO_TRUSTED_TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE = "QLOO_TRUSTED_TASTE_RESOLVER_URL";
519
+ var QLOO_TASTE_RESOLVER_TAG_ENDPOINT_ENVIRONMENT_VARIABLE = "QLOO_TASTE_RESOLVER_TAG_ENDPOINT";
520
+ var QLOO_TASTE_RESOLVER_MIN_SCORE_ENVIRONMENT_VARIABLE = "QLOO_TASTE_RESOLVER_MIN_SCORE";
521
+ var QLOO_TASTE_RESOLVER_MIN_MARGIN_ENVIRONMENT_VARIABLE = "QLOO_TASTE_RESOLVER_MIN_MARGIN";
522
+ var QlooResolutionProviderError = class extends Error {
523
+ code;
524
+ retryable;
525
+ recovery;
526
+ status;
527
+ constructor(code, message, options) {
528
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
529
+ this.name = "QlooResolutionProviderError";
530
+ this.code = code;
531
+ this.retryable = options.retryable ?? false;
532
+ this.recovery = options.recovery;
533
+ if (options.status !== void 0)
534
+ this.status = options.status;
535
+ }
536
+ };
537
+ function nonEmptyString(value) {
538
+ if (typeof value !== "string")
539
+ return void 0;
540
+ const trimmed = value.trim();
541
+ return trimmed || void 0;
542
+ }
543
+ function parseUnitInterval(value, name, defaultValue) {
544
+ if (value === void 0 || value === null || value === "")
545
+ return defaultValue;
546
+ const parsed = typeof value === "number" ? value : Number(value);
547
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
548
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", `${name} must be a number from 0 through 1.`, { recovery: `Set ${name} to a decimal from 0 through 1.` });
549
+ }
550
+ return parsed;
551
+ }
552
+ function parseTasteResolverTagEndpoint(value) {
553
+ const endpoint = nonEmptyString(value) ?? "legacy";
554
+ if (endpoint === "legacy" || endpoint === "structured")
555
+ return endpoint;
556
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", `${QLOO_TASTE_RESOLVER_TAG_ENDPOINT_ENVIRONMENT_VARIABLE} must be legacy or structured.`, {
557
+ recovery: `Use legacy for POST /v1/resolve or structured for POST /v1/tags/resolve.`
558
+ });
559
+ }
560
+ function asRecord(value) {
561
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
562
+ }
563
+ function asArray(value) {
564
+ return Array.isArray(value) ? value : [];
565
+ }
566
+ function resultArray(response, key) {
567
+ if (Array.isArray(response.results))
568
+ return response.results;
569
+ return asArray(asRecord(response.results)?.[key] ?? response.results ?? []);
570
+ }
571
+ function scalar(record, ...keys) {
572
+ for (const key of keys) {
573
+ if (record[key] !== void 0 && record[key] !== null)
574
+ return record[key];
575
+ }
576
+ return void 0;
577
+ }
578
+ function normalizeName(value) {
579
+ return value.normalize("NFKC").trim().toLocaleLowerCase("en-US");
580
+ }
581
+ function requestOptions(context) {
582
+ return {
583
+ ...context.signal ? { signal: context.signal } : {},
584
+ correlationId: context.correlationId
585
+ };
586
+ }
587
+ function entityType(record) {
588
+ const directType = scalar(record, "subtype", "type");
589
+ if (typeof directType === "string" && directType)
590
+ return directType;
591
+ const types = asArray(record.types).filter((type) => typeof type === "string" && type.length > 0);
592
+ return types.find((type) => type.startsWith("urn:entity:")) ?? types[0];
593
+ }
594
+ function entityReleaseYear(properties) {
595
+ const value = properties?.release_year;
596
+ if (typeof value === "number" && Number.isInteger(value))
597
+ return value;
598
+ if (typeof value === "string" && /^\d{4}$/u.test(value))
599
+ return Number(value);
600
+ const releaseDate = properties?.release_date;
601
+ if (typeof releaseDate !== "string")
602
+ return void 0;
603
+ const year = /^(\d{4})(?:-|$)/u.exec(releaseDate)?.[1];
604
+ return year ? Number(year) : void 0;
605
+ }
606
+ function entityCandidate(value) {
607
+ const record = asRecord(value);
608
+ if (!record)
609
+ return void 0;
610
+ const id = scalar(record, "entity_id", "id");
611
+ const name = record.name;
612
+ if (typeof id !== "string" || !id || typeof name !== "string" || !name)
613
+ return void 0;
614
+ const properties = asRecord(record.properties);
615
+ const type = entityType(record);
616
+ const description = nonEmptyString(scalar(record, "short_description", "description") ?? scalar(properties ?? {}, "short_description", "description"));
617
+ const releaseYear = entityReleaseYear(properties);
618
+ const address = record.address ?? properties?.address;
619
+ return {
620
+ id,
621
+ name,
622
+ ...type ? { type } : {},
623
+ ...typeof record.popularity === "number" ? { popularity: record.popularity } : {},
624
+ ...description ? { description: description.slice(0, 500) } : {},
625
+ ...releaseYear !== void 0 ? { release_year: releaseYear } : {},
626
+ ...address !== void 0 ? { address } : {}
627
+ };
628
+ }
629
+ function tagCandidate(value, rank) {
630
+ const record = asRecord(value);
631
+ if (!record)
632
+ return void 0;
633
+ const id = scalar(record, "id", "tag_id");
634
+ const name = record.name;
635
+ if (typeof id !== "string" || !id || typeof name !== "string" || !name)
636
+ return void 0;
637
+ const type = scalar(record, "type", "subtype");
638
+ const query = asRecord(record.query);
639
+ const score = typeof record.score === "number" ? record.score : typeof record.affinity === "number" ? record.affinity : typeof query?.affinity === "number" ? query.affinity : void 0;
640
+ const parentTypes = [...new Set(asArray(record.parents).map(asRecord).map((parent) => parent ? scalar(parent, "type") : void 0).filter((type2) => typeof type2 === "string" && type2.length > 0))].slice(0, 20);
641
+ return {
642
+ id,
643
+ name,
644
+ ...typeof type === "string" && type ? { type } : {},
645
+ ...rank !== void 0 ? { rank } : {},
646
+ ...score !== void 0 ? { score } : {},
647
+ ...typeof record.popularity === "number" ? { popularity: record.popularity } : {},
648
+ ...parentTypes.length > 0 ? { parent_types: parentTypes } : {}
649
+ };
650
+ }
651
+ function provenance(descriptor, endpoint, strategy, extras = {}) {
652
+ return {
653
+ contract_version: QLOO_RESOLUTION_CONTRACT_VERSION,
654
+ provider_id: descriptor.provider_id,
655
+ provider_name: descriptor.name,
656
+ endpoint,
657
+ strategy,
658
+ remote: descriptor.remote,
659
+ uses_model: descriptor.uses_model,
660
+ ...extras.providerVersion ? { provider_version: extras.providerVersion } : {},
661
+ ...extras.cacheStatus ? { cache_status: extras.cacheStatus } : {}
662
+ };
663
+ }
664
+ function resolved(input, selected, match, source, options = {}) {
665
+ return {
666
+ input,
667
+ status: "resolved",
668
+ match,
669
+ selected,
670
+ provenance: source,
671
+ ...options.alternatives && options.alternatives.length > 0 ? { alternatives: [...options.alternatives] } : {},
672
+ ...options.warnings && options.warnings.length > 0 ? { warnings: [...options.warnings] } : {}
673
+ };
674
+ }
675
+ function createNativeQlooResolutionProvider(client, options = {}) {
676
+ const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_RESOLUTION_CACHE_TTL_MS;
677
+ const cacheMaxEntries = options.cacheMaxEntries ?? DEFAULT_RESOLUTION_CACHE_MAX_ENTRIES;
678
+ const now = options.now ?? Date.now;
679
+ if (!Number.isSafeInteger(cacheTtlMs) || cacheTtlMs < 1) {
680
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Native Qloo resolution cache TTL must be a positive integer.", { recovery: "Configure a positive cache TTL in milliseconds." });
681
+ }
682
+ if (!Number.isSafeInteger(cacheMaxEntries) || cacheMaxEntries < 0) {
683
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Native Qloo resolution cache capacity must be a non-negative integer.", { recovery: "Configure zero to disable caching or a positive entry limit." });
684
+ }
685
+ const descriptor = {
686
+ contract_version: QLOO_RESOLUTION_CONTRACT_VERSION,
687
+ provider_id: "qloo_api",
688
+ name: "Qloo native resolution",
689
+ entity_strategy: "exact_name_or_choice",
690
+ tag_strategy: "target_scoped_semantic_search_exact_name_or_choice",
691
+ remote: true,
692
+ uses_model: false,
693
+ fallback: "none"
694
+ };
695
+ const cache = /* @__PURE__ */ new Map();
696
+ const cacheKey = (kind, input, scope) => JSON.stringify([
697
+ QLOO_RESOLUTION_CONTRACT_VERSION,
698
+ descriptor.provider_id,
699
+ descriptor.tag_strategy,
700
+ kind,
701
+ scope ?? "",
702
+ normalizeName(input)
703
+ ]);
704
+ const cachedOutcome = (kind, input, scope) => {
705
+ if (cacheMaxEntries === 0)
706
+ return void 0;
707
+ const key = cacheKey(kind, input, scope);
708
+ const entry = cache.get(key);
709
+ if (!entry)
710
+ return void 0;
711
+ if (entry.expiresAt <= now()) {
712
+ cache.delete(key);
713
+ return void 0;
714
+ }
715
+ cache.delete(key);
716
+ cache.set(key, entry);
717
+ return {
718
+ ...structuredClone(entry.outcome),
719
+ input,
720
+ provenance: {
721
+ ...entry.outcome.provenance,
722
+ cache_status: "memory_hit"
723
+ }
724
+ };
725
+ };
726
+ const rememberOutcome = (kind, input, scope, outcome) => {
727
+ if (cacheMaxEntries === 0)
728
+ return outcome;
729
+ const key = cacheKey(kind, input, scope);
730
+ cache.delete(key);
731
+ cache.set(key, {
732
+ expiresAt: now() + cacheTtlMs,
733
+ outcome: structuredClone(outcome)
734
+ });
735
+ while (cache.size > cacheMaxEntries) {
736
+ const oldest = cache.keys().next().value;
737
+ if (oldest === void 0)
738
+ break;
739
+ cache.delete(oldest);
740
+ }
741
+ return outcome;
742
+ };
743
+ const resolveEntity = async (input, type, context) => {
744
+ if (UUID_PATTERN.test(input)) {
745
+ const source2 = provenance(descriptor, "/entities", "identifier_lookup");
746
+ const response2 = await client.entities([input], requestOptions(context));
747
+ const candidates2 = resultArray(response2, "entities").map(entityCandidate).filter((candidate) => candidate !== void 0);
748
+ const exact2 = candidates2.filter(({ id }) => id.toLocaleLowerCase("en-US") === input.toLocaleLowerCase("en-US"));
749
+ if (exact2.length === 1 && exact2[0])
750
+ return resolved(input, exact2[0], "identifier", source2);
751
+ if (exact2.length > 1)
752
+ return { input, status: "ambiguous", candidates: exact2.slice(0, 5), provenance: source2 };
753
+ return { input, status: "not_found", provenance: source2 };
754
+ }
755
+ const source = provenance(descriptor, "/search", "exact_name_or_choice");
756
+ const response = await client.searchEntities({
757
+ query: input,
758
+ ...type ? { types: type } : {},
759
+ take: 5
760
+ }, requestOptions(context));
761
+ const candidates = resultArray(response, "entities").map(entityCandidate).filter((candidate) => candidate !== void 0);
762
+ if (candidates.length === 0)
763
+ return { input, status: "not_found", provenance: source };
764
+ const exact = candidates.filter(({ name }) => normalizeName(name) === normalizeName(input));
765
+ if (exact.length === 1 && exact[0]) {
766
+ const alternatives = candidates.filter(({ id }) => id !== exact[0]?.id).slice(0, 4);
767
+ return resolved(input, exact[0], "exact", source, { alternatives });
768
+ }
769
+ return {
770
+ input,
771
+ status: "ambiguous",
772
+ candidates: (exact.length > 0 ? exact : candidates).slice(0, 5),
773
+ provenance: source
774
+ };
775
+ };
776
+ const resolveTag = async (input, targetType, context) => {
777
+ if (input.startsWith(TAG_URN_PREFIX)) {
778
+ return resolved(input, { id: input, name: input }, "identifier", provenance(descriptor, "local:tag-urn", "supplied_identifier"));
779
+ }
780
+ const source = provenance(descriptor, "/v2/tags", targetType ? "semantic_search_local_parent_scope_exact_name_or_choice" : "semantic_search_exact_name_or_choice");
781
+ const response = await client.tags({
782
+ "filter.query": input,
783
+ "feature.semantic_search": true,
784
+ take: 20
785
+ }, requestOptions(context));
786
+ const rankedCandidates = resultArray(response, "tags").map((value, index) => tagCandidate(value, index + 1)).filter((candidate) => candidate !== void 0);
787
+ const candidatesWithParentMetadata = rankedCandidates.filter(({ parent_types: parentTypes }) => parentTypes !== void 0 && parentTypes.length > 0);
788
+ const candidates = targetType && candidatesWithParentMetadata.length > 0 ? rankedCandidates.filter(({ parent_types: parentTypes }) => parentTypes?.includes(targetType)) : rankedCandidates;
789
+ const scopeWarnings = targetType && candidatesWithParentMetadata.length === 0 && rankedCandidates.length > 0 ? [
790
+ `Qloo tag candidates did not include parent-type metadata, so the bounded choices could not be scoped to ${targetType}.`
791
+ ] : [];
792
+ if (targetType && candidatesWithParentMetadata.length > 0 && candidates.length === 0) {
793
+ return {
794
+ input,
795
+ status: "not_found",
796
+ provenance: source,
797
+ warnings: [
798
+ `No candidate in the first ${rankedCandidates.length} Qloo semantic tag results supports ${targetType}.`
799
+ ]
800
+ };
801
+ }
802
+ if (candidates.length === 0)
803
+ return { input, status: "not_found", provenance: source };
804
+ const exact = candidates.filter(({ name }) => normalizeName(name) === normalizeName(input));
805
+ if (exact.length === 1 && exact[0]) {
806
+ const alternatives = candidates.filter(({ id }) => id !== exact[0]?.id).slice(0, 4);
807
+ return resolved(input, exact[0], "exact", source, {
808
+ alternatives,
809
+ warnings: scopeWarnings
810
+ });
811
+ }
812
+ return {
813
+ input,
814
+ status: "ambiguous",
815
+ candidates: (exact.length > 0 ? exact : candidates).slice(0, 5),
816
+ provenance: source,
817
+ warnings: [
818
+ ...scopeWarnings,
819
+ ...exact.length === 0 ? [
820
+ "Qloo semantic search returned ranked candidates without a calibrated relevance score; choose one before the workflow continues."
821
+ ] : []
822
+ ]
823
+ };
824
+ };
825
+ return {
826
+ descriptor,
827
+ async resolveEntities(request, context) {
828
+ return {
829
+ outcomes: await Promise.all(request.inputs.map((input) => cachedOutcome("entity", input, request.type) ?? resolveEntity(input, request.type, context).then((outcome) => rememberOutcome("entity", input, request.type, outcome))))
830
+ };
831
+ },
832
+ async resolveTags(request, context) {
833
+ return {
834
+ outcomes: await Promise.all(request.inputs.map((input) => input.startsWith(TAG_URN_PREFIX) ? resolveTag(input, request.targetType, context) : cachedOutcome("tag", input, request.targetType) ?? resolveTag(input, request.targetType, context).then((outcome) => rememberOutcome("tag", input, request.targetType, outcome))))
835
+ };
836
+ }
837
+ };
838
+ }
839
+ function createPublicQlooResolutionProvider(client, options = {}) {
840
+ return createNativeQlooResolutionProvider(client, options);
841
+ }
842
+ function normalizeResolverBaseUrl(value) {
843
+ let url;
844
+ try {
845
+ url = new URL(value);
846
+ } catch (cause) {
847
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Taste-resolver URL is invalid.", {
848
+ cause,
849
+ recovery: "Set TASTE_RESOLVER_URL to an absolute HTTPS URL or a loopback HTTP URL."
850
+ });
851
+ }
852
+ const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
853
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
854
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Taste-resolver URL must use HTTPS unless it is a loopback development endpoint.", {
855
+ recovery: "Use HTTPS or run the resolver on localhost for local development."
856
+ });
857
+ }
858
+ if (url.username || url.password || url.search || url.hash) {
859
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Taste-resolver URL cannot contain credentials, query parameters, or a fragment.", {
860
+ recovery: "Move resolver authentication to TASTE_RESOLVER_API_KEY and use a clean base URL."
861
+ });
862
+ }
863
+ url.pathname = url.pathname.replace(/\/+$/u, "") + "/";
864
+ return url;
865
+ }
866
+ function resolverUrlsMatch(left, right) {
867
+ try {
868
+ return normalizeResolverBaseUrl(left).toString() === normalizeResolverBaseUrl(right).toString();
869
+ } catch {
870
+ return false;
871
+ }
872
+ }
873
+ function requireTrustedResolverBaseUrl(baseUrl, trustedBaseUrl) {
874
+ const normalized = normalizeResolverBaseUrl(baseUrl);
875
+ const isLoopback = normalized.hostname === "localhost" || normalized.hostname === "127.0.0.1" || normalized.hostname === "[::1]";
876
+ if (!isLoopback && (!trustedBaseUrl || !resolverUrlsMatch(baseUrl, trustedBaseUrl))) {
877
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Remote taste-resolver URL is not explicitly trusted.", {
878
+ recovery: `Set ${QLOO_TRUSTED_TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE} to the exact resolver base URL.`
879
+ });
880
+ }
881
+ return normalized;
882
+ }
883
+ function inspectQlooResolutionProviderConfiguration(options = {}) {
884
+ const env = options.env ?? process.env;
885
+ const selection = nonEmptyString(env[QLOO_RESOLUTION_PROVIDER_ENVIRONMENT_VARIABLE]) ?? "native";
886
+ if (selection === "native" || selection === "public") {
887
+ return {
888
+ selection,
889
+ valid: true,
890
+ providerId: "qloo_api",
891
+ endpoint: "/search and /v2/tags",
892
+ trusted: true,
893
+ authenticated: false,
894
+ usesModel: false,
895
+ message: selection === "public" ? "Native, server-free Qloo resolution is active through the legacy public alias; semantic tag candidates are target-scoped and material ambiguity requires a choice." : "Native, server-free Qloo resolution is active; semantic tag candidates are target-scoped and material ambiguity requires a choice."
896
+ };
897
+ }
898
+ if (selection !== "taste-resolver") {
899
+ return {
900
+ selection,
901
+ valid: false,
902
+ trusted: false,
903
+ authenticated: false,
904
+ usesModel: false,
905
+ message: `Unsupported Qloo resolution provider "${selection}".`,
906
+ recovery: `Set ${QLOO_RESOLUTION_PROVIDER_ENVIRONMENT_VARIABLE} to native or taste-resolver. The value public remains a compatibility alias.`
907
+ };
908
+ }
909
+ const configuredBaseUrl = nonEmptyString(env[TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE]) ?? nonEmptyString(options.fileConfig?.taste_resolver_url);
910
+ if (!configuredBaseUrl) {
911
+ return {
912
+ selection,
913
+ valid: false,
914
+ providerId: "taste_resolver_assisted",
915
+ trusted: false,
916
+ authenticated: Boolean(nonEmptyString(env[TASTE_RESOLVER_API_KEY_ENVIRONMENT_VARIABLE])),
917
+ usesModel: true,
918
+ message: "Taste-resolver assistance is selected but no resolver URL is configured.",
919
+ recovery: `Set ${TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE} and ${QLOO_TRUSTED_TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE}.`
920
+ };
921
+ }
922
+ const trustedBaseUrl = nonEmptyString(env[QLOO_TRUSTED_TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE]);
923
+ try {
924
+ const endpoint = requireTrustedResolverBaseUrl(configuredBaseUrl, trustedBaseUrl);
925
+ const tagEndpoint = parseTasteResolverTagEndpoint(env[QLOO_TASTE_RESOLVER_TAG_ENDPOINT_ENVIRONMENT_VARIABLE]);
926
+ const minimumSemanticScore = parseUnitInterval(env[QLOO_TASTE_RESOLVER_MIN_SCORE_ENVIRONMENT_VARIABLE], QLOO_TASTE_RESOLVER_MIN_SCORE_ENVIRONMENT_VARIABLE, DEFAULT_MINIMUM_SEMANTIC_SCORE);
927
+ const minimumSemanticScoreMargin = parseUnitInterval(env[QLOO_TASTE_RESOLVER_MIN_MARGIN_ENVIRONMENT_VARIABLE], QLOO_TASTE_RESOLVER_MIN_MARGIN_ENVIRONMENT_VARIABLE, DEFAULT_MINIMUM_SEMANTIC_SCORE_MARGIN);
928
+ return {
929
+ selection,
930
+ valid: true,
931
+ providerId: "taste_resolver_assisted",
932
+ endpoint: endpoint.toString().replace(/\/$/u, ""),
933
+ trusted: true,
934
+ authenticated: Boolean(nonEmptyString(env[TASTE_RESOLVER_API_KEY_ENVIRONMENT_VARIABLE])),
935
+ usesModel: true,
936
+ tagEndpoint,
937
+ minimumSemanticScore,
938
+ minimumSemanticScoreMargin,
939
+ message: `Taste-resolver semantic tag assistance is explicitly selected in ${tagEndpoint} mode with score \u2265 ${minimumSemanticScore} and margin \u2265 ${minimumSemanticScoreMargin}; entity resolution remains on the Qloo API and no fallback is enabled.`
940
+ };
941
+ } catch (error) {
942
+ const providerError = error instanceof QlooResolutionProviderError ? error : void 0;
943
+ return {
944
+ selection,
945
+ valid: false,
946
+ providerId: "taste_resolver_assisted",
947
+ trusted: false,
948
+ authenticated: Boolean(nonEmptyString(env[TASTE_RESOLVER_API_KEY_ENVIRONMENT_VARIABLE])),
949
+ usesModel: true,
950
+ message: providerError?.message ?? "Taste-resolver configuration is invalid.",
951
+ recovery: providerError?.recovery ?? "Correct the taste-resolver endpoint configuration."
952
+ };
953
+ }
954
+ }
955
+ async function readBoundedResponse(response, maximumBytes) {
956
+ const contentLength = response.headers.get("content-length");
957
+ if (contentLength !== null && Number(contentLength) > maximumBytes) {
958
+ await response.body?.cancel();
959
+ throw new QlooResolutionProviderError("RESOLVER_CONTRACT", "Taste-resolver response exceeded the configured size limit.", {
960
+ recovery: "Inspect the resolver deployment and return only bounded resolution metadata."
961
+ });
962
+ }
963
+ if (!response.body)
964
+ return {};
965
+ const reader = response.body.getReader();
966
+ const chunks = [];
967
+ let totalBytes = 0;
968
+ while (true) {
969
+ const { done, value } = await reader.read();
970
+ if (done)
971
+ break;
972
+ if (!value)
973
+ continue;
974
+ totalBytes += value.byteLength;
975
+ if (totalBytes > maximumBytes) {
976
+ await reader.cancel();
977
+ throw new QlooResolutionProviderError("RESOLVER_CONTRACT", "Taste-resolver response exceeded the configured size limit.", {
978
+ recovery: "Inspect the resolver deployment and return only bounded resolution metadata."
979
+ });
980
+ }
981
+ chunks.push(value);
982
+ }
983
+ const bytes = new Uint8Array(totalBytes);
984
+ let offset = 0;
985
+ for (const chunk of chunks) {
986
+ bytes.set(chunk, offset);
987
+ offset += chunk.byteLength;
988
+ }
989
+ const text = new TextDecoder().decode(bytes);
990
+ if (!text.trim())
991
+ return {};
992
+ try {
993
+ return JSON.parse(text);
994
+ } catch (cause) {
995
+ throw new QlooResolutionProviderError("RESOLVER_CONTRACT", "Taste-resolver returned invalid JSON.", {
996
+ cause,
997
+ recovery: "Verify that the configured endpoint implements the taste-resolver v1 contract."
998
+ });
999
+ }
1000
+ }
1001
+ function resolverHealthFailure(kind, message, startedAt, options = {}) {
1002
+ return {
1003
+ ok: false,
1004
+ kind,
1005
+ message,
1006
+ durationMs: Math.max(0, Date.now() - startedAt),
1007
+ retryable: options.retryable ?? false,
1008
+ ...options.status === void 0 ? {} : { status: options.status }
1009
+ };
1010
+ }
1011
+ async function probeTasteResolverHealth(options) {
1012
+ const startedAt = Date.now();
1013
+ let baseUrl;
1014
+ try {
1015
+ baseUrl = requireTrustedResolverBaseUrl(options.baseUrl, options.trustedBaseUrl);
1016
+ } catch {
1017
+ return resolverHealthFailure("resolver-contract", "Taste-resolver health cannot be checked until its endpoint is valid and explicitly trusted.", startedAt);
1018
+ }
1019
+ const timeoutMs = options.timeoutMs ?? DEFAULT_RESOLVER_HEALTH_TIMEOUT_MS;
1020
+ const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESOLVER_HEALTH_RESPONSE_BYTES;
1021
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || !Number.isSafeInteger(maxResponseBytes) || maxResponseBytes < 1) {
1022
+ return resolverHealthFailure("resolver-contract", "Taste-resolver health limits must be positive integers.", startedAt);
1023
+ }
1024
+ const controller = new AbortController();
1025
+ let timedOut = false;
1026
+ const timeout = setTimeout(() => {
1027
+ timedOut = true;
1028
+ controller.abort(new Error("Taste-resolver health check timed out."));
1029
+ }, timeoutMs);
1030
+ timeout.unref?.();
1031
+ const onAbort = () => controller.abort(options.signal?.reason);
1032
+ if (options.signal?.aborted)
1033
+ onAbort();
1034
+ else
1035
+ options.signal?.addEventListener("abort", onAbort, { once: true });
1036
+ const cleanup = () => {
1037
+ clearTimeout(timeout);
1038
+ options.signal?.removeEventListener("abort", onAbort);
1039
+ };
1040
+ try {
1041
+ const response = await (options.fetch ?? globalThis.fetch)(new URL("v1/health", baseUrl), {
1042
+ method: "GET",
1043
+ headers: { Accept: "application/json" },
1044
+ redirect: "error",
1045
+ signal: controller.signal
1046
+ });
1047
+ const body = await readBoundedResponse(response, maxResponseBytes);
1048
+ if (!response.ok) {
1049
+ return resolverHealthFailure("resolver-http", `Taste-resolver health returned HTTP ${response.status}.`, startedAt, { retryable: response.status === 429 || response.status >= 500, status: response.status });
1050
+ }
1051
+ const record = asRecord(body);
1052
+ const status = nonEmptyString(record?.status);
1053
+ const upstreams = asRecord(record?.upstreams);
1054
+ if (status !== "ok" && status !== "degraded" || !upstreams || TASTE_RESOLVER_HEALTH_UPSTREAMS.some((name) => !nonEmptyString(upstreams[name]))) {
1055
+ return resolverHealthFailure("resolver-contract", "Taste-resolver health response does not match the v1 contract.", startedAt);
1056
+ }
1057
+ const unavailable = TASTE_RESOLVER_HEALTH_UPSTREAMS.filter((name) => upstreams[name] !== "ok");
1058
+ if (status === "degraded" || unavailable.length > 0) {
1059
+ return resolverHealthFailure("resolver-degraded", unavailable.length > 0 ? `Taste-resolver is degraded; unavailable upstreams: ${unavailable.join(", ")}.` : "Taste-resolver is degraded.", startedAt, { retryable: true });
1060
+ }
1061
+ return {
1062
+ ok: true,
1063
+ kind: "resolver-healthy",
1064
+ message: "Taste-resolver and its declared upstreams are healthy.",
1065
+ durationMs: Math.max(0, Date.now() - startedAt),
1066
+ retryable: false
1067
+ };
1068
+ } catch (error) {
1069
+ if (timedOut) {
1070
+ return resolverHealthFailure("resolver-timeout", "Taste-resolver health check timed out.", startedAt, { retryable: true });
1071
+ }
1072
+ if (error instanceof QlooResolutionProviderError) {
1073
+ return resolverHealthFailure("resolver-contract", error.message, startedAt);
1074
+ }
1075
+ return resolverHealthFailure("resolver-connect", options.signal?.aborted ? "Taste-resolver health check was cancelled." : "Taste-resolver health endpoint is not reachable.", startedAt, { retryable: !options.signal?.aborted });
1076
+ } finally {
1077
+ cleanup();
1078
+ }
1079
+ }
1080
+ function createTasteResolverAssistedResolutionProvider(options) {
1081
+ const baseUrl = requireTrustedResolverBaseUrl(options.baseUrl, options.trustedBaseUrl);
1082
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
1083
+ const timeoutMs = options.timeoutMs ?? DEFAULT_RESOLVER_TIMEOUT_MS;
1084
+ const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESOLVER_RESPONSE_BYTES;
1085
+ const tagEndpoint = parseTasteResolverTagEndpoint(options.tagEndpoint);
1086
+ const minimumSemanticScore = parseUnitInterval(options.minimumSemanticScore, "minimumSemanticScore", DEFAULT_MINIMUM_SEMANTIC_SCORE);
1087
+ const minimumSemanticScoreMargin = parseUnitInterval(options.minimumSemanticScoreMargin, "minimumSemanticScoreMargin", DEFAULT_MINIMUM_SEMANTIC_SCORE_MARGIN);
1088
+ const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_RESOLUTION_CACHE_TTL_MS;
1089
+ const cacheMaxEntries = options.cacheMaxEntries ?? DEFAULT_RESOLUTION_CACHE_MAX_ENTRIES;
1090
+ const now = options.now ?? Date.now;
1091
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
1092
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Taste-resolver timeout must be a positive integer.", { recovery: "Configure a positive resolver timeout in milliseconds." });
1093
+ }
1094
+ if (!Number.isSafeInteger(maxResponseBytes) || maxResponseBytes < 1) {
1095
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Taste-resolver response limit must be a positive integer.", { recovery: "Configure a positive resolver response-size limit." });
1096
+ }
1097
+ if (!Number.isSafeInteger(cacheTtlMs) || cacheTtlMs < 1) {
1098
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Taste-resolver cache TTL must be a positive integer.", { recovery: "Configure a positive cache TTL in milliseconds." });
1099
+ }
1100
+ if (!Number.isSafeInteger(cacheMaxEntries) || cacheMaxEntries < 0) {
1101
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Taste-resolver cache capacity must be a non-negative integer.", { recovery: "Configure zero to disable caching or a positive entry limit." });
1102
+ }
1103
+ const descriptor = {
1104
+ contract_version: QLOO_RESOLUTION_CONTRACT_VERSION,
1105
+ provider_id: "taste_resolver_assisted",
1106
+ name: "Qloo taste-resolver assistance",
1107
+ entity_strategy: "qloo_api_exact_name_or_choice",
1108
+ tag_strategy: tagEndpoint === "structured" ? "taste_resolver_structured_tag_rerank_confidence_gated" : "taste_resolver_semantic_rerank_confidence_gated",
1109
+ remote: true,
1110
+ uses_model: true,
1111
+ fallback: "none"
1112
+ };
1113
+ const cache = /* @__PURE__ */ new Map();
1114
+ const cacheKey = (input, targetType) => JSON.stringify([
1115
+ QLOO_RESOLUTION_CONTRACT_VERSION,
1116
+ descriptor.provider_id,
1117
+ tagEndpoint,
1118
+ targetType ?? "",
1119
+ normalizeName(input)
1120
+ ]);
1121
+ const cachedOutcome = (input, targetType) => {
1122
+ if (cacheMaxEntries === 0)
1123
+ return void 0;
1124
+ const key = cacheKey(input, targetType);
1125
+ const entry = cache.get(key);
1126
+ if (!entry)
1127
+ return void 0;
1128
+ if (entry.expiresAt <= now()) {
1129
+ cache.delete(key);
1130
+ return void 0;
1131
+ }
1132
+ cache.delete(key);
1133
+ cache.set(key, entry);
1134
+ return {
1135
+ ...structuredClone(entry.outcome),
1136
+ input,
1137
+ provenance: {
1138
+ ...entry.outcome.provenance,
1139
+ cache_status: "memory_hit"
1140
+ }
1141
+ };
1142
+ };
1143
+ const rememberOutcome = (input, targetType, outcome) => {
1144
+ if (cacheMaxEntries === 0)
1145
+ return outcome;
1146
+ const key = cacheKey(input, targetType);
1147
+ cache.delete(key);
1148
+ cache.set(key, { expiresAt: now() + cacheTtlMs, outcome: structuredClone(outcome) });
1149
+ while (cache.size > cacheMaxEntries) {
1150
+ const oldest = cache.keys().next().value;
1151
+ if (oldest === void 0)
1152
+ break;
1153
+ cache.delete(oldest);
1154
+ }
1155
+ return outcome;
1156
+ };
1157
+ const resolveSemanticTag = async (input, targetType, context) => {
1158
+ if (input.length > 1e3) {
1159
+ throw new QlooResolutionProviderError("RESOLVER_CONTRACT", "Taste-resolver inputs cannot exceed 1000 characters.", { recovery: "Shorten the tag concept and retry the same structured workflow." });
1160
+ }
1161
+ const controller = new AbortController();
1162
+ let timedOut = false;
1163
+ const timeout = setTimeout(() => {
1164
+ timedOut = true;
1165
+ controller.abort(new Error("Taste-resolver request timed out."));
1166
+ }, timeoutMs);
1167
+ timeout.unref?.();
1168
+ const onAbort = () => controller.abort(context.signal?.reason);
1169
+ if (context.signal?.aborted)
1170
+ onAbort();
1171
+ else
1172
+ context.signal?.addEventListener("abort", onAbort, { once: true });
1173
+ const cleanup = () => {
1174
+ clearTimeout(timeout);
1175
+ context.signal?.removeEventListener("abort", onAbort);
1176
+ };
1177
+ let response;
1178
+ try {
1179
+ response = await fetchImplementation(new URL(tagEndpoint === "structured" ? "v1/tags/resolve" : "v1/resolve", baseUrl), {
1180
+ method: "POST",
1181
+ headers: {
1182
+ Accept: "application/json",
1183
+ "Content-Type": "application/json",
1184
+ "X-Correlation-Id": context.correlationId,
1185
+ ...options.apiKey ? { "X-Api-Key": options.apiKey } : {}
1186
+ },
1187
+ body: JSON.stringify(tagEndpoint === "structured" ? {
1188
+ inputs: [input],
1189
+ ...targetType ? { filter_type: targetType } : {},
1190
+ take: 5
1191
+ } : {
1192
+ input,
1193
+ ...targetType ? { filter_type: targetType } : {},
1194
+ options: { take: 5, explain: true }
1195
+ }),
1196
+ redirect: "error",
1197
+ signal: controller.signal
1198
+ });
1199
+ } catch (cause) {
1200
+ cleanup();
1201
+ if (context.signal?.aborted) {
1202
+ throw new QlooResolutionProviderError("RESOLVER_ABORTED", "Taste-resolver request was cancelled.", {
1203
+ cause,
1204
+ recovery: "Retry only if the workflow was cancelled unintentionally."
1205
+ });
1206
+ }
1207
+ if (timedOut) {
1208
+ throw new QlooResolutionProviderError("RESOLVER_TIMEOUT", "Taste-resolver request timed out.", {
1209
+ cause,
1210
+ retryable: true,
1211
+ recovery: "Check resolver health and retry the same interpreted request once."
1212
+ });
1213
+ }
1214
+ throw new QlooResolutionProviderError("RESOLVER_CONNECT", "Taste-resolver is not reachable.", {
1215
+ cause,
1216
+ retryable: true,
1217
+ recovery: "Verify TASTE_RESOLVER_URL, VPN routing, and resolver health before retrying."
1218
+ });
1219
+ }
1220
+ let body;
1221
+ try {
1222
+ body = await readBoundedResponse(response, maxResponseBytes);
1223
+ } catch (cause) {
1224
+ if (cause instanceof QlooResolutionProviderError)
1225
+ throw cause;
1226
+ if (context.signal?.aborted) {
1227
+ throw new QlooResolutionProviderError("RESOLVER_ABORTED", "Taste-resolver request was cancelled.", {
1228
+ cause,
1229
+ recovery: "Retry only if the workflow was cancelled unintentionally."
1230
+ });
1231
+ }
1232
+ if (timedOut) {
1233
+ throw new QlooResolutionProviderError("RESOLVER_TIMEOUT", "Taste-resolver response timed out.", {
1234
+ cause,
1235
+ retryable: true,
1236
+ recovery: "Check resolver health and retry the same interpreted request once."
1237
+ });
1238
+ }
1239
+ throw new QlooResolutionProviderError("RESOLVER_CONNECT", "Taste-resolver response could not be read completely.", {
1240
+ cause,
1241
+ retryable: true,
1242
+ recovery: "Check resolver health and retry the same interpreted request once."
1243
+ });
1244
+ } finally {
1245
+ cleanup();
1246
+ }
1247
+ if (!response.ok) {
1248
+ const status = response.status;
1249
+ const code = status === 401 || status === 403 ? "RESOLVER_AUTH" : status >= 500 || status === 429 ? "RESOLVER_UPSTREAM" : "RESOLVER_CONTRACT";
1250
+ throw new QlooResolutionProviderError(code, `Taste-resolver returned HTTP ${status}.`, {
1251
+ status,
1252
+ retryable: status >= 500 || status === 429,
1253
+ recovery: status === 401 || status === 403 ? "Configure a valid TASTE_RESOLVER_API_KEY before retrying." : status >= 500 || status === 429 ? "Check resolver health and retry the same interpreted request once." : `Verify that the hosted resolver implements the ${tagEndpoint === "structured" ? "v1 tags/resolve" : "v1 resolve"} contract.`
1254
+ });
1255
+ }
1256
+ const record = asRecord(body);
1257
+ const resolution = asRecord(record?.resolution);
1258
+ const groups = asArray(tagEndpoint === "structured" ? record?.tags : resolution?.tags).map(asRecord).filter((group) => group !== void 0);
1259
+ const matchingGroup = groups.find((group) => {
1260
+ const groupInput = group.input;
1261
+ return typeof groupInput === "string" && normalizeName(groupInput) === normalizeName(input);
1262
+ }) ?? groups[0];
1263
+ const candidates = asArray(matchingGroup?.tags).map((value) => tagCandidate(value)).filter((candidate) => candidate !== void 0).sort((left, right) => (right.score ?? Number.NEGATIVE_INFINITY) - (left.score ?? Number.NEGATIVE_INFINITY)).slice(0, 5);
1264
+ const cacheStatus = nonEmptyString(record?.cache_status);
1265
+ const providerVersion = nonEmptyString(record?.contract_version ?? record?.version);
1266
+ const source = provenance(descriptor, tagEndpoint === "structured" ? "/v1/tags/resolve" : "/v1/resolve", tagEndpoint === "structured" ? "structured_semantic_embedding_taxonomy_rerank" : "semantic_embedding_taxonomy_rerank", {
1267
+ ...cacheStatus ? { cacheStatus } : {},
1268
+ ...providerVersion ? { providerVersion } : {}
1269
+ });
1270
+ const responseWarnings = asArray(record?.warnings).filter((warning) => typeof warning === "string").slice(0, 10);
1271
+ if (candidates.length === 0) {
1272
+ return {
1273
+ input,
1274
+ status: "not_found",
1275
+ provenance: source,
1276
+ ...responseWarnings.length > 0 ? { warnings: responseWarnings } : {}
1277
+ };
1278
+ }
1279
+ const exact = candidates.filter(({ name }) => normalizeName(name) === normalizeName(input));
1280
+ if (exact.length > 1) {
1281
+ return {
1282
+ input,
1283
+ status: "ambiguous",
1284
+ candidates: exact,
1285
+ provenance: source,
1286
+ ...responseWarnings.length > 0 ? { warnings: responseWarnings } : {}
1287
+ };
1288
+ }
1289
+ const selected = exact[0] ?? candidates[0];
1290
+ if (!selected) {
1291
+ return { input, status: "not_found", provenance: source };
1292
+ }
1293
+ const alternatives = candidates.filter(({ id }) => id !== selected.id).slice(0, 4);
1294
+ if (exact.length === 0) {
1295
+ const runnerUp = alternatives[0];
1296
+ const score = selected.score;
1297
+ const margin = score !== void 0 && runnerUp?.score !== void 0 ? score - runnerUp.score : runnerUp === void 0 ? Number.POSITIVE_INFINITY : void 0;
1298
+ const confidenceWarnings = [
1299
+ ...score === void 0 || score < 0 || score > 1 ? ["The top semantic candidate did not provide a valid normalized score."] : score < minimumSemanticScore ? [`The top semantic score ${score.toFixed(3)} is below the required ${minimumSemanticScore.toFixed(3)}.`] : [],
1300
+ ...margin === void 0 ? ["The resolver did not provide enough score information to distinguish the top candidates."] : margin < minimumSemanticScoreMargin ? [`The top semantic candidates are separated by ${margin.toFixed(3)}, below the required ${minimumSemanticScoreMargin.toFixed(3)}.`] : []
1301
+ ];
1302
+ if (confidenceWarnings.length > 0) {
1303
+ return {
1304
+ input,
1305
+ status: "ambiguous",
1306
+ candidates,
1307
+ provenance: source,
1308
+ warnings: [...responseWarnings, ...confidenceWarnings]
1309
+ };
1310
+ }
1311
+ }
1312
+ const semanticWarning = exact.length === 0 ? `Taste-resolver selected "${selected.name}" as the top semantic match for "${input}"; alternatives remain available in resolution provenance.` : void 0;
1313
+ const warnings = [...responseWarnings, ...semanticWarning ? [semanticWarning] : []];
1314
+ return resolved(input, selected, exact.length === 1 ? "exact" : "semantic", source, {
1315
+ alternatives,
1316
+ warnings
1317
+ });
1318
+ };
1319
+ return {
1320
+ descriptor,
1321
+ resolveEntities: (request, context) => options.entityProvider.resolveEntities(request, context),
1322
+ async resolveTags(request, context) {
1323
+ const outcomes = await Promise.all(request.inputs.map((input) => input.startsWith(TAG_URN_PREFIX) ? options.entityProvider.resolveTags({ ...request, inputs: [input] }, context).then((batch) => batch.outcomes[0] ?? {
1324
+ input,
1325
+ status: "not_found",
1326
+ provenance: provenance(descriptor, "local:tag-urn", "supplied_identifier")
1327
+ }) : cachedOutcome(input, request.targetType) ?? resolveSemanticTag(input, request.targetType, context).then((outcome) => rememberOutcome(input, request.targetType, outcome))));
1328
+ return { outcomes };
1329
+ }
1330
+ };
1331
+ }
1332
+ function createQlooResolutionProviderFromEnvironment(options) {
1333
+ const env = options.env ?? process.env;
1334
+ const selection = nonEmptyString(env[QLOO_RESOLUTION_PROVIDER_ENVIRONMENT_VARIABLE]) ?? "native";
1335
+ const nativeProvider = createNativeQlooResolutionProvider(options.client);
1336
+ if (selection === "native" || selection === "public")
1337
+ return nativeProvider;
1338
+ if (selection !== "taste-resolver") {
1339
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", `Unsupported Qloo resolution provider "${selection}".`, {
1340
+ recovery: `Set ${QLOO_RESOLUTION_PROVIDER_ENVIRONMENT_VARIABLE} to native or taste-resolver. The value public remains a compatibility alias.`
1341
+ });
1342
+ }
1343
+ const baseUrl = nonEmptyString(env[TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE]) ?? nonEmptyString(options.fileConfig?.taste_resolver_url);
1344
+ if (!baseUrl) {
1345
+ throw new QlooResolutionProviderError("RESOLVER_CONFIGURATION", "Taste-resolver assistance was selected but no resolver URL is configured.", {
1346
+ recovery: `Set ${TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE} and ${QLOO_TRUSTED_TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE}, or select the native provider.`
1347
+ });
1348
+ }
1349
+ const trustedBaseUrl = nonEmptyString(env[QLOO_TRUSTED_TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE]);
1350
+ const apiKey = nonEmptyString(env[TASTE_RESOLVER_API_KEY_ENVIRONMENT_VARIABLE]);
1351
+ const tagEndpoint = parseTasteResolverTagEndpoint(env[QLOO_TASTE_RESOLVER_TAG_ENDPOINT_ENVIRONMENT_VARIABLE]);
1352
+ const minimumSemanticScore = parseUnitInterval(env[QLOO_TASTE_RESOLVER_MIN_SCORE_ENVIRONMENT_VARIABLE], QLOO_TASTE_RESOLVER_MIN_SCORE_ENVIRONMENT_VARIABLE, DEFAULT_MINIMUM_SEMANTIC_SCORE);
1353
+ const minimumSemanticScoreMargin = parseUnitInterval(env[QLOO_TASTE_RESOLVER_MIN_MARGIN_ENVIRONMENT_VARIABLE], QLOO_TASTE_RESOLVER_MIN_MARGIN_ENVIRONMENT_VARIABLE, DEFAULT_MINIMUM_SEMANTIC_SCORE_MARGIN);
1354
+ return createTasteResolverAssistedResolutionProvider({
1355
+ baseUrl,
1356
+ ...trustedBaseUrl ? { trustedBaseUrl } : {},
1357
+ ...apiKey ? { apiKey } : {},
1358
+ tagEndpoint,
1359
+ minimumSemanticScore,
1360
+ minimumSemanticScoreMargin,
1361
+ entityProvider: nativeProvider,
1362
+ ...options.fetch ? { fetch: options.fetch } : {}
1363
+ });
1364
+ }
1365
+ export {
1366
+ QLOO_RESOLUTION_PROVIDER_ENVIRONMENT_VARIABLE,
1367
+ QLOO_TASTE_RESOLVER_MIN_MARGIN_ENVIRONMENT_VARIABLE,
1368
+ QLOO_TASTE_RESOLVER_MIN_SCORE_ENVIRONMENT_VARIABLE,
1369
+ QLOO_TASTE_RESOLVER_TAG_ENDPOINT_ENVIRONMENT_VARIABLE,
1370
+ QLOO_TRUSTED_TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE,
1371
+ QlooResolutionProviderError,
1372
+ TASTE_RESOLVER_API_KEY_ENVIRONMENT_VARIABLE,
1373
+ TASTE_RESOLVER_URL_ENVIRONMENT_VARIABLE,
1374
+ createNativeQlooResolutionProvider,
1375
+ createPublicQlooResolutionProvider,
1376
+ createQlooResolutionProviderFromEnvironment,
1377
+ createTasteResolverAssistedResolutionProvider,
1378
+ inspectQlooResolutionProviderConfiguration,
1379
+ probeTasteResolverHealth
1380
+ };