@farming-labs/docs 0.2.75 → 0.2.78

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 (39) hide show
  1. package/dist/{agent-BHqaZx-X.mjs → agent-Ba94vwM2.mjs} +4308 -340
  2. package/dist/{agent-cAPjC2jb.d.mts → agent-CHgWJcK_.d.mts} +60 -2
  3. package/dist/{agent-DxTa3PLB.mjs → agent-CM-hzhfh.mjs} +2 -2
  4. package/dist/{agent-evals-21LZ_uEl.mjs → agent-evals-C8dkgJMN.mjs} +1 -1
  5. package/dist/{agent-export-3VheegEK.mjs → agent-export-DdF-IVfg.mjs} +8 -7
  6. package/dist/agent-provenance-D8UBpnMc.mjs +463 -0
  7. package/dist/agent-skills-bundle.d.mts +1 -1
  8. package/dist/{agent-skills-server-noFn1l_p.mjs → agent-skills-server-B2EXn2QD.mjs} +1 -1
  9. package/dist/{agent-skills-server-Cix64aGB.d.mts → agent-skills-server-zvs0Yknt.d.mts} +2 -2
  10. package/dist/agent-skills-vite.d.mts +3 -3
  11. package/dist/agent-skills-vite.mjs +2 -2
  12. package/dist/{agents-ByTdP0Hl.mjs → agents-v0Em2KZo.mjs} +7 -16
  13. package/dist/cli/index.mjs +15 -15
  14. package/dist/client/react.d.mts +1 -1
  15. package/dist/{cloud-ask-ai-Bt0rzFbe.d.mts → cloud-ask-ai-BwsdF7AQ.d.mts} +1 -1
  16. package/dist/{dev-Cc1cFFj-.mjs → dev-kjaxOOla.mjs} +8 -2
  17. package/dist/docs-cloud-server.d.mts +2 -2
  18. package/dist/{doctor-C34VVF85.mjs → doctor-BbrN0rdC.mjs} +56 -24
  19. package/dist/{golden-evaluations-D_oDJwGv.mjs → golden-evaluations-Bk9t-HNq.mjs} +12 -3
  20. package/dist/index.d.mts +5 -5
  21. package/dist/index.mjs +6 -6
  22. package/dist/{mcp-Bnqfu8P5.mjs → mcp-DT-WNsA-.mjs} +6 -6
  23. package/dist/mcp.d.mts +28 -6
  24. package/dist/mcp.mjs +557 -62
  25. package/dist/{prompt-references-CfyYOa_W.mjs → prompt-references-CoDorsvS.mjs} +3 -3
  26. package/dist/{retrieval-digest-CFOcSKTo.d.mts → retrieval-digest-CO3wqeMk.d.mts} +105 -6
  27. package/dist/{review-Dw6oCQX3.mjs → review-DrCaaT7G.mjs} +5 -5
  28. package/dist/{robots-DaYBoy_A.mjs → robots-C_v2HK43.mjs} +2 -2
  29. package/dist/{robots-KWLujSLI.mjs → robots-izJbM9cv.mjs} +3 -3
  30. package/dist/{search-DBdmbUkc.mjs → search-YiZMZDfb.mjs} +6 -6
  31. package/dist/server.d.mts +9 -7
  32. package/dist/server.mjs +7 -7
  33. package/dist/{sitemap-CrzG5Wpf.mjs → sitemap-NO8UMO6Y.mjs} +6 -6
  34. package/dist/{sitemap-server-B9LZQ1FE.mjs → sitemap-server-Dgxs9V6S.mjs} +12 -3
  35. package/dist/{standards-discovery-BwL5ffGx.mjs → standards-discovery-BKlEnK_H.mjs} +32 -4
  36. package/dist/{standards-discovery-DBI1PD7m.d.mts → standards-discovery-BWyNgZSs.d.mts} +34 -7
  37. package/dist/{types-BDRwrExu.d.mts → types-CpLnwHak.d.mts} +190 -2
  38. package/package.json +1 -1
  39. package/dist/search-BmC8wVr0.mjs +0 -3490
@@ -1,3490 +0,0 @@
1
- import { G as isDocsAgentDiscoveryRequest, K as isDocsAgentsRequest, Q as isDocsSkillRequest, _t as resolveDocsMarkdownRequest, dt as resolveDocsAgentsFormat, jt as resolveDocsAudienceMdxContent, lt as resolveDocsAgentFeedbackConfig, pt as resolveDocsLlmsTxtRequest, ut as resolveDocsAgentFeedbackRequest, xt as resolveDocsSkillFormat } from "./agent-BHqaZx-X.mjs";
2
- import { S as upsertPageAgentContractMarkdown, a as findDocsGeneratedAgentContractRanges, d as PAGE_AGENT_CONTRACT_END_MARKER, l as stripDocsGeneratedAgentContractMarkers, m as PAGE_AGENT_CONTRACT_START_MARKER, o as findDocsMarkdownSection, s as parseDocsMarkdownSections, y as renderPageAgentContractMarkdown } from "./markdown-sections-7OoA7ylx.mjs";
3
-
4
- //#region src/telemetry.ts
5
- const DOCS_PACKAGE_NAME = "@farming-labs/docs";
6
- const DOCS_PACKAGE_VERSION = "0.2.25";
7
- const DEFAULT_DOCS_TELEMETRY_ENDPOINT = "https://docs.farming-labs.dev/api/telemetry/events";
8
- const PROJECT_TELEMETRY_CACHE_TTL_MS = 1440 * 60 * 1e3;
9
- const PROJECT_TELEMETRY_CACHE_MAX_KEYS = 256;
10
- function getRuntimeEnv() {
11
- if (typeof process === "undefined" || !process.env) return;
12
- return process.env;
13
- }
14
- function readRuntimeEnv(name) {
15
- const value = getRuntimeEnv()?.[name]?.trim();
16
- return value ? value : void 0;
17
- }
18
- function isTruthyEnv(value) {
19
- return /^(1|true|yes|on)$/i.test(value ?? "");
20
- }
21
- function isFalsyEnv(value) {
22
- return /^(0|false|no|off)$/i.test(value ?? "");
23
- }
24
- function isBrowserRuntime() {
25
- return typeof window !== "undefined" && typeof document !== "undefined";
26
- }
27
- function isProductionTelemetryRuntime() {
28
- if (isBrowserRuntime()) return false;
29
- const env = getRuntimeEnv();
30
- if (!env) return true;
31
- if (env.NODE_ENV === "test") return false;
32
- if (env.VERCEL_ENV === "production") return true;
33
- if (env.CONTEXT === "production" && isTruthyEnv(env.NETLIFY)) return true;
34
- if (isTruthyEnv(env.CF_PAGES)) return true;
35
- if (env.RENDER_SERVICE_ID || env.FLY_APP_NAME || env.RAILWAY_ENVIRONMENT) return true;
36
- return env.NODE_ENV === "production";
37
- }
38
- function resolveDocsTelemetryConfig(telemetry) {
39
- const envToggle = readRuntimeEnv("DOCS_TELEMETRY");
40
- const envDisabled = readRuntimeEnv("DOCS_TELEMETRY_DISABLED");
41
- if (isFalsyEnv(envToggle) || isTruthyEnv(envDisabled) || telemetry === false) return { enabled: false };
42
- const objectConfig = telemetry && typeof telemetry === "object" ? telemetry : void 0;
43
- if (objectConfig?.enabled === false) return { enabled: false };
44
- return {
45
- enabled: telemetry === true || objectConfig?.enabled === true || isTruthyEnv(envToggle) || isProductionTelemetryRuntime(),
46
- endpoint: objectConfig?.endpoint?.trim() || readRuntimeEnv("DOCS_TELEMETRY_ENDPOINT") || DEFAULT_DOCS_TELEMETRY_ENDPOINT
47
- };
48
- }
49
- function readRequestOrigin(request) {
50
- if (!request?.url) return void 0;
51
- try {
52
- return new URL(request.url).origin;
53
- } catch {
54
- return;
55
- }
56
- }
57
- /** Normalize an HTTP(S) telemetry site value to its origin. */
58
- function normalizeDocsTelemetryOrigin(candidate) {
59
- if (typeof candidate !== "string") return void 0;
60
- const trimmed = candidate.trim();
61
- if (!trimmed) return void 0;
62
- try {
63
- const hasScheme = /^[a-z][a-z\d+.-]*:(?!\d)/i.test(trimmed);
64
- const url = new URL(hasScheme ? trimmed : `https://${trimmed}`);
65
- if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
66
- return url.origin;
67
- } catch {
68
- return;
69
- }
70
- }
71
- /** Return whether a telemetry origin points at a local development server. */
72
- function isLocalDocsTelemetryOrigin(candidate) {
73
- const origin = normalizeDocsTelemetryOrigin(candidate);
74
- if (!origin) return false;
75
- const hostname = new URL(origin).hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
76
- if ([
77
- "localhost",
78
- "localhost.localdomain",
79
- "localhost6",
80
- "localhost6.localdomain6",
81
- "ip6-localhost",
82
- "ip6-loopback"
83
- ].includes(hostname) || hostname.endsWith(".localhost")) return true;
84
- if (hostname === "::" || hostname === "::1") return true;
85
- const ipv4 = hostname.split(".").map((part) => Number(part));
86
- if (ipv4.length === 4 && ipv4.every((part) => Number.isInteger(part) && part >= 0 && part <= 255)) return ipv4[0] === 127 || ipv4.every((part) => part === 0);
87
- return /^::ffff:7f[0-9a-f]{2}:/.test(hostname) || hostname === "::ffff:0:0";
88
- }
89
- function isBlockedDocsTelemetryOrigin(candidate) {
90
- if (candidate === void 0) return false;
91
- return !normalizeDocsTelemetryOrigin(candidate) || isLocalDocsTelemetryOrigin(candidate);
92
- }
93
- function readDeploymentOrigin() {
94
- const candidates = [
95
- readRuntimeEnv("DOCS_SITE_URL"),
96
- readRuntimeEnv("NEXT_PUBLIC_BASE_URL"),
97
- readRuntimeEnv("NEXT_PUBLIC_SITE_URL"),
98
- readRuntimeEnv("SITE_URL"),
99
- readRuntimeEnv("URL"),
100
- readRuntimeEnv("CF_PAGES_URL"),
101
- readRuntimeEnv("VERCEL_PROJECT_PRODUCTION_URL"),
102
- readRuntimeEnv("VERCEL_URL"),
103
- readRuntimeEnv("DEPLOY_PRIME_URL")
104
- ];
105
- for (const candidate of candidates) {
106
- const origin = normalizeDocsTelemetryOrigin(candidate);
107
- if (origin) return origin;
108
- }
109
- }
110
- function detectDeployment() {
111
- const env = getRuntimeEnv();
112
- if (!env) return void 0;
113
- if (env.VERCEL || env.VERCEL_ENV) return {
114
- provider: "vercel",
115
- environment: env.VERCEL_ENV,
116
- id: env.VERCEL_DEPLOYMENT_ID ?? env.VERCEL_GIT_COMMIT_SHA,
117
- region: env.VERCEL_REGION
118
- };
119
- if (env.NETLIFY) return {
120
- provider: "netlify",
121
- environment: env.CONTEXT,
122
- id: env.DEPLOY_ID ?? env.COMMIT_REF
123
- };
124
- if (env.CF_PAGES) return {
125
- provider: "cloudflare-pages",
126
- environment: env.CF_PAGES_BRANCH,
127
- id: env.CF_PAGES_COMMIT_SHA
128
- };
129
- if (env.RENDER_SERVICE_ID) return {
130
- provider: "render",
131
- environment: env.RENDER_ENV,
132
- id: env.RENDER_SERVICE_ID
133
- };
134
- if (env.FLY_APP_NAME) return {
135
- provider: "fly",
136
- environment: env.FLY_APP_NAME,
137
- id: env.FLY_ALLOC_ID,
138
- region: env.FLY_REGION
139
- };
140
- if (env.RAILWAY_ENVIRONMENT) return {
141
- provider: "railway",
142
- environment: env.RAILWAY_ENVIRONMENT,
143
- id: env.RAILWAY_DEPLOYMENT_ID
144
- };
145
- return env.NODE_ENV === "production" ? { environment: "production" } : void 0;
146
- }
147
- function detectRuntime() {
148
- if (typeof process !== "undefined" && process.versions?.node) return {
149
- name: "node",
150
- version: process.versions.node
151
- };
152
- if (typeof navigator !== "undefined" && navigator.userAgent) return { name: "web-standard" };
153
- }
154
- function isObjectConfigEnabled(value, defaultEnabled) {
155
- if (value === false) return false;
156
- if (value === true) return true;
157
- if (value && typeof value === "object" && !Array.isArray(value)) {
158
- const enabled = value.enabled;
159
- return enabled === false ? false : defaultEnabled || enabled === true;
160
- }
161
- return defaultEnabled;
162
- }
163
- function hasObjectConfig(value) {
164
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
165
- }
166
- function getDocsTelemetryFeatures(config) {
167
- const pageActions = config.pageActions;
168
- const feedback = config.feedback;
169
- return {
170
- search: isObjectConfigEnabled(config.search, true),
171
- ai: isObjectConfigEnabled(config.ai, false),
172
- mcp: isObjectConfigEnabled(config.mcp, true),
173
- llmsTxt: isObjectConfigEnabled(config.llmsTxt, true),
174
- pageActions: hasObjectConfig(pageActions),
175
- feedback: feedback === true || hasObjectConfig(feedback) && feedback.enabled !== false,
176
- agentFeedback: feedback !== false && !(hasObjectConfig(feedback) && feedback.agent === false) && !(hasObjectConfig(feedback) && hasObjectConfig(feedback.agent) && feedback.agent.enabled === false),
177
- sitemap: isObjectConfigEnabled(config.sitemap, true),
178
- robots: isObjectConfigEnabled(config.robots, true),
179
- apiReference: isObjectConfigEnabled(config.apiReference, false),
180
- staticExport: config.staticExport === true,
181
- changelog: isObjectConfigEnabled(config.changelog, false),
182
- cloud: typeof config.cloud !== "undefined" && isObjectConfigEnabled(config.cloud, true),
183
- review: isObjectConfigEnabled(config.review, true),
184
- codeBlocksValidate: hasObjectConfig(config.codeBlocks) && Boolean(config.codeBlocks.validate)
185
- };
186
- }
187
- function createDocsTelemetryEvent(config, input, context = {}) {
188
- const telemetryConfig = config.telemetry && typeof config.telemetry === "object" ? config.telemetry : void 0;
189
- const siteOriginCandidates = [
190
- context.siteOrigin,
191
- input.site?.origin,
192
- telemetryConfig?.siteOrigin,
193
- readRequestOrigin(context.request),
194
- readDeploymentOrigin()
195
- ];
196
- if (siteOriginCandidates.some(isBlockedDocsTelemetryOrigin)) return void 0;
197
- const siteOrigin = siteOriginCandidates.map(normalizeDocsTelemetryOrigin).find((origin) => Boolean(origin));
198
- const deployment = input.deployment ?? detectDeployment();
199
- const properties = context.properties || input.properties ? {
200
- ...input.properties,
201
- ...context.properties
202
- } : void 0;
203
- return {
204
- ...input,
205
- timestamp: input.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
206
- package: {
207
- name: DOCS_PACKAGE_NAME,
208
- version: DOCS_PACKAGE_VERSION,
209
- ...input.package
210
- },
211
- framework: input.framework ?? context.framework,
212
- runtime: input.runtime ?? detectRuntime(),
213
- site: siteOrigin ? { origin: siteOrigin } : input.site,
214
- deployment,
215
- features: input.features ?? getDocsTelemetryFeatures(config),
216
- properties
217
- };
218
- }
219
- function projectEventKey(event) {
220
- return [
221
- event.package.name,
222
- event.package.version,
223
- event.framework ?? "",
224
- event.site?.origin ?? "",
225
- event.deployment?.provider ?? "",
226
- event.deployment?.environment ?? "",
227
- event.deployment?.id ?? ""
228
- ].join("|");
229
- }
230
- function getSentProjectKeys() {
231
- const globalValue = globalThis;
232
- globalValue.__farmingLabsDocsTelemetryProjectKeys__ ??= /* @__PURE__ */ new Map();
233
- return globalValue.__farmingLabsDocsTelemetryProjectKeys__;
234
- }
235
- function pruneSentProjectKeys(sent, now) {
236
- for (const [key, expiresAt] of sent) if (expiresAt <= now) sent.delete(key);
237
- while (sent.size >= PROJECT_TELEMETRY_CACHE_MAX_KEYS) {
238
- const oldestKey = sent.keys().next().value;
239
- if (!oldestKey) return;
240
- sent.delete(oldestKey);
241
- }
242
- }
243
- async function emitDocsTelemetryEvent(telemetry, event) {
244
- const resolved = resolveDocsTelemetryConfig(telemetry);
245
- const eventSiteOrigin = event.site?.origin;
246
- const normalizedSiteOrigin = normalizeDocsTelemetryOrigin(eventSiteOrigin);
247
- if (!resolved.enabled || !resolved.endpoint || typeof fetch !== "function" || eventSiteOrigin !== void 0 && (!normalizedSiteOrigin || isLocalDocsTelemetryOrigin(eventSiteOrigin))) return;
248
- const eventToSend = normalizedSiteOrigin ? {
249
- ...event,
250
- site: { origin: normalizedSiteOrigin }
251
- } : event;
252
- try {
253
- const ingestKey = readRuntimeEnv("DOCS_TELEMETRY_INGEST_KEY");
254
- const headers = { "content-type": "application/json" };
255
- if (ingestKey) headers["x-docs-telemetry-key"] = ingestKey;
256
- await fetch(resolved.endpoint, {
257
- method: "POST",
258
- headers,
259
- body: JSON.stringify({ event: eventToSend }),
260
- keepalive: true
261
- });
262
- } catch {}
263
- }
264
- function emitDocsTelemetryProjectEvent(config, context = {}) {
265
- const event = createDocsTelemetryEvent(config, { type: "project_detected" }, context);
266
- if (!event) return;
267
- const key = projectEventKey(event);
268
- const sent = getSentProjectKeys();
269
- const now = Date.now();
270
- const expiresAt = sent.get(key);
271
- if (expiresAt && expiresAt > now) return;
272
- if (typeof expiresAt === "number") sent.delete(key);
273
- pruneSentProjectKeys(sent, now);
274
- sent.set(key, now + PROJECT_TELEMETRY_CACHE_TTL_MS);
275
- emitDocsTelemetryEvent(config.telemetry, event);
276
- }
277
- function emitDocsTelemetryAgentSurfaceEvent(config, context) {
278
- const event = createDocsTelemetryEvent(config, {
279
- type: context.surface === "mcp" ? "mcp_request" : "agent_surface_used",
280
- properties: { surface: context.surface }
281
- }, context);
282
- if (!event) return;
283
- emitDocsTelemetryEvent(config.telemetry, event);
284
- }
285
- function emitDocsTelemetryMcpToolEvent(config, context) {
286
- const event = createDocsTelemetryEvent(config, {
287
- type: "mcp_tool_used",
288
- properties: {
289
- tool: context.tool,
290
- locale: context.locale,
291
- resultCount: context.resultCount
292
- }
293
- }, context);
294
- if (!event) return;
295
- emitDocsTelemetryEvent(config.telemetry, event);
296
- }
297
- function inferDocsTelemetryAgentSurface(request, options) {
298
- const url = new URL(request.url);
299
- const method = request.method.toUpperCase();
300
- const feedbackRequest = resolveDocsAgentFeedbackRequest(url, resolveDocsAgentFeedbackConfig(options.feedback));
301
- if ((method === "GET" || method === "HEAD") && isDocsAgentDiscoveryRequest(url)) return "agent_spec";
302
- if ((method === "GET" || method === "HEAD") && feedbackRequest?.kind === "schema") return "agent_feedback_schema";
303
- if (method === "POST" && feedbackRequest?.kind === "submit") return "agent_feedback_submit";
304
- if (method === "GET" || method === "HEAD") {
305
- if (isDocsAgentsRequest(url) || resolveDocsAgentsFormat(url) === "agents") return "agents";
306
- if (isDocsSkillRequest(url) || resolveDocsSkillFormat(url) === "skill") return "skill";
307
- if (resolveDocsMarkdownRequest(options.entry, url, request)) return "markdown";
308
- if (resolveDocsLlmsTxtRequest(url, options.llmsTxt, options.entry)) return "llms";
309
- }
310
- if (method === "POST") return "ask_ai";
311
- }
312
-
313
- //#endregion
314
- //#region src/sidebar.ts
315
- function resolvePageSidebarFolderIndexBehavior(sidebar) {
316
- if (!sidebar || typeof sidebar !== "object") return void 0;
317
- const value = sidebar.folderIndexBehavior;
318
- return value === "link" || value === "toggle" || value === "hidden" ? value : void 0;
319
- }
320
- function normalizeSidebarFolderBehaviorPath(path) {
321
- if (!path) return void 0;
322
- let value = path.trim();
323
- if (!value) return void 0;
324
- if (/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value)) try {
325
- value = new URL(value).pathname;
326
- } catch {
327
- return;
328
- }
329
- else value = value.split("#", 1)[0]?.split("?", 1)[0] ?? value;
330
- if (!value.startsWith("/")) value = `/${value}`;
331
- return value.replace(/\/$/, "") || "/";
332
- }
333
- function resolveSidebarFolderIndexBehavior(sidebar, defaultBehavior = "link") {
334
- if (sidebar === void 0 || sidebar === true || sidebar === false) return defaultBehavior;
335
- if (sidebar.folderIndexBehavior === "toggle") return "toggle";
336
- if (sidebar.folderIndexBehavior === "hidden") return "hidden";
337
- if (sidebar.folderIndexBehavior === "link") return "link";
338
- return defaultBehavior;
339
- }
340
- function resolveSidebarFolderIndexBehaviorForPath(sidebar, folderPath, defaultBehavior = "link") {
341
- const fallback = resolveSidebarFolderIndexBehavior(sidebar, defaultBehavior);
342
- if (!sidebar || typeof sidebar !== "object") return fallback;
343
- const normalizedPath = normalizeSidebarFolderBehaviorPath(folderPath);
344
- if (!normalizedPath) return fallback;
345
- for (const [rawPath, override] of Object.entries(sidebar.folderIndexBehaviorOverrides ?? {})) if (normalizeSidebarFolderBehaviorPath(rawPath) === normalizedPath) return override === "link" || override === "toggle" || override === "hidden" ? override : fallback;
346
- return fallback;
347
- }
348
- function applySidebarFolderIndexBehavior(tree, behaviorOrOptions) {
349
- const resolveBehavior = typeof behaviorOrOptions === "string" ? () => behaviorOrOptions : (folderPath) => resolveSidebarFolderIndexBehaviorForPath(behaviorOrOptions.sidebar, folderPath, behaviorOrOptions.defaultBehavior);
350
- function mapNode(node) {
351
- if (!node || typeof node !== "object") return node;
352
- const candidate = node;
353
- if (candidate.type !== "folder" || !Array.isArray(candidate.children)) return node;
354
- const children = candidate.children.map(mapNode);
355
- const index = candidate.index ? mapNode(candidate.index) : void 0;
356
- const folderPath = (typeof candidate.url === "string" ? candidate.url : void 0) || (candidate.index && typeof candidate.index === "object" && "url" in candidate.index && typeof candidate.index.url === "string" ? candidate.index.url ?? void 0 : void 0);
357
- const behavior = (candidate.folderIndexBehavior === "link" || candidate.folderIndexBehavior === "toggle" || candidate.folderIndexBehavior === "hidden" ? candidate.folderIndexBehavior : void 0) ?? resolveBehavior(folderPath);
358
- if (behavior === "link") return {
359
- ...candidate,
360
- folderIndexBehavior: void 0,
361
- index,
362
- children
363
- };
364
- if (behavior === "hidden") return {
365
- ...candidate,
366
- folderIndexBehavior: void 0,
367
- index: void 0,
368
- url: void 0,
369
- children
370
- };
371
- return {
372
- ...candidate,
373
- folderIndexBehavior: void 0,
374
- index: void 0,
375
- url: void 0,
376
- children: index ? [index, ...children] : children
377
- };
378
- }
379
- return {
380
- ...tree,
381
- children: tree.children.map(mapNode)
382
- };
383
- }
384
-
385
- //#endregion
386
- //#region src/agent-provenance.ts
387
- const GENERATED_AGENT_PROVENANCE_MARKER = "@farming-labs/docs:generated";
388
- const GENERATED_AGENT_PROVENANCE_VERSION = 1;
389
- function normalizeLineEndings(value) {
390
- return value.replace(/\r\n?/g, "\n").replace(/^\uFEFF/, "");
391
- }
392
- function normalizeGeneratedAgentContent(value) {
393
- return normalizeLineEndings(value).trimEnd();
394
- }
395
- function hashGeneratedAgentContent(value) {
396
- const normalized = normalizeGeneratedAgentContent(value);
397
- const bytes = new TextEncoder().encode(normalized);
398
- let hash = 14695981039346656037n;
399
- for (const byte of bytes) {
400
- hash ^= BigInt(byte);
401
- hash = BigInt.asUintN(64, hash * 1099511628211n);
402
- }
403
- return `fnv1a64:${hash.toString(16).padStart(16, "0")}`;
404
- }
405
- function parseProvenanceBlock(rawBlock) {
406
- const entries = /* @__PURE__ */ new Map();
407
- for (const line of rawBlock.split("\n")) {
408
- const trimmed = line.trim();
409
- if (!trimmed) continue;
410
- const separatorIndex = trimmed.indexOf("=");
411
- if (separatorIndex <= 0) continue;
412
- const key = trimmed.slice(0, separatorIndex).trim();
413
- const value = trimmed.slice(separatorIndex + 1).trim();
414
- if (!key || !value) continue;
415
- entries.set(key, value);
416
- }
417
- const version = Number.parseInt(entries.get("version") ?? "", 10);
418
- const sourceKind = entries.get("sourceKind");
419
- const sourceHash = entries.get("sourceHash");
420
- const settingsHash = entries.get("settingsHash");
421
- const outputHash = entries.get("outputHash");
422
- const generatedAt = entries.get("generatedAt");
423
- if (!Number.isFinite(version) || sourceKind !== "resolved-page" && sourceKind !== "agent-md" || !sourceHash || !settingsHash || !outputHash || !generatedAt) return;
424
- return {
425
- version,
426
- sourceKind,
427
- sourceHash,
428
- settingsHash,
429
- outputHash,
430
- generatedAt
431
- };
432
- }
433
- function parseGeneratedAgentDocument(raw) {
434
- const normalized = normalizeLineEndings(raw);
435
- const headerPattern = new RegExp(`^<!-- ${GENERATED_AGENT_PROVENANCE_MARKER}\\n([\\s\\S]*?)\\n-->\\n?`);
436
- const match = normalized.match(headerPattern);
437
- if (!match) return { content: normalized };
438
- return {
439
- provenance: parseProvenanceBlock(match[1]),
440
- content: normalized.slice(match[0].length)
441
- };
442
- }
443
- function stripGeneratedAgentProvenance(raw) {
444
- return parseGeneratedAgentDocument(raw).content;
445
- }
446
- function serializeGeneratedAgentDocument(content, provenance) {
447
- const normalizedContent = normalizeGeneratedAgentContent(content);
448
- return `${[
449
- `<!-- ${GENERATED_AGENT_PROVENANCE_MARKER}`,
450
- `version=${provenance.version}`,
451
- `sourceKind=${provenance.sourceKind}`,
452
- `sourceHash=${provenance.sourceHash}`,
453
- `settingsHash=${provenance.settingsHash}`,
454
- `outputHash=${provenance.outputHash}`,
455
- `generatedAt=${provenance.generatedAt}`,
456
- "-->",
457
- normalizedContent
458
- ].join("\n")}\n`;
459
- }
460
-
461
- //#endregion
462
- //#region src/retrieval-digest.ts
463
- const SHA256_INITIAL_STATE = [
464
- 1779033703,
465
- 3144134277,
466
- 1013904242,
467
- 2773480762,
468
- 1359893119,
469
- 2600822924,
470
- 528734635,
471
- 1541459225
472
- ];
473
- const SHA256_ROUND_CONSTANTS = [
474
- 1116352408,
475
- 1899447441,
476
- 3049323471,
477
- 3921009573,
478
- 961987163,
479
- 1508970993,
480
- 2453635748,
481
- 2870763221,
482
- 3624381080,
483
- 310598401,
484
- 607225278,
485
- 1426881987,
486
- 1925078388,
487
- 2162078206,
488
- 2614888103,
489
- 3248222580,
490
- 3835390401,
491
- 4022224774,
492
- 264347078,
493
- 604807628,
494
- 770255983,
495
- 1249150122,
496
- 1555081692,
497
- 1996064986,
498
- 2554220882,
499
- 2821834349,
500
- 2952996808,
501
- 3210313671,
502
- 3336571891,
503
- 3584528711,
504
- 113926993,
505
- 338241895,
506
- 666307205,
507
- 773529912,
508
- 1294757372,
509
- 1396182291,
510
- 1695183700,
511
- 1986661051,
512
- 2177026350,
513
- 2456956037,
514
- 2730485921,
515
- 2820302411,
516
- 3259730800,
517
- 3345764771,
518
- 3516065817,
519
- 3600352804,
520
- 4094571909,
521
- 275423344,
522
- 430227734,
523
- 506948616,
524
- 659060556,
525
- 883997877,
526
- 958139571,
527
- 1322822218,
528
- 1537002063,
529
- 1747873779,
530
- 1955562222,
531
- 2024104815,
532
- 2227730452,
533
- 2361852424,
534
- 2428436474,
535
- 2756734187,
536
- 3204031479,
537
- 3329325298
538
- ];
539
- function rotateRight(value, bits) {
540
- return value >>> bits | value << 32 - bits;
541
- }
542
- function normalizeDigestContent(value) {
543
- return value.replace(/\r\n?/gu, "\n").replace(/^\uFEFF/u, "").trimEnd();
544
- }
545
- /** Validate the bounded HTTP(S) URL or root-relative URI reference used by provenance. */
546
- function isDocsRetrievalCanonicalUrl(value) {
547
- const hasUnsafeCharacter = value.includes("\\") || Array.from(value).some((character) => {
548
- const codePoint = character.codePointAt(0) ?? 0;
549
- return codePoint <= 31 || codePoint === 127;
550
- });
551
- if (!value || value !== value.trim() || value.length > 4096 || hasUnsafeCharacter) return false;
552
- if (/^\/(?!\/)/u.test(value)) try {
553
- new URL(value, "https://docs.local");
554
- return true;
555
- } catch {
556
- return false;
557
- }
558
- try {
559
- const url = new URL(value);
560
- return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
561
- } catch {
562
- return false;
563
- }
564
- }
565
- /**
566
- * Hash a retrieval source projection with portable SHA-256.
567
- *
568
- * The projection is normalized by removing one leading BOM, converting CRLF/CR
569
- * line endings to LF, and trimming trailing whitespace. The helper is exported so
570
- * agents can independently verify provenance in Node, edge, and browser runtimes.
571
- */
572
- function digestDocsRetrievalContent(value) {
573
- const input = new TextEncoder().encode(normalizeDigestContent(value));
574
- const paddedLength = Math.ceil((input.length + 9) / 64) * 64;
575
- const padded = new Uint8Array(paddedLength);
576
- padded.set(input);
577
- padded[input.length] = 128;
578
- const bitLength = input.length * 8;
579
- const dataView = new DataView(padded.buffer);
580
- dataView.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296), false);
581
- dataView.setUint32(paddedLength - 4, bitLength >>> 0, false);
582
- const state = Uint32Array.from(SHA256_INITIAL_STATE);
583
- const words = new Uint32Array(64);
584
- for (let offset = 0; offset < padded.length; offset += 64) {
585
- for (let index = 0; index < 16; index += 1) words[index] = dataView.getUint32(offset + index * 4, false);
586
- for (let index = 16; index < 64; index += 1) {
587
- const previous15 = words[index - 15];
588
- const previous2 = words[index - 2];
589
- const sigma0 = rotateRight(previous15, 7) ^ rotateRight(previous15, 18) ^ previous15 >>> 3;
590
- const sigma1 = rotateRight(previous2, 17) ^ rotateRight(previous2, 19) ^ previous2 >>> 10;
591
- words[index] = words[index - 16] + sigma0 + words[index - 7] + sigma1 >>> 0;
592
- }
593
- let a = state[0];
594
- let b = state[1];
595
- let c = state[2];
596
- let d = state[3];
597
- let e = state[4];
598
- let f = state[5];
599
- let g = state[6];
600
- let h = state[7];
601
- for (let index = 0; index < 64; index += 1) {
602
- const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
603
- const choice = e & f ^ ~e & g;
604
- const temporary1 = h + sum1 + choice + SHA256_ROUND_CONSTANTS[index] + words[index] >>> 0;
605
- const temporary2 = (rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
606
- h = g;
607
- g = f;
608
- f = e;
609
- e = d + temporary1 >>> 0;
610
- d = c;
611
- c = b;
612
- b = a;
613
- a = temporary1 + temporary2 >>> 0;
614
- }
615
- state[0] = state[0] + a >>> 0;
616
- state[1] = state[1] + b >>> 0;
617
- state[2] = state[2] + c >>> 0;
618
- state[3] = state[3] + d >>> 0;
619
- state[4] = state[4] + e >>> 0;
620
- state[5] = state[5] + f >>> 0;
621
- state[6] = state[6] + g >>> 0;
622
- state[7] = state[7] + h >>> 0;
623
- }
624
- return `sha256:${Array.from(state, (word) => word.toString(16).padStart(8, "0")).join("")}`;
625
- }
626
-
627
- //#endregion
628
- //#region src/agent-scope.ts
629
- /** Normalize framework aliases used by page metadata, agent contracts, MCP, and evaluations. */
630
- function normalizeAgentFramework(value) {
631
- const normalized = value.toLowerCase().replace(/[^a-z0-9]/gu, "");
632
- if ([
633
- "next",
634
- "nextjs",
635
- "nextjsapp",
636
- "reactnext"
637
- ].includes(normalized)) return "nextjs";
638
- if ([
639
- "tanstack",
640
- "tanstackstart",
641
- "start"
642
- ].includes(normalized)) return "tanstackstart";
643
- if (["svelte", "sveltekit"].includes(normalized)) return "sveltekit";
644
- if (["nuxt", "nuxtjs"].includes(normalized)) return "nuxt";
645
- return normalized;
646
- }
647
- function normalizeAgentLocale(value) {
648
- return value.trim().toLowerCase().replace(/_/gu, "-");
649
- }
650
- function normalizeAgentVersion(value) {
651
- return value.trim().toLowerCase().replace(/^v(?=\d)/u, "");
652
- }
653
- function normalizeAgentScopeValues(value) {
654
- const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
655
- return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean)));
656
- }
657
- function compareVersions(left, right) {
658
- for (let index = 0; index < 3; index += 1) if (left[index] !== right[index]) return left[index] > right[index] ? 1 : -1;
659
- return 0;
660
- }
661
- function parseExactVersion(value) {
662
- const match = normalizeAgentVersion(value).match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/iu);
663
- if (!match) return void 0;
664
- return [
665
- Number(match[1]),
666
- Number(match[2] ?? 0),
667
- Number(match[3] ?? 0)
668
- ];
669
- }
670
- function parseVersionOperand(value) {
671
- const normalized = normalizeAgentVersion(value);
672
- if (normalized === "*" || normalized === "x") return {
673
- version: [
674
- 0,
675
- 0,
676
- 0
677
- ],
678
- components: 0,
679
- wildcard: true
680
- };
681
- const match = normalized.match(/^(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?$/iu);
682
- if (!match) return void 0;
683
- const raw = [
684
- match[1],
685
- match[2],
686
- match[3]
687
- ];
688
- const wildcardIndex = raw.findIndex((part, index) => index > 0 && (part?.toLowerCase() === "x" || part === "*"));
689
- if (wildcardIndex >= 0 && raw.slice(wildcardIndex + 1).some((part) => part && part.toLowerCase() !== "x" && part !== "*")) return;
690
- const components = wildcardIndex >= 0 ? wildcardIndex : raw.filter(Boolean).length;
691
- return {
692
- version: [
693
- Number(raw[0]),
694
- Number(raw[1] ?? 0) || 0,
695
- Number(raw[2] ?? 0) || 0
696
- ],
697
- components,
698
- wildcard: wildcardIndex >= 0
699
- };
700
- }
701
- function nextVersionBoundary(operand) {
702
- const [major, minor] = operand.version;
703
- if (operand.components <= 0) return void 0;
704
- if (operand.components === 1) return [
705
- major + 1,
706
- 0,
707
- 0
708
- ];
709
- return [
710
- major,
711
- minor + 1,
712
- 0
713
- ];
714
- }
715
- function operandRange(operand) {
716
- if (operand.components === 0) return {
717
- minimumInclusive: true,
718
- maximumInclusive: false
719
- };
720
- if (operand.components < 3 || operand.wildcard) return {
721
- minimum: operand.version,
722
- minimumInclusive: true,
723
- maximum: nextVersionBoundary(operand),
724
- maximumInclusive: false
725
- };
726
- return {
727
- minimum: operand.version,
728
- minimumInclusive: true,
729
- maximum: operand.version,
730
- maximumInclusive: true
731
- };
732
- }
733
- function caretRange(operand) {
734
- const [major, minor, patch] = operand.version;
735
- let maximum;
736
- if (major > 0) maximum = [
737
- major + 1,
738
- 0,
739
- 0
740
- ];
741
- else if (operand.components <= 1) maximum = [
742
- 1,
743
- 0,
744
- 0
745
- ];
746
- else if (minor > 0 || operand.components === 2) maximum = [
747
- 0,
748
- minor + 1,
749
- 0
750
- ];
751
- else maximum = [
752
- 0,
753
- 0,
754
- patch + 1
755
- ];
756
- return {
757
- minimum: operand.version,
758
- minimumInclusive: true,
759
- maximum,
760
- maximumInclusive: false
761
- };
762
- }
763
- function tildeRange(operand) {
764
- const [major, minor] = operand.version;
765
- const maximum = operand.components <= 1 ? [
766
- major + 1,
767
- 0,
768
- 0
769
- ] : [
770
- major,
771
- minor + 1,
772
- 0
773
- ];
774
- return {
775
- minimum: operand.version,
776
- minimumInclusive: true,
777
- maximum,
778
- maximumInclusive: false
779
- };
780
- }
781
- function intersectRanges(left, right) {
782
- let minimum = left.minimum;
783
- let minimumInclusive = left.minimumInclusive;
784
- if (!minimum || right.minimum && compareVersions(right.minimum, minimum) > 0) {
785
- minimum = right.minimum;
786
- minimumInclusive = right.minimumInclusive;
787
- } else if (right.minimum && compareVersions(right.minimum, minimum) === 0) minimumInclusive = minimumInclusive && right.minimumInclusive;
788
- let maximum = left.maximum;
789
- let maximumInclusive = left.maximumInclusive;
790
- if (!maximum || right.maximum && compareVersions(right.maximum, maximum) < 0) {
791
- maximum = right.maximum;
792
- maximumInclusive = right.maximumInclusive;
793
- } else if (right.maximum && compareVersions(right.maximum, maximum) === 0) maximumInclusive = maximumInclusive && right.maximumInclusive;
794
- if (minimum && maximum) {
795
- const compared = compareVersions(minimum, maximum);
796
- if (compared > 0 || compared === 0 && !(minimumInclusive && maximumInclusive)) return void 0;
797
- }
798
- return {
799
- minimum,
800
- minimumInclusive,
801
- maximum,
802
- maximumInclusive
803
- };
804
- }
805
- function comparatorRange(operator, operand) {
806
- if (!operator || operator === "=") return operandRange(operand);
807
- if (operator === ">=") return {
808
- minimum: operand.version,
809
- minimumInclusive: true,
810
- maximumInclusive: false
811
- };
812
- if (operator === ">") {
813
- const partialBoundary = operand.components < 3 ? nextVersionBoundary(operand) : void 0;
814
- return {
815
- minimum: partialBoundary ?? operand.version,
816
- minimumInclusive: Boolean(partialBoundary),
817
- maximumInclusive: false
818
- };
819
- }
820
- if (operator === "<") return {
821
- minimumInclusive: true,
822
- maximum: operand.version,
823
- maximumInclusive: false
824
- };
825
- const partialBoundary = operand.components < 3 ? nextVersionBoundary(operand) : void 0;
826
- return {
827
- minimumInclusive: true,
828
- maximum: partialBoundary ?? operand.version,
829
- maximumInclusive: !partialBoundary
830
- };
831
- }
832
- function parseVersionRangeBranch(value) {
833
- const branch = normalizeAgentVersion(value).trim();
834
- if (!branch) return void 0;
835
- const hyphen = branch.match(/^(.+?)\s+-\s+(.+)$/u);
836
- if (hyphen) {
837
- const minimumOperand = parseVersionOperand(hyphen[1]);
838
- const maximumOperand = parseVersionOperand(hyphen[2]);
839
- if (!minimumOperand || !maximumOperand) return void 0;
840
- const maximumBoundary = maximumOperand.components < 3 ? nextVersionBoundary(maximumOperand) : maximumOperand.version;
841
- return intersectRanges({
842
- minimum: minimumOperand.version,
843
- minimumInclusive: true,
844
- maximumInclusive: false
845
- }, {
846
- minimumInclusive: true,
847
- maximum: maximumBoundary,
848
- maximumInclusive: maximumOperand.components === 3
849
- });
850
- }
851
- const special = branch.match(/^(\^|~)\s*(.+)$/u);
852
- if (special) {
853
- const operand = parseVersionOperand(special[2]);
854
- if (!operand || operand.components === 0) return void 0;
855
- return special[1] === "^" ? caretRange(operand) : tildeRange(operand);
856
- }
857
- const comparators = Array.from(branch.matchAll(/(>=|<=|>|<|=)\s*([^\s]+)/gu));
858
- if (comparators.length > 0) {
859
- if (comparators.map((match) => match[0]).join(" ").replace(/\s+/gu, " ") !== branch.replace(/\s+/gu, " ")) return void 0;
860
- let range = {
861
- minimumInclusive: true,
862
- maximumInclusive: false
863
- };
864
- for (const comparator of comparators) {
865
- const operand = parseVersionOperand(comparator[2]);
866
- if (!operand) return void 0;
867
- const next = intersectRanges(range, comparatorRange(comparator[1], operand));
868
- if (!next) return void 0;
869
- range = next;
870
- }
871
- return range;
872
- }
873
- const operand = parseVersionOperand(branch);
874
- return operand ? operandRange(operand) : void 0;
875
- }
876
- function parseVersionRanges(value) {
877
- return normalizeAgentVersion(value).split("||").map((branch) => parseVersionRangeBranch(branch)).filter((range) => Boolean(range));
878
- }
879
- function parseVersionConstraintBranches(value) {
880
- const ranges = parseVersionRanges(value);
881
- if (ranges.length > 0) return ranges.map((range) => ({
882
- kind: "range",
883
- range
884
- }));
885
- const normalized = normalizeAgentVersion(value);
886
- return normalized ? [{
887
- kind: "opaque",
888
- value: normalized
889
- }] : [];
890
- }
891
- function intersectVersionConstraintBranches(left, right) {
892
- if (left.kind === "opaque" || right.kind === "opaque") return left.kind === "opaque" && right.kind === "opaque" && left.value === right.value ? left : void 0;
893
- const range = intersectRanges(left.range, right.range);
894
- return range ? {
895
- kind: "range",
896
- range
897
- } : void 0;
898
- }
899
- /**
900
- * Return true when choosing one alternative from every group can select one shared version.
901
- *
902
- * Each inner group is an OR list while the groups themselves are intersected. Computing the
903
- * accumulated range prevents pairwise matches from accepting three mutually incompatible scopes.
904
- */
905
- function agentVersionConstraintGroupsOverlap(groups) {
906
- if (groups.length === 0) return false;
907
- let intersections = groups[0].flatMap(parseVersionConstraintBranches);
908
- if (intersections.length === 0) return false;
909
- for (const group of groups.slice(1)) {
910
- const branches = group.flatMap(parseVersionConstraintBranches);
911
- if (branches.length === 0) return false;
912
- intersections = intersections.flatMap((intersection) => branches.map((branch) => intersectVersionConstraintBranches(intersection, branch)).filter((value) => Boolean(value)));
913
- if (intersections.length === 0) return false;
914
- }
915
- return true;
916
- }
917
- function rangeContains(range, version) {
918
- if (range.minimum) {
919
- const compared = compareVersions(version, range.minimum);
920
- if (compared < 0 || compared === 0 && !range.minimumInclusive) return false;
921
- }
922
- if (range.maximum) {
923
- const compared = compareVersions(version, range.maximum);
924
- if (compared > 0 || compared === 0 && !range.maximumInclusive) return false;
925
- }
926
- return true;
927
- }
928
- function rangesOverlap(left, right) {
929
- return Boolean(intersectRanges(left, right));
930
- }
931
- /** Match an exact requested version against an exact or range-like documented constraint. */
932
- function agentVersionConstraintMatches(requested, constraint) {
933
- if (normalizeAgentVersion(requested) === normalizeAgentVersion(constraint)) return Boolean(normalizeAgentVersion(requested));
934
- const wanted = parseExactVersion(requested);
935
- if (!wanted) return false;
936
- return parseVersionRanges(constraint).some((range) => rangeContains(range, wanted));
937
- }
938
- /** Return true when two exact or range-like documented version constraints can select one version. */
939
- function agentVersionConstraintsOverlap(left, right) {
940
- const normalizedLeft = normalizeAgentVersion(left);
941
- if (normalizedLeft === normalizeAgentVersion(right)) return Boolean(normalizedLeft);
942
- const leftRanges = parseVersionRanges(left);
943
- const rightRanges = parseVersionRanges(right);
944
- return leftRanges.some((leftRange) => rightRanges.some((rightRange) => rangesOverlap(leftRange, rightRange)));
945
- }
946
-
947
- //#endregion
948
- //#region src/search.ts
949
- const DEFAULT_SEARCH_LIMIT = 10;
950
- const MAX_SEARCH_SNIPPET_CHARS = 160;
951
- const DEFAULT_MCP_PROTOCOL_VERSION = "2025-11-25";
952
- const MCP_SESSION_CLEANUP_TIMEOUT_MS = 1e3;
953
- const syncedIndexes = /* @__PURE__ */ new Map();
954
- const syncingIndexes = /* @__PURE__ */ new Map();
955
- const ALGOLIA_MAX_RECORD_BYTES = 9500;
956
- const DEFAULT_ASK_AI_CONTEXT_CHARS = 24e3;
957
- const DEFAULT_ASK_AI_RESULT_CHARS = 6e3;
958
- const MAX_SEARCH_FILTER_VALUES = 16;
959
- const MAX_SEARCH_FILTER_VALUE_CHARS = 128;
960
- const MAX_SEARCH_FILTER_RAW_CHARS = 4096;
961
- const MAX_SEARCH_FILTER_SEGMENTS = 64;
962
- const MAX_PROVIDER_SCOPE_FILTER_IDS = 1e3;
963
- const MAX_PROVIDER_SCOPE_FILTER_CHARS = 16e3;
964
- const ALGOLIA_BATCH_OPERATIONS = 1e3;
965
- const MAX_SEARCH_WARNINGS = 16;
966
- const MAX_SEARCH_WARNING_VALUES = 16;
967
- const MAX_SEARCH_WARNING_PAGE_URLS = 8;
968
- const RETRIEVAL_INDEX_FORMAT = "docs-retrieval-index.v1";
969
- const MAX_RETRIEVAL_SOURCE_URL_CHARS = 4096;
970
- const MAX_RETRIEVAL_SOURCE_VALUE_CHARS = 256;
971
- const SEARCH_FILTER_FIELDS = [
972
- "framework",
973
- "version",
974
- "package",
975
- "tags"
976
- ];
977
- const SEARCH_AMBIGUITY_FIELDS = [
978
- "framework",
979
- "version",
980
- "package"
981
- ];
982
- const SEARCH_STOP_WORDS = new Set([
983
- "a",
984
- "an",
985
- "and",
986
- "are",
987
- "as",
988
- "at",
989
- "be",
990
- "can",
991
- "do",
992
- "does",
993
- "for",
994
- "from",
995
- "how",
996
- "i",
997
- "in",
998
- "is",
999
- "it",
1000
- "of",
1001
- "on",
1002
- "or",
1003
- "the",
1004
- "this",
1005
- "to",
1006
- "use",
1007
- "what",
1008
- "when",
1009
- "where",
1010
- "which",
1011
- "with"
1012
- ]);
1013
- function normalizeDocsSearchFilterValue(field, value) {
1014
- const bounded = value.trim().slice(0, MAX_SEARCH_FILTER_VALUE_CHARS).trim();
1015
- if (!bounded) return "";
1016
- if (field === "framework") return normalizeAgentFramework(bounded);
1017
- if (field === "version") return normalizeAgentVersion(bounded);
1018
- return bounded.toLowerCase();
1019
- }
1020
- function normalizeDocsSearchFilterValues(field, input) {
1021
- const values = typeof input === "string" ? [input] : input ?? [];
1022
- const normalized = [];
1023
- const seen = /* @__PURE__ */ new Set();
1024
- let remainingChars = MAX_SEARCH_FILTER_RAW_CHARS;
1025
- let remainingSegments = MAX_SEARCH_FILTER_SEGMENTS;
1026
- for (const item of values) {
1027
- if (remainingChars <= 0 || remainingSegments <= 0) break;
1028
- const boundedItem = item.slice(0, remainingChars);
1029
- remainingChars -= boundedItem.length;
1030
- for (const part of boundedItem.split(",")) {
1031
- if (remainingSegments <= 0) break;
1032
- remainingSegments -= 1;
1033
- const value = normalizeDocsSearchFilterValue(field, part);
1034
- if (!value || seen.has(value)) continue;
1035
- seen.add(value);
1036
- normalized.push(value);
1037
- if (normalized.length >= MAX_SEARCH_FILTER_VALUES) return normalized;
1038
- }
1039
- }
1040
- return normalized;
1041
- }
1042
- /** Normalize scalar or array-valued scope filters for programmatic, HTTP, and MCP callers. */
1043
- function normalizeDocsSearchFilters(input = {}) {
1044
- const filters = {};
1045
- for (const field of SEARCH_FILTER_FIELDS) {
1046
- const values = normalizeDocsSearchFilterValues(field, input[field]);
1047
- if (values.length > 0) filters[field] = values;
1048
- }
1049
- return filters;
1050
- }
1051
- /** Parse the public search scope parameters from a URL query string. */
1052
- function resolveDocsSearchFilters(searchParams) {
1053
- return normalizeDocsSearchFilters({
1054
- framework: searchParams.getAll("framework"),
1055
- version: searchParams.getAll("version"),
1056
- package: searchParams.getAll("package"),
1057
- tags: searchParams.getAll("tags")
1058
- });
1059
- }
1060
- /** Resolve search filters and the backwards-compatible structured response opt-in. */
1061
- function resolveDocsSearchRequest(searchParams) {
1062
- return {
1063
- filters: resolveDocsSearchFilters(searchParams),
1064
- structured: searchParams.get("response") === "structured"
1065
- };
1066
- }
1067
- function hasDocsSearchFilters(filters) {
1068
- return SEARCH_FILTER_FIELDS.some((field) => (filters[field]?.length ?? 0) > 0);
1069
- }
1070
- function resolveProviderScopeDocumentIds(query, context, corpusId) {
1071
- if (!hasDocsSearchFilters(query.filters ?? {})) return void 0;
1072
- const documents = corpusId ? buildDocsSearchDocuments(context.pages, context.chunking ?? { strategy: "section" }, "human") : context.documents;
1073
- const ids = Array.from(new Set(documents.map((document) => corpusId ? makeHostedProviderDocumentId(corpusId, document.id) : document.id)));
1074
- return ids.length <= MAX_PROVIDER_SCOPE_FILTER_IDS ? ids : [];
1075
- }
1076
- function docsSearchFiltersMatch(expected, actual) {
1077
- return SEARCH_FILTER_FIELDS.every((field) => {
1078
- const expectedValues = expected[field] ?? [];
1079
- const actualValues = actual[field] ?? [];
1080
- return expectedValues.length === actualValues.length && expectedValues.every((value) => actualValues.includes(value));
1081
- });
1082
- }
1083
- function parseVerifiedMcpSearchFilters(value) {
1084
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1085
- const record = value;
1086
- const filterFields = SEARCH_FILTER_FIELDS;
1087
- if (Object.keys(record).some((field) => !filterFields.includes(field))) return;
1088
- const input = {};
1089
- for (const field of SEARCH_FILTER_FIELDS) {
1090
- const values = record[field];
1091
- if (values === void 0) continue;
1092
- if (!Array.isArray(values) || values.length > MAX_SEARCH_FILTER_VALUES || values.some((item) => typeof item !== "string" || item.length > MAX_SEARCH_FILTER_VALUE_CHARS)) return;
1093
- input[field] = values;
1094
- }
1095
- const normalized = normalizeDocsSearchFilters(input);
1096
- for (const field of SEARCH_FILTER_FIELDS) {
1097
- const values = record[field];
1098
- if (values === void 0) continue;
1099
- if (!Array.isArray(values) || values.length !== (normalized[field]?.length ?? 0) || values.some((value, index) => value !== normalized[field]?.[index])) return;
1100
- }
1101
- return normalized;
1102
- }
1103
- function normalizeDocsSearchMetadataValues(field, value) {
1104
- const values = normalizeAgentScopeValues(value);
1105
- return Array.from(new Set(values.map((item) => normalizeDocsSearchFilterValue(field, item)).filter((item) => Boolean(item))));
1106
- }
1107
- function valuesOverlap(left, right, matches) {
1108
- return left.some((leftValue) => right.some((rightValue) => matches(leftValue, rightValue)));
1109
- }
1110
- function resolveDocsSearchPageScope(page) {
1111
- const topFramework = normalizeDocsSearchMetadataValues("framework", page.framework);
1112
- const contractFramework = normalizeDocsSearchMetadataValues("framework", page.agent?.appliesTo?.framework);
1113
- const topVersion = normalizeDocsSearchMetadataValues("version", page.version);
1114
- const contractVersion = normalizeDocsSearchMetadataValues("version", page.agent?.appliesTo?.version);
1115
- const packageValues = normalizeDocsSearchMetadataValues("package", page.agent?.appliesTo?.package);
1116
- const tags = normalizeDocsSearchMetadataValues("tags", page.tags);
1117
- const conflicts = [];
1118
- if (topFramework.length > 0 && contractFramework.length > 0 && !valuesOverlap(topFramework, contractFramework, (left, right) => left === right)) conflicts.push("framework");
1119
- if (topVersion.length > 0 && contractVersion.length > 0 && !valuesOverlap(topVersion, contractVersion, agentVersionConstraintsOverlap)) conflicts.push("version");
1120
- return {
1121
- framework: topFramework.length > 0 && contractFramework.length > 0 ? topFramework.filter((value) => contractFramework.includes(value)) : Array.from(new Set([...topFramework, ...contractFramework])),
1122
- version: Array.from(new Set([...topVersion, ...contractVersion])),
1123
- package: packageValues,
1124
- tags,
1125
- declarations: {
1126
- framework: [topFramework, contractFramework].filter((values) => values.length > 0),
1127
- version: [topVersion, contractVersion].filter((values) => values.length > 0),
1128
- package: packageValues.length > 0 ? [packageValues] : [],
1129
- tags: tags.length > 0 ? [tags] : []
1130
- },
1131
- conflicts
1132
- };
1133
- }
1134
- function docsSearchScopeValueMatches(field, requested, candidate) {
1135
- return field === "version" ? agentVersionConstraintsOverlap(requested, candidate) : requested === candidate;
1136
- }
1137
- function docsSearchPageMatchesFilters(scope, filters) {
1138
- if (scope.conflicts.length > 0) return false;
1139
- return SEARCH_FILTER_FIELDS.every((field) => {
1140
- const requested = filters[field];
1141
- if (!requested || requested.length === 0) return true;
1142
- return docsSearchScopeFieldMatches(scope, field, requested);
1143
- });
1144
- }
1145
- function docsSearchScopeFieldMatches(scope, field, requested) {
1146
- const declarations = scope.declarations[field];
1147
- if (declarations.length === 0) return false;
1148
- if (field === "version") return agentVersionConstraintGroupsOverlap([requested, ...declarations]);
1149
- return requested.some((value) => declarations.every((candidates) => candidates.some((candidate) => docsSearchScopeValueMatches(field, value, candidate))));
1150
- }
1151
- function stripMarkdownText(content) {
1152
- return removeMdxModuleLinesOutsideFences(content).replace(/```[^\n]*\n([\s\S]*?)```/g, "$1").replace(/```([\s\S]*?)```/g, "$1").replace(/~~~[^\n]*\n([\s\S]*?)~~~/g, "$1").replace(/~~~([\s\S]*?)~~~/g, "$1").replace(/<[^>]+\/>/g, "").replace(/<\/?[A-Z][^>]*>/g, "").replace(/<\/?[a-z][^>]*>/g, "").replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^\|?[\s:-]+(\|[\s:-]+)+\|?\s*$/gm, "").replace(/\|/g, " ").replace(/^[-*+]\s+/gm, "").replace(/(\*{1,3}|_{1,3})(.*?)\1/g, "$2").replace(/`{3,}[^\n]*$/gm, "").replace(/`([^`]+)`/g, "$1").replace(/`+/g, "").replace(/^>\s+/gm, "").replace(/^[-*_]{3,}\s*$/gm, "").replace(/\n{3,}/g, "\n\n").replace(/\s{2,}/g, " ").trim();
1153
- }
1154
- function stripHtml(text) {
1155
- return text.replace(/<[^>]+>/g, "");
1156
- }
1157
- function normalizeMcpSsePayload(body) {
1158
- const payload = body.split("\n").filter((line) => line.startsWith("data: ")).map((line) => line.slice(6).trim()).filter(Boolean).at(-1);
1159
- return payload ? JSON.parse(payload) : null;
1160
- }
1161
- function normalizeWhitespace(value) {
1162
- return value.replace(/\s+/g, " ").trim();
1163
- }
1164
- function normalizeSearchPhrase(value) {
1165
- return normalizeWhitespace(value.toLowerCase().replace(/[?!.,;:]+$/g, ""));
1166
- }
1167
- function escapeRegExp(value) {
1168
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1169
- }
1170
- function literalMatchPriority(query, value) {
1171
- const q = normalizeSearchPhrase(query);
1172
- const text = normalizeSearchPhrase(value ?? "");
1173
- if (!q || !text) return 0;
1174
- if (text === q) return 2;
1175
- const boundary = "[^\\p{L}\\p{N}]";
1176
- return new RegExp(`(^|${boundary})${escapeRegExp(q)}(?=$|${boundary})`, "u").test(text) ? 1 : 0;
1177
- }
1178
- function isLiteralLookupQuery(query) {
1179
- const q = normalizeSearchPhrase(query);
1180
- const words = tokenizeSearchQuery(q);
1181
- return words.length > 0 && words.length <= 3 && words.join(" ") === q;
1182
- }
1183
- function tokenizeSearchQuery(query) {
1184
- return Array.from(new Set(query.toLowerCase().replace(/[^\p{L}\p{N}@/_:.-]+/gu, " ").split(/\s+/).map((word) => word.replace(/^[^\p{L}\p{N}@]+|[^\p{L}\p{N}]+$/gu, "")).filter((word) => word.length > 1 && !SEARCH_STOP_WORDS.has(word))));
1185
- }
1186
- function normalizeUrlRouteKey(value) {
1187
- try {
1188
- const url = new URL(value, "https://docs.local");
1189
- return `${url.pathname || "/"}${url.search}`;
1190
- } catch {
1191
- return value.split("#", 1)[0] || "/";
1192
- }
1193
- }
1194
- function normalizeAuthoredUrlIdentity(value) {
1195
- const trimmed = value.trim();
1196
- const explicitScheme = trimmed.match(/^([a-z][a-z\d+.-]*):/iu)?.[1]?.toLowerCase();
1197
- if (explicitScheme && explicitScheme !== "http" && explicitScheme !== "https") return normalizeUrlRouteKey("/");
1198
- try {
1199
- const url = new URL(trimmed, "https://docs.local");
1200
- const route = `${url.pathname || "/"}${url.search}`;
1201
- if (explicitScheme) return `${url.origin}${route}`;
1202
- if (trimmed.startsWith("//")) return `//${url.host}${route}`;
1203
- return route;
1204
- } catch {
1205
- return normalizeUrlRouteKey(trimmed);
1206
- }
1207
- }
1208
- function appendDocsLocaleQuery(value, locale) {
1209
- const hashIndex = value.indexOf("#");
1210
- const withoutHash = hashIndex >= 0 ? value.slice(0, hashIndex) : value;
1211
- const hash = hashIndex >= 0 ? value.slice(hashIndex) : "";
1212
- const queryIndex = withoutHash.indexOf("?");
1213
- const rawQuery = queryIndex >= 0 ? withoutHash.slice(queryIndex + 1) : "";
1214
- if (new URLSearchParams(rawQuery).has("lang")) return value;
1215
- return `${withoutHash}${queryIndex >= 0 ? "&" : "?"}lang=${encodeURIComponent(locale)}${hash}`;
1216
- }
1217
- function localizeDocsSearchPage(page, localeFallback) {
1218
- const rawLocale = (page.locale ?? localeFallback)?.trim();
1219
- if (!rawLocale) return page;
1220
- const locale = rawLocale;
1221
- const url = appendDocsLocaleQuery(page.url, locale);
1222
- if (url === page.url && page.locale === locale) return page;
1223
- return {
1224
- ...page,
1225
- url,
1226
- locale
1227
- };
1228
- }
1229
- function safeDecodeUrlSegment(value) {
1230
- try {
1231
- return decodeURIComponent(value);
1232
- } catch {
1233
- return value;
1234
- }
1235
- }
1236
- function getUrlSearchSegments(value) {
1237
- let pathname = "";
1238
- try {
1239
- pathname = new URL(value, "https://docs.local").pathname;
1240
- } catch {
1241
- pathname = value.split(/[?#]/)[0] ?? "";
1242
- }
1243
- return Array.from(new Set(pathname.split("/").flatMap((segment) => {
1244
- const decoded = safeDecodeUrlSegment(segment);
1245
- return [decoded, decoded.replace(/[-_]+/g, " ")];
1246
- }).map(normalizeSearchPhrase).filter(Boolean)));
1247
- }
1248
- function resolveAskAIContextUrl(value, baseUrl) {
1249
- if (!baseUrl) return value;
1250
- try {
1251
- return new URL(value, baseUrl).toString();
1252
- } catch {
1253
- return value;
1254
- }
1255
- }
1256
- function cleanGeneratedAgentContractMarkers(content) {
1257
- return stripDocsGeneratedAgentContractMarkers(content);
1258
- }
1259
- function getAskAIPageSectionContent(page) {
1260
- return getPageAudienceSectionContent(page, "agent");
1261
- }
1262
- function getAskAIPageContent(page) {
1263
- return cleanGeneratedAgentContractMarkers(getAskAIPageSectionContent(page));
1264
- }
1265
- function getPageAgentContractSearchText(page) {
1266
- return stripMarkdownText(renderPageAgentContractMarkdown(page.agent).replace(PAGE_AGENT_CONTRACT_START_MARKER, "").replace(PAGE_AGENT_CONTRACT_END_MARKER, ""));
1267
- }
1268
- function removeMdxModuleLinesOutsideFences(content) {
1269
- let inFence = false;
1270
- return content.split("\n").filter((line) => {
1271
- const trimmed = line.trimStart();
1272
- if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
1273
- inFence = !inFence;
1274
- return true;
1275
- }
1276
- return inFence || !/^(import|export)\s/.test(trimmed);
1277
- }).join("\n");
1278
- }
1279
- function cleanAskAIContextMarkdown(content) {
1280
- return removeMdxModuleLinesOutsideFences(content).replace(/<[^>]+\/>/g, "").replace(/<\/?[A-Z][^>]*>/g, "").replace(/<\/?[a-z][^>]*>/g, "").replace(/\n{3,}/g, "\n\n").trim();
1281
- }
1282
- function packageRootFromSpecifier(specifier) {
1283
- if (!specifier || specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("@/") || specifier.startsWith("~/") || specifier.startsWith("#")) return null;
1284
- const parts = specifier.split("/").filter(Boolean);
1285
- if (parts.length === 0) return null;
1286
- if (parts[0]?.startsWith("@")) return parts.length > 1 ? `${parts[0]}/${parts[1]}` : null;
1287
- return parts[0];
1288
- }
1289
- function cleanPackageToken(token) {
1290
- const trimmed = token.trim().replace(/^["'`]+|["'`,;]+$/g, "").replace(/\\$/g, "");
1291
- if (!trimmed || trimmed.startsWith("-") || /^[A-Z_][A-Z0-9_]*=/.test(trimmed)) return null;
1292
- if (/^(npm|pnpm|yarn|bun|install|add|i|x|dlx|run|exec)$/.test(trimmed)) return null;
1293
- return packageRootFromSpecifier(trimmed.startsWith("@") ? trimmed.replace(/^(@[^/]+\/[^@]+)@.+$/, "$1") : trimmed.replace(/^([^@]+)@.+$/, "$1"));
1294
- }
1295
- function inferDocsAskAIPackageHints(content) {
1296
- const packages = /* @__PURE__ */ new Set();
1297
- const imports = /* @__PURE__ */ new Set();
1298
- const installCommands = /* @__PURE__ */ new Set();
1299
- for (const line of content.split("\n")) {
1300
- const trimmed = line.trim();
1301
- if (!trimmed) continue;
1302
- const importSpecifier = trimmed.match(/^(?:import|export)\s+(?:type\s+)?[\s\S]*?\s+from\s+["']([^"']+)["']/)?.[1];
1303
- const bareImportSpecifier = trimmed.match(/^import\s+["']([^"']+)["']/)?.[1];
1304
- const requireSpecifier = trimmed.match(/require\(["']([^"']+)["']\)/)?.[1];
1305
- const specifier = importSpecifier ?? bareImportSpecifier ?? requireSpecifier;
1306
- const packageName = specifier ? packageRootFromSpecifier(specifier) : null;
1307
- if (packageName) {
1308
- packages.add(packageName);
1309
- if (/^(?:import|export)\s/.test(trimmed)) imports.add(trimmed);
1310
- }
1311
- const installMatch = trimmed.match(/^(?:npm\s+(?:install|i)|pnpm\s+add|yarn\s+add|bun\s+add)\s+(.+)$/);
1312
- if (!installMatch) continue;
1313
- const commandPackages = installMatch[1].split(/\s+/).map(cleanPackageToken).filter((value) => Boolean(value));
1314
- if (commandPackages.length > 0) {
1315
- installCommands.add(trimmed);
1316
- for (const name of commandPackages) packages.add(name);
1317
- }
1318
- }
1319
- return {
1320
- packages: Array.from(packages).slice(0, 8),
1321
- imports: Array.from(imports).slice(0, 12),
1322
- installCommands: Array.from(installCommands).slice(0, 8)
1323
- };
1324
- }
1325
- function formatDocsAskAIPackageHints(hints, packageName) {
1326
- const packages = packageName ? Array.from(new Set([packageName, ...hints.packages])) : hints.packages;
1327
- if (packages.length === 0 && hints.imports.length === 0 && hints.installCommands.length === 0) return;
1328
- const lines = ["Package and import hints inferred from the retrieved documentation context:"];
1329
- if (packages.length > 0) lines.push(`- Package names found in install/import examples: ${packages.join(", ")}`);
1330
- if (hints.imports.length > 0) lines.push(`- Exact import lines found in context: ${hints.imports.map((line) => `\`${line}\``).join("; ")}`);
1331
- if (hints.installCommands.length > 0) lines.push(`- Exact install commands found in context: ${hints.installCommands.map((line) => `\`${line}\``).join("; ")}`);
1332
- lines.push("Use these exact package names, install commands, and import lines when relevant. Do not replace them with placeholders.");
1333
- return lines.join("\n");
1334
- }
1335
- function clampText(value, maxChars) {
1336
- if (maxChars <= 0) return "";
1337
- if (value.length <= maxChars) return value;
1338
- return `${value.slice(0, maxChars).trimEnd()}...`;
1339
- }
1340
- function findPageForSearchResult(pages, result, baseUrl, pagesByRoute) {
1341
- const rawUrl = result.url.trim();
1342
- const explicitlySchemed = /^[a-z][a-z\d+.-]*:/iu.test(rawUrl);
1343
- const explicitlyHosted = explicitlySchemed || /^[\\/]{2}/u.test(rawUrl);
1344
- if (explicitlySchemed && !/^https?:/iu.test(rawUrl)) return void 0;
1345
- if (explicitlyHosted && !baseUrl) return void 0;
1346
- if (explicitlyHosted && baseUrl) try {
1347
- if (new URL(result.url, baseUrl).origin !== new URL(baseUrl).origin) return void 0;
1348
- } catch {
1349
- return;
1350
- }
1351
- const resultPath = normalizeUrlRouteKey(result.url);
1352
- return pagesByRoute?.get(resultPath) ?? pages.find((page) => normalizeUrlRouteKey(page.url) === resultPath);
1353
- }
1354
- function inferResultTitle(result, page) {
1355
- if (page) return page.title;
1356
- return stripHtml(result.content).trim().split("—")[0]?.trim() || result.url;
1357
- }
1358
- function formatAskAIContextResult(options) {
1359
- const { result, page, maxChars, baseUrl } = options;
1360
- const title = inferResultTitle(result, page);
1361
- const section = result.section;
1362
- const sectionSelector = getSearchResultAnchor(result.url) ?? section;
1363
- const contextContent = clampText(cleanAskAIContextMarkdown(page ? sectionSelector ? cleanGeneratedAgentContractMarkers(findDocsMarkdownSection(getAskAIPageSectionContent(page), sectionSelector)?.content ?? "") : getAskAIPageContent(page) : [result.content, result.description].filter(Boolean).join("\n\n")), maxChars);
1364
- return {
1365
- ...result,
1366
- url: resolveAskAIContextUrl(result.url, baseUrl),
1367
- title,
1368
- contextContent
1369
- };
1370
- }
1371
- function getSearchResultKey(result) {
1372
- const anchor = getSearchResultAnchor(result.url);
1373
- const sectionFallback = normalizeWhitespace(result.section ?? "").toLowerCase();
1374
- return `${normalizeUrlRouteKey(result.url)}#${anchor ?? sectionFallback}`;
1375
- }
1376
- function getSearchResultAnchor(value) {
1377
- let hash = "";
1378
- try {
1379
- hash = new URL(value, "https://docs.local").hash.replace(/^#/, "");
1380
- } catch {
1381
- const hashIndex = value.indexOf("#");
1382
- hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : "";
1383
- }
1384
- if (!hash) return void 0;
1385
- try {
1386
- return decodeURIComponent(hash);
1387
- } catch {
1388
- return hash;
1389
- }
1390
- }
1391
- function getAskAIResultPageKey(value, baseUrl, strictExternalOrigins = false) {
1392
- const path = normalizeUrlRouteKey(value);
1393
- const rawValue = value.trim();
1394
- if (!(/^[a-z][a-z\d+.-]*:/iu.test(rawValue) || /^[\\/]{2}/u.test(rawValue)) || !baseUrl && !strictExternalOrigins) return path;
1395
- try {
1396
- const fallbackBase = baseUrl ?? "https://docs.local";
1397
- const parsed = new URL(value, fallbackBase);
1398
- const configuredBase = baseUrl ? new URL(baseUrl) : void 0;
1399
- if (configuredBase && parsed.origin === configuredBase.origin) return path;
1400
- return `${parsed.protocol}//${parsed.host}${path}`;
1401
- } catch {
1402
- return `${value.split("#", 1)[0]}::${path}`;
1403
- }
1404
- }
1405
- function getAskAIResultKey(result, baseUrl, strictExternalOrigins = false) {
1406
- const anchor = getSearchResultAnchor(result.url);
1407
- const sectionFallback = normalizeWhitespace(result.section ?? "").toLowerCase();
1408
- return `${getAskAIResultPageKey(result.url, baseUrl, strictExternalOrigins)}#${anchor ?? sectionFallback}`;
1409
- }
1410
- function mergeSearchResults(groups, getResultKey = getSearchResultKey) {
1411
- const seen = /* @__PURE__ */ new Set();
1412
- const results = [];
1413
- for (const group of groups) for (const result of group) {
1414
- const key = getResultKey(result);
1415
- if (seen.has(key)) continue;
1416
- seen.add(key);
1417
- results.push(result);
1418
- }
1419
- return results;
1420
- }
1421
- function buildAudienceProjectionSearchResults(documents, query) {
1422
- return documents.map((document) => {
1423
- const score = scoreDocument(query, document);
1424
- return {
1425
- id: document.id,
1426
- url: document.url,
1427
- content: document.section ? `${document.title} — ${document.section}` : document.title,
1428
- description: cleanSearchResultText(buildSnippet(document, query) ?? document.description),
1429
- type: document.type,
1430
- score,
1431
- section: document.section
1432
- };
1433
- });
1434
- }
1435
- function sanitizeExternalAudienceSearchResults(results, localAudienceResults, baseUrl, preserveUnmatched, fallbackSectionToLocalPage = false) {
1436
- const localByKey = new Map(localAudienceResults.map((result) => [getSearchResultKey(result), result]));
1437
- const localPageResults = /* @__PURE__ */ new Map();
1438
- for (const result of localAudienceResults) {
1439
- const pageUrl = normalizeUrlRouteKey(result.url);
1440
- if (!localPageResults.get(pageUrl) || result.type === "page") localPageResults.set(pageUrl, result);
1441
- }
1442
- return results.flatMap((result) => {
1443
- const rawUrl = result.url.trim();
1444
- const explicitlySchemed = /^[a-z][a-z\d+.-]*:/iu.test(rawUrl);
1445
- const externallyHosted = explicitlySchemed && /^https?:/iu.test(rawUrl) || /^[\\/]{2}/u.test(rawUrl);
1446
- const unsupportedScheme = explicitlySchemed && !/^https?:/iu.test(rawUrl);
1447
- let sameOriginOrUnknown = !externallyHosted;
1448
- if (unsupportedScheme) sameOriginOrUnknown = false;
1449
- if (externallyHosted && baseUrl) try {
1450
- sameOriginOrUnknown = new URL(result.url, baseUrl).origin === new URL(baseUrl).origin;
1451
- } catch {
1452
- sameOriginOrUnknown = false;
1453
- }
1454
- const hasSection = Boolean(getSearchResultAnchor(result.url) || result.section);
1455
- const local = sameOriginOrUnknown ? localByKey.get(getSearchResultKey(result)) ?? (hasSection && !fallbackSectionToLocalPage ? void 0 : localPageResults.get(normalizeUrlRouteKey(result.url))) : void 0;
1456
- if (!local) return preserveUnmatched?.(result) ? [result] : [];
1457
- return [{
1458
- ...local,
1459
- id: result.id,
1460
- score: result.score ?? local.score
1461
- }];
1462
- });
1463
- }
1464
- function shouldPreserveUnmatchedExternalResult(options) {
1465
- const { result, localPagePaths, baseUrl } = options;
1466
- const path = normalizeUrlRouteKey(result.url);
1467
- const rawUrl = result.url.trim();
1468
- const explicitScheme = rawUrl.match(/^([a-z][a-z\d+.-]*):/iu)?.[1]?.toLowerCase();
1469
- if (explicitScheme && explicitScheme !== "http" && explicitScheme !== "https") return false;
1470
- const explicitlyHosted = /^[a-z][a-z\d+.-]*:/iu.test(rawUrl) || /^[\\/]{2}/u.test(rawUrl);
1471
- const knownLocalPath = localPagePaths.has(path);
1472
- if (!explicitlyHosted) return !knownLocalPath;
1473
- if (!baseUrl) return true;
1474
- try {
1475
- if (new URL(result.url, baseUrl).origin !== new URL(baseUrl).origin) return true;
1476
- } catch {
1477
- return false;
1478
- }
1479
- return !knownLocalPath;
1480
- }
1481
- function getPageAudienceSource(page, audience) {
1482
- return audience === "agent" ? page.agentRawContent ?? page.agentFallbackRawContent ?? page.agentContent ?? page.agentFallbackContent ?? page.rawContent ?? page.content : page.rawContent ?? page.content;
1483
- }
1484
- function getPageAudienceRawContent(page, audience) {
1485
- return resolveDocsAudienceMdxContent(getPageAudienceSource(page, audience), audience);
1486
- }
1487
- function getPageAudienceSectionContent(page, audience) {
1488
- const content = getPageAudienceRawContent(page, audience);
1489
- return audience === "agent" ? upsertPageAgentContractMarkdown(content, page.agent) : content;
1490
- }
1491
- /**
1492
- * Build the exact normalized-input projection represented by a retrieval source digest.
1493
- * This is public so consumers can independently reproduce `source.digest`.
1494
- */
1495
- function buildDocsRetrievalDigestProjection(page, audience = "human") {
1496
- const audienceContent = getPageAudienceRawContent(page, audience);
1497
- return audience === "agent" ? upsertPageAgentContractMarkdown(audienceContent, page.agent) : [audienceContent, renderPageAgentContractMarkdown(page.agent)].filter(Boolean).join("\n\n");
1498
- }
1499
- function getPageAudienceSearchText(page, audience) {
1500
- return stripMarkdownText(getPageAudienceRawContent(page, audience));
1501
- }
1502
- function getPageAudienceIndexContent(page, audience) {
1503
- return normalizeWhitespace([getPageAudienceSearchText(page, audience), getPageAgentContractSearchText(page)].join(" "));
1504
- }
1505
- function sortRetrievalSourceValues(values) {
1506
- const sorted = Array.from(new Set(values)).sort(compareSearchMetadataValues).slice(0, MAX_SEARCH_FILTER_VALUES);
1507
- return sorted.length > 0 ? sorted : void 0;
1508
- }
1509
- function buildDocsRetrievalSourceScope(page, audience, localeFallback) {
1510
- const resolved = resolveDocsSearchPageScope(page);
1511
- const rawLocale = (page.locale ?? localeFallback)?.trim();
1512
- const locale = rawLocale ? normalizeAgentLocale(rawLocale).slice(0, MAX_SEARCH_FILTER_VALUE_CHARS) : void 0;
1513
- const framework = sortRetrievalSourceValues(resolved.framework);
1514
- const version = sortRetrievalSourceValues(resolved.version);
1515
- const versionGroups = resolved.declarations.version.map((group) => sortRetrievalSourceValues(group)).filter((group) => Boolean(group)).slice(0, MAX_SEARCH_FILTER_VALUES);
1516
- const packageNames = sortRetrievalSourceValues(resolved.package);
1517
- const tags = sortRetrievalSourceValues(resolved.tags);
1518
- const truncated = SEARCH_FILTER_FIELDS.filter((field) => {
1519
- if (field === "version") return resolved.version.length > MAX_SEARCH_FILTER_VALUES || resolved.declarations.version.length > MAX_SEARCH_FILTER_VALUES || resolved.declarations.version.some((group) => group.length > MAX_SEARCH_FILTER_VALUES);
1520
- return resolved[field].length > MAX_SEARCH_FILTER_VALUES;
1521
- });
1522
- return {
1523
- audience,
1524
- ...locale ? { locale: [locale] } : {},
1525
- ...framework ? { framework } : {},
1526
- ...version ? { version } : {},
1527
- ...versionGroups.length > 1 ? { versionGroups } : {},
1528
- ...packageNames ? { package: packageNames } : {},
1529
- ...tags ? { tags } : {},
1530
- ...truncated.length > 0 ? { truncated } : {},
1531
- ...resolved.conflicts.length > 0 ? { conflicts: [...resolved.conflicts].sort((left, right) => SEARCH_FILTER_FIELDS.indexOf(left) - SEARCH_FILTER_FIELDS.indexOf(right)) } : {}
1532
- };
1533
- }
1534
- function buildDocsRetrievalScopeIdentity(page, audience, localeFallback) {
1535
- const resolved = resolveDocsSearchPageScope(page);
1536
- const sortValues = (values) => Array.from(new Set(values)).sort(compareSearchMetadataValues);
1537
- const declarations = Object.fromEntries(SEARCH_FILTER_FIELDS.map((field) => [field, resolved.declarations[field].map(sortValues).sort((left, right) => compareSearchMetadataValues(JSON.stringify(left), JSON.stringify(right)))]));
1538
- return hashDocsRetrievalValue(JSON.stringify({
1539
- audience,
1540
- locale: (page.locale ?? localeFallback)?.trim(),
1541
- declarations,
1542
- conflicts: [...resolved.conflicts].sort((left, right) => SEARCH_FILTER_FIELDS.indexOf(left) - SEARCH_FILTER_FIELDS.indexOf(right))
1543
- }));
1544
- }
1545
- function getUrlHash(value) {
1546
- const hashIndex = value.indexOf("#");
1547
- return hashIndex >= 0 ? value.slice(hashIndex) : "";
1548
- }
1549
- function resolveDocsRetrievalCanonicalUrl(page, resultUrl, baseUrl) {
1550
- const requestedCanonical = page.canonicalUrl?.trim();
1551
- const requested = requestedCanonical && requestedCanonical.length <= MAX_RETRIEVAL_SOURCE_URL_CHARS ? requestedCanonical : page.url;
1552
- const explicitScheme = requested.match(/^([a-z][a-z\d+.-]*):/iu)?.[1]?.toLowerCase();
1553
- const pageScheme = page.url.match(/^([a-z][a-z\d+.-]*):/iu)?.[1]?.toLowerCase();
1554
- const safePageUrl = pageScheme && pageScheme !== "http" && pageScheme !== "https" ? "/" : page.url;
1555
- const configured = explicitScheme && explicitScheme !== "http" && explicitScheme !== "https" ? safePageUrl : requested;
1556
- let canonical = configured;
1557
- if (baseUrl) try {
1558
- canonical = new URL(configured, baseUrl).toString();
1559
- } catch {
1560
- canonical = configured;
1561
- }
1562
- else if (configured.startsWith("//")) canonical = `https:${configured}`;
1563
- const withSection = `${canonical.split("#", 1)[0]}${getUrlHash(resultUrl)}`;
1564
- if (withSection.length <= MAX_RETRIEVAL_SOURCE_URL_CHARS && isDocsRetrievalCanonicalUrl(withSection)) return withSection;
1565
- const fallback = (baseUrl ? (() => {
1566
- try {
1567
- return new URL(safePageUrl, baseUrl).toString();
1568
- } catch {
1569
- return safePageUrl;
1570
- }
1571
- })() : safePageUrl).split("#", 1)[0];
1572
- if (fallback.length <= MAX_RETRIEVAL_SOURCE_URL_CHARS && isDocsRetrievalCanonicalUrl(fallback)) return fallback;
1573
- if (baseUrl) try {
1574
- const root = new URL("/", baseUrl).toString();
1575
- if (root.length <= MAX_RETRIEVAL_SOURCE_URL_CHARS && isDocsRetrievalCanonicalUrl(root)) return root;
1576
- } catch {}
1577
- return "/";
1578
- }
1579
- function hashDocsRetrievalValue(value) {
1580
- return digestDocsRetrievalContent(value);
1581
- }
1582
- const docsRetrievalDigestMemo = /* @__PURE__ */ new WeakMap();
1583
- function getDocsRetrievalSourceDigest(page, audience, cache) {
1584
- const cached = cache?.get(page);
1585
- if (cached) return cached;
1586
- const source = getPageAudienceSource(page, audience);
1587
- let agentContractKey = "";
1588
- let cacheable = true;
1589
- try {
1590
- agentContractKey = JSON.stringify(page.agent ?? null);
1591
- } catch {
1592
- cacheable = false;
1593
- }
1594
- const memo = docsRetrievalDigestMemo.get(page)?.[audience];
1595
- if (cacheable && memo && memo.source === source && memo.agentContractKey === agentContractKey) {
1596
- cache?.set(page, memo.digest);
1597
- return memo.digest;
1598
- }
1599
- const digest = hashDocsRetrievalValue(buildDocsRetrievalDigestProjection(page, audience));
1600
- if (cacheable) {
1601
- const pageMemo = docsRetrievalDigestMemo.get(page) ?? {};
1602
- pageMemo[audience] = {
1603
- source,
1604
- agentContractKey,
1605
- digest
1606
- };
1607
- docsRetrievalDigestMemo.set(page, pageMemo);
1608
- }
1609
- cache?.set(page, digest);
1610
- return digest;
1611
- }
1612
- function resolveDocsRetrievalLastModified(page, audience) {
1613
- const parseCandidate = (value) => {
1614
- const normalized = value?.trim();
1615
- if (!normalized) return void 0;
1616
- const timestamp = Date.parse(normalized);
1617
- return Number.isFinite(timestamp) ? {
1618
- value: normalized,
1619
- timestamp
1620
- } : void 0;
1621
- };
1622
- const pageModified = parseCandidate(page.lastmod) ?? parseCandidate(page.lastModified);
1623
- if (!(audience === "agent" && (page.agentRawContent !== void 0 || page.agentContent !== void 0))) return pageModified?.value;
1624
- const agentModified = parseCandidate(page.agentLastModified);
1625
- if (!renderPageAgentContractMarkdown(page.agent)) return agentModified?.value ?? pageModified?.value;
1626
- if (!agentModified) return pageModified?.value;
1627
- if (!pageModified) return agentModified.value;
1628
- return agentModified.timestamp >= pageModified.timestamp ? agentModified.value : pageModified.value;
1629
- }
1630
- async function buildDocsSearchIndexGeneration(pages, options) {
1631
- const sources = (await Promise.all(pages.map(async (rawPage) => {
1632
- const page = localizeDocsSearchPage(rawPage, options.locale);
1633
- const authoredLastModified = page.lastmod?.trim();
1634
- return {
1635
- canonicalIdentity: normalizeAuthoredUrlIdentity(page.canonicalUrl?.trim() || page.url),
1636
- url: normalizeAuthoredUrlIdentity(page.url),
1637
- indexedUrl: page.url,
1638
- title: page.title,
1639
- description: page.description,
1640
- type: page.type,
1641
- scope: buildDocsRetrievalSourceScope(page, options.audience, options.locale),
1642
- scopeIdentity: buildDocsRetrievalScopeIdentity(page, options.audience, options.locale),
1643
- lastModified: authoredLastModified && Number.isFinite(Date.parse(authoredLastModified)) ? authoredLastModified : void 0,
1644
- digest: getDocsRetrievalSourceDigest(rawPage, options.audience, options.digestCache),
1645
- agentContract: getPageAgentContractSearchText(rawPage)
1646
- };
1647
- }))).sort((left, right) => {
1648
- const canonical = compareSearchMetadataValues(left.canonicalIdentity, right.canonicalIdentity);
1649
- if (canonical !== 0) return canonical;
1650
- const url = compareSearchMetadataValues(left.url, right.url);
1651
- if (url !== 0) return url;
1652
- const digest = compareSearchMetadataValues(left.digest, right.digest);
1653
- if (digest !== 0) return digest;
1654
- const title = compareSearchMetadataValues(left.title, right.title);
1655
- return title !== 0 ? title : compareSearchMetadataValues(JSON.stringify(left), JSON.stringify(right));
1656
- });
1657
- return hashDocsRetrievalValue(JSON.stringify({
1658
- format: RETRIEVAL_INDEX_FORMAT,
1659
- audience: options.audience,
1660
- chunking: options.chunking.strategy ?? "section",
1661
- sources
1662
- }));
1663
- }
1664
- async function buildDocsRetrievalSource(rawPage, resultUrl, options) {
1665
- const page = localizeDocsSearchPage(rawPage, options.locale);
1666
- const lastModified = resolveDocsRetrievalLastModified(rawPage, options.audience);
1667
- return {
1668
- canonicalUrl: resolveDocsRetrievalCanonicalUrl(page, resultUrl, options.baseUrl),
1669
- scope: buildDocsRetrievalSourceScope(page, options.audience, options.locale),
1670
- ...lastModified ? { lastModified } : {},
1671
- digest: getDocsRetrievalSourceDigest(rawPage, options.audience, options.digestCache),
1672
- indexGeneration: options.indexGeneration
1673
- };
1674
- }
1675
- function parseRetrievalSourceString(value, maxChars = MAX_RETRIEVAL_SOURCE_VALUE_CHARS) {
1676
- if (typeof value !== "string") return void 0;
1677
- const normalized = value.trim();
1678
- if (!normalized || normalized.length > maxChars) return void 0;
1679
- if (Array.from(normalized).some((character) => {
1680
- const codePoint = character.codePointAt(0) ?? 0;
1681
- return codePoint <= 31 || codePoint === 127;
1682
- })) return void 0;
1683
- return normalized;
1684
- }
1685
- function parseRetrievalSourceValues(field, value) {
1686
- if (!Array.isArray(value) || value.length > MAX_SEARCH_FILTER_VALUES) return { valid: false };
1687
- const parsed = [];
1688
- for (const item of value) {
1689
- const stringValue = parseRetrievalSourceString(item, MAX_SEARCH_FILTER_VALUE_CHARS);
1690
- if (!stringValue) return { valid: false };
1691
- if (field === "locale") {
1692
- const normalizedLocale = normalizeAgentLocale(stringValue);
1693
- if (!normalizedLocale) return { valid: false };
1694
- parsed.push(normalizedLocale);
1695
- continue;
1696
- }
1697
- const normalized = normalizeDocsSearchFilterValue(field, stringValue);
1698
- if (!normalized) return { valid: false };
1699
- parsed.push(normalized);
1700
- }
1701
- return {
1702
- valid: true,
1703
- values: sortRetrievalSourceValues(parsed)
1704
- };
1705
- }
1706
- function parseRetrievalSourceVersionGroups(value) {
1707
- if (!Array.isArray(value) || value.length === 0 || value.length > MAX_SEARCH_FILTER_VALUES) return { valid: false };
1708
- const groups = value.map((group) => parseRetrievalSourceValues("version", group));
1709
- if (groups.some((group) => !group.valid || !group.values || group.values.length === 0)) return { valid: false };
1710
- return {
1711
- valid: true,
1712
- groups: groups.map((group) => group.values)
1713
- };
1714
- }
1715
- function parseDocsRetrievalSource(value, options = {}) {
1716
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1717
- const record = value;
1718
- const canonicalUrl = parseRetrievalSourceString(record.canonicalUrl, MAX_RETRIEVAL_SOURCE_URL_CHARS);
1719
- const digest = parseRetrievalSourceString(record.digest);
1720
- const indexGeneration = parseRetrievalSourceString(record.indexGeneration);
1721
- if (!canonicalUrl || !digest || !indexGeneration) return void 0;
1722
- const digestPattern = /^sha256:[a-f\d]{64}$/iu;
1723
- if (!digestPattern.test(digest) || !digestPattern.test(indexGeneration)) return void 0;
1724
- if (!isDocsRetrievalCanonicalUrl(canonicalUrl) || /^\/(?!\/)/u.test(canonicalUrl) && !options.allowRootRelativeCanonical) return;
1725
- if (!record.scope || typeof record.scope !== "object" || Array.isArray(record.scope)) return;
1726
- const rawScope = record.scope;
1727
- const audience = rawScope.audience === "human" || rawScope.audience === "agent" ? rawScope.audience : void 0;
1728
- if (!audience) return void 0;
1729
- const parseScopeFields = (value) => {
1730
- if (!Array.isArray(value) || value.length > SEARCH_FILTER_FIELDS.length || value.some((field) => typeof field !== "string" || !SEARCH_FILTER_FIELDS.includes(field))) return { valid: false };
1731
- return {
1732
- valid: true,
1733
- values: Array.from(new Set(value)).sort((left, right) => SEARCH_FILTER_FIELDS.indexOf(left) - SEARCH_FILTER_FIELDS.indexOf(right))
1734
- };
1735
- };
1736
- const parseOptionalValues = (field, value) => value === void 0 ? { valid: true } : parseRetrievalSourceValues(field, value);
1737
- const parseOptionalScopeFields = (value) => value === void 0 ? { valid: true } : parseScopeFields(value);
1738
- const conflicts = parseOptionalScopeFields(rawScope.conflicts);
1739
- const truncated = parseOptionalScopeFields(rawScope.truncated);
1740
- const locale = parseOptionalValues("locale", rawScope.locale);
1741
- const framework = parseOptionalValues("framework", rawScope.framework);
1742
- const version = parseOptionalValues("version", rawScope.version);
1743
- const versionGroups = rawScope.versionGroups === void 0 ? {
1744
- valid: true,
1745
- groups: void 0
1746
- } : parseRetrievalSourceVersionGroups(rawScope.versionGroups);
1747
- const packageNames = parseOptionalValues("package", rawScope.package);
1748
- const tags = parseOptionalValues("tags", rawScope.tags);
1749
- if (!conflicts.valid || !truncated.valid || !locale.valid || !framework.valid || !version.valid || !versionGroups.valid || !packageNames.valid || !tags.valid) return;
1750
- const scope = {
1751
- audience,
1752
- ...locale.values ? { locale: locale.values } : {},
1753
- ...framework.values ? { framework: framework.values } : {},
1754
- ...version.values ? { version: version.values } : {},
1755
- ...versionGroups.groups ? { versionGroups: versionGroups.groups } : {},
1756
- ...packageNames.values ? { package: packageNames.values } : {},
1757
- ...tags.values ? { tags: tags.values } : {},
1758
- ...truncated.values && truncated.values.length > 0 ? { truncated: truncated.values } : {},
1759
- ...conflicts.values && conflicts.values.length > 0 ? { conflicts: conflicts.values } : {}
1760
- };
1761
- const rawLastModified = parseRetrievalSourceString(record.lastModified);
1762
- if (record.lastModified !== void 0 && (!rawLastModified || !Number.isFinite(Date.parse(rawLastModified)))) return;
1763
- const lastModified = rawLastModified && Number.isFinite(Date.parse(rawLastModified)) ? rawLastModified : void 0;
1764
- return {
1765
- canonicalUrl,
1766
- scope,
1767
- ...lastModified ? { lastModified } : {},
1768
- digest,
1769
- indexGeneration
1770
- };
1771
- }
1772
- function docsRetrievalSourceMatchesRequest(source, audience, filters, locale) {
1773
- if (source.scope.audience !== audience) return false;
1774
- for (const field of SEARCH_FILTER_FIELDS) {
1775
- const requested = filters?.[field];
1776
- if (!requested?.length) continue;
1777
- if (source.scope.conflicts?.includes(field)) return false;
1778
- if (source.scope.truncated?.includes(field)) return false;
1779
- if (field === "version" && source.scope.versionGroups?.length) {
1780
- if (!agentVersionConstraintGroupsOverlap([requested, ...source.scope.versionGroups])) return false;
1781
- continue;
1782
- }
1783
- const candidates = source.scope[field];
1784
- if (!candidates?.length) return false;
1785
- if (!requested.some((requestedValue) => candidates.some((candidate) => docsSearchScopeValueMatches(field, requestedValue, candidate)))) return false;
1786
- }
1787
- const requestedLocale = locale ? normalizeAgentLocale(locale) : void 0;
1788
- if (requestedLocale && (!source.scope.locale?.length || !source.scope.locale.some((candidate) => normalizeAgentLocale(candidate) === requestedLocale))) return false;
1789
- return true;
1790
- }
1791
- const HOSTED_RETRIEVAL_SOURCE_FIELDS = [
1792
- "source_canonical_url",
1793
- "source_scope_audience",
1794
- "source_scope_locale",
1795
- "source_scope_framework",
1796
- "source_scope_version",
1797
- "source_scope_version_groups",
1798
- "source_scope_package",
1799
- "source_scope_tags",
1800
- "source_scope_truncated",
1801
- "source_scope_conflicts",
1802
- "source_last_modified",
1803
- "source_digest",
1804
- "source_index_generation"
1805
- ];
1806
- function readFlattenedHostedRetrievalSource(record) {
1807
- return parseDocsRetrievalSource({
1808
- canonicalUrl: record.source_canonical_url,
1809
- scope: {
1810
- audience: record.source_scope_audience,
1811
- locale: record.source_scope_locale,
1812
- framework: record.source_scope_framework,
1813
- version: record.source_scope_version,
1814
- versionGroups: (() => {
1815
- const value = record.source_scope_version_groups;
1816
- if (typeof value !== "string") return value;
1817
- try {
1818
- return JSON.parse(value);
1819
- } catch {
1820
- return null;
1821
- }
1822
- })(),
1823
- package: record.source_scope_package,
1824
- tags: record.source_scope_tags,
1825
- truncated: record.source_scope_truncated,
1826
- conflicts: record.source_scope_conflicts
1827
- },
1828
- lastModified: record.source_last_modified,
1829
- digest: record.source_digest,
1830
- indexGeneration: record.source_index_generation
1831
- }, { allowRootRelativeCanonical: true });
1832
- }
1833
- function readHostedRetrievalSource(record) {
1834
- const hasNested = record.source !== void 0;
1835
- const hasFlattened = HOSTED_RETRIEVAL_SOURCE_FIELDS.some((key) => key in record);
1836
- if (hasNested) {
1837
- const nested = parseDocsRetrievalSource(record.source, { allowRootRelativeCanonical: true });
1838
- if (!nested) return void 0;
1839
- if (!hasFlattened) return nested;
1840
- const flattened = readFlattenedHostedRetrievalSource(record);
1841
- return flattened && JSON.stringify(flattened) === JSON.stringify(nested) ? nested : void 0;
1842
- }
1843
- return readFlattenedHostedRetrievalSource(record);
1844
- }
1845
- function hasHostedRetrievalSource(record) {
1846
- return record.source !== void 0 || [
1847
- "source_corpus_id",
1848
- "source_document_id",
1849
- ...HOSTED_RETRIEVAL_SOURCE_FIELDS
1850
- ].some((key) => key in record);
1851
- }
1852
- function serializeHostedRetrievalSource(source, corpusId, sourceDocumentId) {
1853
- if (!source) return {};
1854
- return {
1855
- source_corpus_id: corpusId,
1856
- source_document_id: sourceDocumentId,
1857
- source_canonical_url: source.canonicalUrl,
1858
- source_scope_audience: source.scope.audience,
1859
- source_scope_locale: source.scope.locale,
1860
- source_scope_framework: source.scope.framework,
1861
- source_scope_version: source.scope.version,
1862
- source_scope_version_groups: source.scope.versionGroups ? JSON.stringify(source.scope.versionGroups) : void 0,
1863
- source_scope_package: source.scope.package,
1864
- source_scope_tags: source.scope.tags,
1865
- source_scope_truncated: source.scope.truncated,
1866
- source_scope_conflicts: source.scope.conflicts,
1867
- source_last_modified: source.lastModified,
1868
- source_digest: source.digest,
1869
- source_index_generation: source.indexGeneration
1870
- };
1871
- }
1872
- function resolveHostedCorpusId(syncNamespace, context) {
1873
- const explicitNamespace = syncNamespace?.trim();
1874
- if (explicitNamespace && explicitNamespace.length > 1024) throw new Error("Search syncNamespace must be 1024 characters or fewer.");
1875
- let canonicalIdentity;
1876
- const indexBaseUrl = context.indexBaseUrl;
1877
- if (!explicitNamespace && indexBaseUrl) try {
1878
- const url = new URL(indexBaseUrl);
1879
- if ((url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password) canonicalIdentity = `${url.origin}${url.pathname.replace(/\/+$/u, "") || "/"}`;
1880
- } catch {}
1881
- const identity = explicitNamespace ? `namespace:${explicitNamespace}` : canonicalIdentity ? `canonical:${canonicalIdentity}` : void 0;
1882
- if (!identity) return void 0;
1883
- return hashDocsRetrievalValue(JSON.stringify({
1884
- format: "docs-hosted-corpus.v1",
1885
- identity,
1886
- audience: resolveDocsSearchAudience(context.audience),
1887
- locale: context.locale ? normalizeAgentLocale(context.locale) : "__all__"
1888
- }));
1889
- }
1890
- function resolveHostedHumanCorpusId(syncNamespace, context) {
1891
- return resolveHostedCorpusId(syncNamespace, {
1892
- audience: "human",
1893
- locale: context.locale,
1894
- baseUrl: context.baseUrl,
1895
- indexBaseUrl: context.indexBaseUrl
1896
- });
1897
- }
1898
- function makeHostedProviderDocumentId(corpusId, sourceDocumentId) {
1899
- return `docs_${hashDocsRetrievalValue(JSON.stringify({
1900
- format: "docs-hosted-document.v1",
1901
- corpusId,
1902
- sourceDocumentId
1903
- })).slice(7)}`;
1904
- }
1905
- function readHostedSourceDocumentId(record) {
1906
- return parseRetrievalSourceString(record.source_document_id, MAX_RETRIEVAL_SOURCE_URL_CHARS);
1907
- }
1908
- function hostedRecordMatchesCorpus(record, corpusId) {
1909
- if (!corpusId) return true;
1910
- if (record.source_corpus_id === corpusId) return true;
1911
- return record.source_corpus_id === void 0 && !hasHostedRetrievalSource(record);
1912
- }
1913
- async function enrichDocsSearchResultsWithSources(options) {
1914
- const digestCache = /* @__PURE__ */ new Map();
1915
- const localizedPages = options.pages.map((page) => localizeDocsSearchPage(page, options.locale));
1916
- const pagesByRoute = /* @__PURE__ */ new Map();
1917
- for (const page of localizedPages) {
1918
- const route = normalizeUrlRouteKey(page.url);
1919
- if (!pagesByRoute.has(route)) pagesByRoute.set(route, page);
1920
- }
1921
- let resolvedIndexGeneration;
1922
- const getIndexGeneration = () => {
1923
- resolvedIndexGeneration ??= options.indexGeneration ? Promise.resolve(options.indexGeneration) : buildDocsSearchIndexGeneration(options.generationPages ?? options.pages, {
1924
- audience: options.audience,
1925
- chunking: options.chunking,
1926
- locale: options.locale,
1927
- baseUrl: options.baseUrl,
1928
- digestCache
1929
- });
1930
- return resolvedIndexGeneration;
1931
- };
1932
- return (await Promise.all(options.results.map(async (result) => {
1933
- const { source: rawSource, ...resultWithoutSource } = result;
1934
- const page = findPageForSearchResult(localizedPages, result, options.baseUrl, pagesByRoute);
1935
- if (page) return {
1936
- ...resultWithoutSource,
1937
- source: await buildDocsRetrievalSource(page, result.url, {
1938
- audience: options.audience,
1939
- chunking: options.chunking,
1940
- locale: options.locale,
1941
- baseUrl: options.baseUrl,
1942
- indexGeneration: await getIndexGeneration(),
1943
- digestCache
1944
- })
1945
- };
1946
- if (rawSource === void 0) return resultWithoutSource;
1947
- const source = parseDocsRetrievalSource(rawSource, { allowRootRelativeCanonical: options.requireCurrentIndexGeneration });
1948
- if (!source) return null;
1949
- if (!docsRetrievalSourceMatchesRequest(source, options.audience, options.filters, options.locale)) return null;
1950
- if (options.requireCurrentIndexGeneration && source.indexGeneration !== await getIndexGeneration()) return null;
1951
- return {
1952
- ...resultWithoutSource,
1953
- source
1954
- };
1955
- }))).filter((result) => Boolean(result));
1956
- }
1957
- async function enrichDocsSearchDocumentsWithSources(options) {
1958
- const results = await enrichDocsSearchResultsWithSources({
1959
- results: options.documents.map((document) => ({
1960
- id: document.id,
1961
- url: document.url,
1962
- content: document.title,
1963
- description: document.description,
1964
- type: document.type,
1965
- section: document.section,
1966
- source: document.source
1967
- })),
1968
- pages: options.pages,
1969
- audience: options.audience,
1970
- chunking: options.chunking,
1971
- locale: options.locale,
1972
- baseUrl: options.baseUrl,
1973
- indexGeneration: options.indexGeneration
1974
- });
1975
- const sources = new Map(results.map((result) => [result.id, result.source]));
1976
- return options.documents.map((document) => {
1977
- const source = sources.get(document.id);
1978
- return source ? {
1979
- ...document,
1980
- source
1981
- } : document;
1982
- });
1983
- }
1984
- /**
1985
- * Attach the same canonical provenance used by built-in search providers.
1986
- * Custom adapters can call this before persisting their own hosted records.
1987
- */
1988
- function enrichDocsSearchDocumentsWithProvenance(options) {
1989
- return enrichDocsSearchDocumentsWithSources({
1990
- ...options,
1991
- audience: resolveDocsSearchAudience(options.audience),
1992
- chunking: options.chunking ?? { strategy: "section" }
1993
- });
1994
- }
1995
- function isLocalProviderResult(result, baseUrl) {
1996
- const rawUrl = result.url.trim();
1997
- const explicitScheme = rawUrl.match(/^([a-z][a-z\d+.-]*):/iu)?.[1]?.toLowerCase();
1998
- if (explicitScheme && explicitScheme !== "http" && explicitScheme !== "https") return false;
1999
- if (!(/^[a-z][a-z\d+.-]*:/iu.test(rawUrl) || /^[\\/]{2}/u.test(rawUrl))) return true;
2000
- if (!baseUrl) return false;
2001
- try {
2002
- return new URL(result.url, baseUrl).origin === new URL(baseUrl).origin;
2003
- } catch {
2004
- return false;
2005
- }
2006
- }
2007
- function hasOppositeAudienceEvidence(options) {
2008
- const { result, pages, query, audience, baseUrl } = options;
2009
- if (!isLocalProviderResult(result, baseUrl)) return false;
2010
- const pagePath = normalizeUrlRouteKey(result.url);
2011
- const page = pages.find((candidate) => normalizeUrlRouteKey(candidate.url) === pagePath);
2012
- if (!page) return false;
2013
- if (scoreDocument(query, pageToSearchDocument(page, audience)) > 0) return false;
2014
- const oppositeAudience = audience === "agent" ? "human" : "agent";
2015
- if (scoreDocument(query, pageToSearchDocument(page, oppositeAudience)) > 0) return true;
2016
- const evidence = cleanSearchResultText(result.description);
2017
- if (!evidence) return false;
2018
- const selectedTokens = new Set(tokenizeSearchQuery(getPageAudienceSearchText(page, audience)));
2019
- const oppositeTokens = new Set(tokenizeSearchQuery(getPageAudienceSearchText(page, oppositeAudience)));
2020
- const evidenceTokens = [...new Set(tokenizeSearchQuery(evidence))];
2021
- let selectedOnlyMatches = 0;
2022
- let oppositeOnlyMatches = 0;
2023
- for (const token of evidenceTokens) {
2024
- const inSelected = selectedTokens.has(token);
2025
- const inOpposite = oppositeTokens.has(token);
2026
- if (inSelected && !inOpposite) selectedOnlyMatches += 1;
2027
- if (inOpposite && !inSelected) oppositeOnlyMatches += 1;
2028
- }
2029
- return oppositeOnlyMatches > selectedOnlyMatches;
2030
- }
2031
- function pageToSearchDocument(rawPage, audience = "human") {
2032
- const page = localizeDocsSearchPage(rawPage, rawPage.locale);
2033
- const scope = resolveDocsSearchPageScope(page);
2034
- return {
2035
- id: makeDocumentId(page.url, "page"),
2036
- url: page.url,
2037
- title: page.title,
2038
- content: getPageAudienceIndexContent(page, audience),
2039
- description: page.description,
2040
- type: "page",
2041
- locale: page.locale,
2042
- framework: scope.framework[0],
2043
- version: scope.version[0],
2044
- package: scope.package.length > 0 ? scope.package : void 0,
2045
- tags: scope.tags.length > 0 ? scope.tags : void 0
2046
- };
2047
- }
2048
- function buildExactPageSearchResults(query, pages, audience = "human") {
2049
- const normalizedQuery = normalizeSearchPhrase(query);
2050
- if (!normalizedQuery) return [];
2051
- const results = [];
2052
- for (const page of pages) {
2053
- const document = pageToSearchDocument(page, audience);
2054
- const title = normalizeSearchPhrase(page.title);
2055
- const urlSegments = getUrlSearchSegments(page.url);
2056
- if (!(title === normalizedQuery || urlSegments.includes(normalizedQuery))) continue;
2057
- results.push({
2058
- id: document.id,
2059
- url: document.url,
2060
- content: cleanSearchResultText(document.title) ?? document.title,
2061
- description: cleanSearchResultText(buildSnippet(document, query) ?? document.description),
2062
- type: "page",
2063
- score: scoreDocument(query, document) + 2e3
2064
- });
2065
- }
2066
- return results.sort((a, b) => (b.score ?? 0) - (a.score ?? 0) || a.url.localeCompare(b.url));
2067
- }
2068
- function hasDistinctResultSection(result) {
2069
- if (result.type === "page") return false;
2070
- const section = normalizeSearchPhrase(stripHtml(result.section ?? ""));
2071
- if (!section) return true;
2072
- return section !== normalizeSearchPhrase(normalizeSearchPhrase(stripHtml(result.content)).split(/\s+[—–]\s+/)[0] ?? "");
2073
- }
2074
- function insideLiteralResultPriority(query, result) {
2075
- if (!hasDistinctResultSection(result) || !isLiteralLookupQuery(query)) return 0;
2076
- return Math.max(literalMatchPriority(query, stripHtml(result.section ?? "")), literalMatchPriority(query, stripHtml(result.description ?? "")));
2077
- }
2078
- function prioritizeLiteralInsideResults(query, results) {
2079
- if (!isLiteralLookupQuery(query)) return results;
2080
- return [...results].sort((a, b) => {
2081
- const literalDelta = insideLiteralResultPriority(query, b) - insideLiteralResultPriority(query, a);
2082
- if (literalDelta) return literalDelta;
2083
- return 0;
2084
- });
2085
- }
2086
- function rankAskAIContextResult(query, result) {
2087
- return scoreDocument(query, {
2088
- id: result.id,
2089
- url: result.url,
2090
- title: result.title,
2091
- content: result.contextContent,
2092
- description: result.description,
2093
- type: result.type,
2094
- section: result.section
2095
- });
2096
- }
2097
- function buildAskAIContextBlock(result) {
2098
- const lines = [`## ${result.title}`, `URL: ${result.url}`];
2099
- if (result.section) lines.push(`Section: ${result.section}`);
2100
- if (result.description) lines.push(`Search snippet: ${result.description}`);
2101
- if (result.source) {
2102
- const scope = result.source.scope;
2103
- const scopeEntries = [
2104
- `audience=${scope.audience}`,
2105
- scope.locale?.length ? `locale=${scope.locale.join(",")}` : void 0,
2106
- scope.framework?.length ? `framework=${scope.framework.join(",")}` : void 0,
2107
- scope.version?.length ? `version=${scope.version.join(",")}` : void 0,
2108
- scope.package?.length ? `package=${scope.package.join(",")}` : void 0,
2109
- scope.tags?.length ? `tags=${scope.tags.join(",")}` : void 0,
2110
- scope.conflicts?.length ? `conflicts=${scope.conflicts.join(",")}` : void 0
2111
- ].filter((value) => Boolean(value));
2112
- lines.push(`Canonical URL: ${result.source.canonicalUrl}`);
2113
- lines.push(`Source scope: ${scopeEntries.join("; ")}`);
2114
- if (result.source.lastModified) lines.push(`Source modified: ${result.source.lastModified}`);
2115
- lines.push(`Source digest: ${result.source.digest}`);
2116
- lines.push(`Index generation: ${result.source.indexGeneration}`);
2117
- }
2118
- lines.push("", result.contextContent);
2119
- return lines.join("\n").trim();
2120
- }
2121
- function makeDocumentId(url, suffix) {
2122
- return `${url}#${suffix}`;
2123
- }
2124
- function splitPageIntoSections(page, audience = "human") {
2125
- const raw = getPageAudienceSectionContent(page, audience);
2126
- const generatedContract = audience === "agent" ? findDocsGeneratedAgentContractRanges(raw)[0] : void 0;
2127
- const scope = resolveDocsSearchPageScope(page);
2128
- let documentIndex = 0;
2129
- return parseDocsMarkdownSections(raw).flatMap((section) => {
2130
- if (generatedContract && section.startLine > generatedContract.startLine && section.startLine < generatedContract.endLine) return [];
2131
- const content = normalizeWhitespace(stripMarkdownText(section.content));
2132
- if (!content) return [];
2133
- const index = documentIndex;
2134
- documentIndex += 1;
2135
- return [{
2136
- id: makeDocumentId(page.url, `section-${index}`),
2137
- url: `${page.url.split("#", 1)[0]}#${encodeURIComponent(section.anchor)}`,
2138
- title: page.title,
2139
- section: section.title,
2140
- content,
2141
- description: page.description,
2142
- type: "heading",
2143
- locale: page.locale,
2144
- framework: scope.framework[0],
2145
- version: scope.version[0],
2146
- package: scope.package.length > 0 ? scope.package : void 0,
2147
- tags: scope.tags.length > 0 ? scope.tags : void 0
2148
- }];
2149
- });
2150
- }
2151
- function buildDocsSearchDocuments(pages, chunking = {}, audience = "human") {
2152
- const strategy = chunking.strategy ?? "section";
2153
- return pages.flatMap((rawPage) => {
2154
- const page = localizeDocsSearchPage(rawPage, rawPage.locale);
2155
- const base = pageToSearchDocument(page, audience);
2156
- if (strategy === "page") return [base];
2157
- const sections = splitPageIntoSections(page, audience);
2158
- if (sections.length === 0) return [base];
2159
- return [...base.content ? [base] : [], ...sections];
2160
- });
2161
- }
2162
- function scoreDocument(query, document) {
2163
- const q = normalizeSearchPhrase(query);
2164
- if (!q) return 0;
2165
- const words = tokenizeSearchQuery(q);
2166
- const title = normalizeSearchPhrase(document.title);
2167
- const section = document.section ? normalizeSearchPhrase(document.section) : "";
2168
- const hasDistinctSection = Boolean(section && section !== title);
2169
- const titleSection = section ? normalizeSearchPhrase(`${document.title} ${document.section}`) : "";
2170
- const description = document.description ? normalizeSearchPhrase(document.description) : "";
2171
- const content = normalizeSearchPhrase(document.content);
2172
- const url = normalizeSearchPhrase(document.url);
2173
- const urlSegments = getUrlSearchSegments(document.url);
2174
- const titleTokens = tokenizeSearchQuery(title);
2175
- const sectionTokens = tokenizeSearchQuery(section);
2176
- let score = 0;
2177
- const insideLiteralPriority = document.type !== "page" && hasDistinctSection && isLiteralLookupQuery(q) ? Math.max(literalMatchPriority(q, section), literalMatchPriority(q, description), literalMatchPriority(q, content)) : 0;
2178
- if (insideLiteralPriority > 0) score += insideLiteralPriority * 2250;
2179
- if (title === q) score += 1120;
2180
- else if (title.startsWith(q)) score += 70;
2181
- else if (title.includes(q)) score += 45;
2182
- if (hasDistinctSection) {
2183
- if (section === q) score += 1080;
2184
- else if (section.startsWith(q)) score += 55;
2185
- else if (section.includes(q)) score += 30;
2186
- if (titleSection === q) score += 1e3;
2187
- else if (titleSection.startsWith(q)) score += 50;
2188
- else if (titleSection.includes(q)) score += 28;
2189
- }
2190
- if (urlSegments.includes(q)) score += 950;
2191
- if (url.includes(q)) score += 12;
2192
- if (description.includes(q)) score += 18;
2193
- if (content.includes(q)) score += 12;
2194
- let matchedWords = 0;
2195
- for (const word of words) {
2196
- let matched = false;
2197
- if (title === word) {
2198
- score += 28;
2199
- matched = true;
2200
- } else if (title.startsWith(word)) {
2201
- score += 20;
2202
- matched = true;
2203
- } else if (title.includes(word)) {
2204
- score += 12;
2205
- matched = true;
2206
- }
2207
- if (hasDistinctSection) {
2208
- if (section === word) {
2209
- score += 22;
2210
- matched = true;
2211
- } else if (section.startsWith(word)) {
2212
- score += 16;
2213
- matched = true;
2214
- } else if (section.includes(word)) {
2215
- score += 10;
2216
- matched = true;
2217
- }
2218
- }
2219
- if (description.includes(word)) {
2220
- score += 6;
2221
- matched = true;
2222
- }
2223
- if (content.includes(word)) {
2224
- score += 4;
2225
- matched = true;
2226
- }
2227
- if (matched) matchedWords += 1;
2228
- }
2229
- if (words.length > 1) {
2230
- if (hasDistinctSection && sectionTokens.length > 0 && words.every((word) => sectionTokens.includes(word))) score += 30;
2231
- if (document.type === "page" && titleTokens.length > 0 && words.every((word) => titleTokens.includes(word))) score += 24;
2232
- }
2233
- if (matchedWords === words.length && words.length > 1) score += 20;
2234
- if (score > 0 && document.type === "heading" && hasDistinctSection) score += 6;
2235
- return score;
2236
- }
2237
- function buildSnippet(document, query) {
2238
- const q = query.trim().toLowerCase();
2239
- const sources = [normalizeWhitespace(stripMarkdownText(document.content)), normalizeWhitespace(stripMarkdownText(document.description ?? ""))].filter(Boolean);
2240
- for (const source of sources) {
2241
- if (!q) return clampSearchSnippet(source);
2242
- const idx = source.toLowerCase().indexOf(q);
2243
- if (idx === -1) continue;
2244
- const start = Math.max(0, idx - 48);
2245
- const end = Math.min(source.length, idx + q.length + 96);
2246
- const prefix = start > 0 ? "..." : "";
2247
- const suffix = end < source.length ? "..." : "";
2248
- return clampSearchSnippet(`${prefix}${source.slice(start, end).trim()}${suffix}`);
2249
- }
2250
- return sources[0] ? clampSearchSnippet(sources[0]) : void 0;
2251
- }
2252
- function clampSearchSnippet(value) {
2253
- if (value.length <= MAX_SEARCH_SNIPPET_CHARS) return value;
2254
- return `${value.slice(0, MAX_SEARCH_SNIPPET_CHARS - 3).trimEnd()}...`;
2255
- }
2256
- function cleanSearchResultText(value) {
2257
- if (!value) return void 0;
2258
- return normalizeWhitespace(stripHtml(stripMarkdownText(value))) || void 0;
2259
- }
2260
- function buildAlgoliaRecord(document, corpusId) {
2261
- const providerDocumentId = corpusId ? makeHostedProviderDocumentId(corpusId, document.id) : document.id;
2262
- const record = {
2263
- objectID: providerDocumentId,
2264
- id: providerDocumentId,
2265
- url: document.url,
2266
- title: document.title,
2267
- section: document.section,
2268
- content: document.content,
2269
- description: document.description,
2270
- type: document.type,
2271
- locale: document.locale ?? document.source?.scope.locale?.[0],
2272
- framework: document.framework,
2273
- version: document.version,
2274
- package: document.package,
2275
- tags: document.tags,
2276
- _tags: corpusId ? [corpusId] : void 0,
2277
- ...serializeHostedRetrievalSource(document.source, corpusId, document.id)
2278
- };
2279
- const encoder = new TextEncoder();
2280
- const sizeOf = (value) => encoder.encode(JSON.stringify(value)).length;
2281
- if (sizeOf(record) <= ALGOLIA_MAX_RECORD_BYTES) return record;
2282
- delete record.description;
2283
- if (sizeOf(record) <= ALGOLIA_MAX_RECORD_BYTES) return record;
2284
- record.content = "";
2285
- for (const field of [
2286
- "package",
2287
- "tags",
2288
- "framework",
2289
- "version"
2290
- ]) {
2291
- if (sizeOf(record) <= ALGOLIA_MAX_RECORD_BYTES) break;
2292
- delete record[field];
2293
- }
2294
- const trimFieldToFit = (field, value) => {
2295
- let low = 0;
2296
- let high = value.length;
2297
- let best = "";
2298
- while (low <= high) {
2299
- const middle = Math.floor((low + high) / 2);
2300
- const candidate = middle < value.length ? `${value.slice(0, middle).trimEnd()}...` : value;
2301
- record[field] = candidate;
2302
- if (sizeOf(record) <= ALGOLIA_MAX_RECORD_BYTES) {
2303
- best = candidate;
2304
- low = middle + 1;
2305
- } else high = middle - 1;
2306
- }
2307
- record[field] = best;
2308
- };
2309
- if (sizeOf(record) > ALGOLIA_MAX_RECORD_BYTES && typeof record.section === "string") trimFieldToFit("section", record.section);
2310
- if (sizeOf(record) > ALGOLIA_MAX_RECORD_BYTES && typeof record.title === "string") trimFieldToFit("title", record.title);
2311
- if (sizeOf(record) > ALGOLIA_MAX_RECORD_BYTES) throw new Error(`Algolia record ${document.id} exceeds the record limit with required provenance.`);
2312
- trimFieldToFit("content", document.content);
2313
- if (sizeOf(record) > ALGOLIA_MAX_RECORD_BYTES) throw new Error(`Algolia record ${document.id} still exceeds the record limit after trimming.`);
2314
- return record;
2315
- }
2316
- function createSimpleSearchAdapter() {
2317
- return {
2318
- name: "simple",
2319
- async search(query, context) {
2320
- const limit = query.limit ?? DEFAULT_SEARCH_LIMIT;
2321
- const results = context.documents.map((document) => ({
2322
- document,
2323
- score: scoreDocument(query.query, document)
2324
- })).filter((item) => item.score > 0).sort((a, b) => {
2325
- if (b.score !== a.score) return b.score - a.score;
2326
- return a.document.url.localeCompare(b.document.url);
2327
- }).slice(0, limit).map(({ document, score }) => ({
2328
- id: document.id,
2329
- url: document.url,
2330
- content: cleanSearchResultText(document.section ? `${document.title} — ${document.section}` : document.title) ?? (document.section ? `${document.title} — ${document.section}` : document.title),
2331
- description: cleanSearchResultText(buildSnippet(document, query.query) ?? document.description),
2332
- type: document.type,
2333
- score,
2334
- section: document.section,
2335
- source: document.source
2336
- }));
2337
- if (context.deferSourceProvenance) return results;
2338
- return enrichDocsSearchResultsWithSources({
2339
- results,
2340
- pages: context.pages,
2341
- audience: resolveDocsSearchAudience(context.audience),
2342
- chunking: context.chunking ?? { strategy: "section" },
2343
- locale: query.locale ?? context.locale,
2344
- baseUrl: context.baseUrl,
2345
- indexGeneration: context.indexGeneration,
2346
- filters: query.filters
2347
- });
2348
- }
2349
- };
2350
- }
2351
- function normalizeDocsSearchConfig(search) {
2352
- if (search === false) return {
2353
- enabled: false,
2354
- provider: "simple",
2355
- maxResults: DEFAULT_SEARCH_LIMIT,
2356
- chunking: { strategy: "section" }
2357
- };
2358
- if (!search || search === true) return {
2359
- enabled: true,
2360
- provider: "simple",
2361
- maxResults: DEFAULT_SEARCH_LIMIT,
2362
- chunking: { strategy: "section" },
2363
- raw: typeof search === "object" ? search : void 0
2364
- };
2365
- const provider = search.provider ?? "simple";
2366
- const maxResults = search.maxResults ?? DEFAULT_SEARCH_LIMIT;
2367
- const chunking = search.chunking ?? { strategy: "section" };
2368
- return {
2369
- enabled: search.enabled ?? true,
2370
- provider,
2371
- maxResults,
2372
- chunking,
2373
- raw: search
2374
- };
2375
- }
2376
- async function readResponseJson(response) {
2377
- const text = await response.text();
2378
- return text ? JSON.parse(text) : null;
2379
- }
2380
- async function readMcpResponsePayload(response) {
2381
- const text = await response.text();
2382
- if (!text) return null;
2383
- if ((response.headers.get("content-type") ?? "").includes("application/json")) return JSON.parse(text);
2384
- return normalizeMcpSsePayload(text);
2385
- }
2386
- function ensureOk(response, message) {
2387
- if (response.ok) return;
2388
- throw new Error(`${message} (${response.status} ${response.statusText})`);
2389
- }
2390
- function ensureJsonRpcOk(payload, message) {
2391
- if (payload && typeof payload === "object" && "error" in payload && payload.error && typeof payload.error === "object" && "message" in payload.error) throw new Error(`${message}: ${String(payload.error.message)}`);
2392
- }
2393
- function resolveMcpEndpoint(endpoint) {
2394
- if (/^https?:\/\//i.test(endpoint)) return endpoint;
2395
- throw new Error("Relative MCP search endpoints must be resolved before creating the MCP adapter.");
2396
- }
2397
- function isDocsSearchResultType(value) {
2398
- return value === "page" || value === "heading" || value === "text";
2399
- }
2400
- function mapMcpSearchResult(value, sourceBaseUrl) {
2401
- if (!value || typeof value !== "object") return null;
2402
- const item = value;
2403
- const section = typeof item.section === "string" ? item.section : void 0;
2404
- const title = typeof item.title === "string" ? item.title : void 0;
2405
- const content = typeof item.content === "string" ? item.content : title ? section ? `${title} — ${section}` : title : void 0;
2406
- const url = typeof item.url === "string" ? item.url : void 0;
2407
- if (!content || !url) return null;
2408
- const hasSource = item.source !== void 0;
2409
- let sourceValue = item.source;
2410
- if (hasSource && sourceBaseUrl && sourceValue && typeof sourceValue === "object" && !Array.isArray(sourceValue)) {
2411
- const sourceRecord = sourceValue;
2412
- if (typeof sourceRecord.canonicalUrl === "string" && /^\/(?!\/)/u.test(sourceRecord.canonicalUrl)) try {
2413
- sourceValue = {
2414
- ...sourceRecord,
2415
- canonicalUrl: new URL(sourceRecord.canonicalUrl, sourceBaseUrl).toString()
2416
- };
2417
- } catch {}
2418
- }
2419
- const source = hasSource ? parseDocsRetrievalSource(sourceValue) : void 0;
2420
- if (hasSource && !source) return null;
2421
- return {
2422
- id: typeof item.id === "string" ? item.id : typeof item.slug === "string" ? item.slug : url,
2423
- url,
2424
- content: cleanSearchResultText(content) ?? content,
2425
- description: cleanSearchResultText(typeof item.description === "string" ? item.description : typeof item.excerpt === "string" ? item.excerpt : void 0) ?? void 0,
2426
- type: isDocsSearchResultType(item.type) ? item.type : section ? "heading" : "page",
2427
- score: typeof item.score === "number" ? item.score : void 0,
2428
- section,
2429
- ...source ? { source } : {}
2430
- };
2431
- }
2432
- function readMcpSearchToolPayload(payload) {
2433
- if (!payload || typeof payload !== "object" || !("result" in payload) || !payload.result || typeof payload.result !== "object") return null;
2434
- const result = payload.result;
2435
- if (result.structuredContent && typeof result.structuredContent === "object" && !Array.isArray(result.structuredContent)) return result.structuredContent;
2436
- const content = Array.isArray(result.content) ? result.content : [];
2437
- const resultText = content.length > 0 && content[0] && typeof content[0] === "object" && "text" in content[0] && typeof content[0].text === "string" ? content[0].text : null;
2438
- return resultText ? JSON.parse(resultText) : null;
2439
- }
2440
- async function createOllamaEmbedding(text, config, signal) {
2441
- const response = await fetch(`${(config.baseUrl ?? "http://127.0.0.1:11434").replace(/\/$/, "")}/api/embed`, {
2442
- method: "POST",
2443
- headers: { "Content-Type": "application/json" },
2444
- body: JSON.stringify({
2445
- model: config.model,
2446
- input: text
2447
- }),
2448
- signal
2449
- });
2450
- ensureOk(response, "Failed to create Ollama embedding");
2451
- const payload = await readResponseJson(response);
2452
- if (Array.isArray(payload.embeddings?.[0])) return payload.embeddings[0];
2453
- if (Array.isArray(payload.embedding)) return payload.embedding;
2454
- throw new Error("Ollama embedding response did not include an embedding vector.");
2455
- }
2456
- function getTypesenseSearchBase(config) {
2457
- return config.baseUrl.replace(/\/$/, "");
2458
- }
2459
- function quoteTypesenseFilterValue(value) {
2460
- return `\`${value.replace(/\\/gu, "\\\\").replace(/`/gu, "\\`")}\``;
2461
- }
2462
- const TYPESENSE_RETRIEVAL_SOURCE_FIELDS = [
2463
- {
2464
- name: "source_corpus_id",
2465
- type: "string",
2466
- optional: true
2467
- },
2468
- {
2469
- name: "source_document_id",
2470
- type: "string",
2471
- optional: true
2472
- },
2473
- {
2474
- name: "source_canonical_url",
2475
- type: "string",
2476
- optional: true
2477
- },
2478
- {
2479
- name: "source_scope_audience",
2480
- type: "string",
2481
- optional: true
2482
- },
2483
- {
2484
- name: "source_scope_locale",
2485
- type: "string[]",
2486
- optional: true
2487
- },
2488
- {
2489
- name: "source_scope_framework",
2490
- type: "string[]",
2491
- optional: true
2492
- },
2493
- {
2494
- name: "source_scope_version",
2495
- type: "string[]",
2496
- optional: true
2497
- },
2498
- {
2499
- name: "source_scope_version_groups",
2500
- type: "string",
2501
- optional: true
2502
- },
2503
- {
2504
- name: "source_scope_package",
2505
- type: "string[]",
2506
- optional: true
2507
- },
2508
- {
2509
- name: "source_scope_tags",
2510
- type: "string[]",
2511
- optional: true
2512
- },
2513
- {
2514
- name: "source_scope_truncated",
2515
- type: "string[]",
2516
- optional: true
2517
- },
2518
- {
2519
- name: "source_scope_conflicts",
2520
- type: "string[]",
2521
- optional: true
2522
- },
2523
- {
2524
- name: "source_last_modified",
2525
- type: "string",
2526
- optional: true
2527
- },
2528
- {
2529
- name: "source_digest",
2530
- type: "string",
2531
- optional: true
2532
- },
2533
- {
2534
- name: "source_index_generation",
2535
- type: "string",
2536
- optional: true
2537
- }
2538
- ];
2539
- async function ensureTypesenseCollection(config, dimensions, signal, retryAfterCreateConflict = true, retryAfterAlterFailure = true) {
2540
- const baseUrl = getTypesenseSearchBase(config);
2541
- const headers = {
2542
- "X-TYPESENSE-API-KEY": config.adminApiKey ?? config.apiKey,
2543
- "Content-Type": "application/json"
2544
- };
2545
- const existing = await fetch(`${baseUrl}/collections/${encodeURIComponent(config.collection)}`, {
2546
- headers,
2547
- signal
2548
- });
2549
- if (existing.ok) {
2550
- const existingPayload = await readResponseJson(existing);
2551
- if (!Array.isArray(existingPayload?.fields)) throw new Error("Typesense collection response did not include a fields array.");
2552
- const existingFields = new Map(existingPayload.fields.flatMap((field) => typeof field.name === "string" ? [[field.name, field]] : []));
2553
- const incompatibleFields = TYPESENSE_RETRIEVAL_SOURCE_FIELDS.flatMap((expected) => {
2554
- const name = typeof expected.name === "string" ? expected.name : void 0;
2555
- const existingField = name ? existingFields.get(name) : void 0;
2556
- if (!name || !existingField) return [];
2557
- return existingField.type === expected.type && existingField.optional === true ? [] : [name];
2558
- });
2559
- if (incompatibleFields.length > 0) throw new Error(`Typesense collection has incompatible provenance fields: ${incompatibleFields.join(", ")}.`);
2560
- const missingFields = TYPESENSE_RETRIEVAL_SOURCE_FIELDS.filter((field) => typeof field.name === "string" && !existingFields.has(field.name));
2561
- const embeddingField = existingPayload.fields.find((field) => field.name === "embedding");
2562
- const expectedEmbeddingField = config.embeddings && dimensions ? {
2563
- name: "embedding",
2564
- type: "float[]",
2565
- num_dim: dimensions,
2566
- optional: true
2567
- } : void 0;
2568
- const replaceEmbedding = Boolean(expectedEmbeddingField && embeddingField && (embeddingField.type !== "float[]" || embeddingField.num_dim !== dimensions));
2569
- if (replaceEmbedding) ensureOk(await fetch(`${baseUrl}/collections/${encodeURIComponent(config.collection)}`, {
2570
- method: "PATCH",
2571
- headers,
2572
- body: JSON.stringify({ fields: [{
2573
- name: "embedding",
2574
- drop: true
2575
- }] }),
2576
- signal
2577
- }), "Failed to replace an incompatible Typesense embedding field");
2578
- const fieldsToAdd = [...missingFields, ...expectedEmbeddingField && (!embeddingField || replaceEmbedding) ? [expectedEmbeddingField] : []];
2579
- if (fieldsToAdd.length === 0) return;
2580
- const altered = await fetch(`${baseUrl}/collections/${encodeURIComponent(config.collection)}`, {
2581
- method: "PATCH",
2582
- headers,
2583
- body: JSON.stringify({ fields: fieldsToAdd }),
2584
- signal
2585
- });
2586
- if (!altered.ok && retryAfterAlterFailure) {
2587
- await ensureTypesenseCollection(config, dimensions, signal, retryAfterCreateConflict, false);
2588
- return;
2589
- }
2590
- ensureOk(altered, "Failed to update the Typesense search schema");
2591
- return;
2592
- }
2593
- if (existing.status !== 404) ensureOk(existing, "Failed to inspect Typesense collection");
2594
- const fields = [
2595
- {
2596
- name: "id",
2597
- type: "string"
2598
- },
2599
- {
2600
- name: "url",
2601
- type: "string"
2602
- },
2603
- {
2604
- name: "title",
2605
- type: "string"
2606
- },
2607
- {
2608
- name: "section",
2609
- type: "string",
2610
- optional: true
2611
- },
2612
- {
2613
- name: "content",
2614
- type: "string"
2615
- },
2616
- {
2617
- name: "description",
2618
- type: "string",
2619
- optional: true
2620
- },
2621
- {
2622
- name: "type",
2623
- type: "string"
2624
- },
2625
- {
2626
- name: "locale",
2627
- type: "string",
2628
- optional: true
2629
- },
2630
- {
2631
- name: "framework",
2632
- type: "string",
2633
- optional: true
2634
- },
2635
- {
2636
- name: "version",
2637
- type: "string",
2638
- optional: true
2639
- },
2640
- {
2641
- name: "package",
2642
- type: "string[]",
2643
- optional: true
2644
- },
2645
- {
2646
- name: "tags",
2647
- type: "string[]",
2648
- optional: true
2649
- },
2650
- ...TYPESENSE_RETRIEVAL_SOURCE_FIELDS
2651
- ];
2652
- if (config.embeddings && dimensions) fields.push({
2653
- name: "embedding",
2654
- type: "float[]",
2655
- num_dim: dimensions,
2656
- optional: true
2657
- });
2658
- const response = await fetch(`${baseUrl}/collections`, {
2659
- method: "POST",
2660
- headers,
2661
- body: JSON.stringify({
2662
- name: config.collection,
2663
- fields
2664
- }),
2665
- signal
2666
- });
2667
- if (response.status === 409 && retryAfterCreateConflict) {
2668
- await ensureTypesenseCollection(config, dimensions, signal, false, retryAfterAlterFailure);
2669
- return;
2670
- }
2671
- ensureOk(response, "Failed to create Typesense collection");
2672
- }
2673
- async function ensureTypesenseImportSucceeded(response, expectedRecords) {
2674
- ensureOk(response, "Failed to sync documents to Typesense");
2675
- const lines = (await response.text()).split(/\r?\n/u).filter((line) => line.trim());
2676
- if (lines.length !== expectedRecords) throw new Error(`Typesense acknowledged ${lines.length} of ${expectedRecords} imported records.`);
2677
- for (const [index, line] of lines.entries()) {
2678
- let result;
2679
- try {
2680
- result = JSON.parse(line);
2681
- } catch {
2682
- throw new Error(`Typesense import returned invalid JSON on record ${index + 1}.`);
2683
- }
2684
- if (result.success !== true) {
2685
- const detail = typeof result.error === "string" ? `: ${result.error}` : "";
2686
- throw new Error(`Typesense failed to import record ${index + 1}${detail}`);
2687
- }
2688
- }
2689
- }
2690
- function createTypesenseSearchAdapter(config) {
2691
- return {
2692
- name: "typesense",
2693
- async index(context) {
2694
- const adminApiKey = config.adminApiKey ?? config.apiKey;
2695
- const corpusId = resolveHostedHumanCorpusId(config.syncNamespace, context);
2696
- const documents = await enrichDocsSearchDocumentsWithSources({
2697
- documents: context.documents,
2698
- pages: context.pages,
2699
- audience: resolveDocsSearchAudience(context.audience),
2700
- chunking: context.chunking ?? { strategy: "section" },
2701
- locale: context.locale,
2702
- baseUrl: context.baseUrl,
2703
- indexGeneration: context.indexGeneration
2704
- });
2705
- const docsForImport = await Promise.all(documents.map(async (document) => {
2706
- const next = {
2707
- id: corpusId ? makeHostedProviderDocumentId(corpusId, document.id) : document.id,
2708
- url: document.url,
2709
- title: document.title,
2710
- section: document.section,
2711
- content: document.content,
2712
- description: document.description,
2713
- type: document.type,
2714
- locale: document.locale ?? document.source?.scope.locale?.[0],
2715
- framework: document.framework,
2716
- version: document.version,
2717
- package: document.package,
2718
- tags: document.tags,
2719
- ...serializeHostedRetrievalSource(document.source, corpusId, document.id)
2720
- };
2721
- if (config.mode === "hybrid" && config.embeddings) next.embedding = await createOllamaEmbedding(`${document.title}\n${document.section ?? ""}\n${document.content}`.trim(), config.embeddings, context.signal);
2722
- return next;
2723
- }));
2724
- await ensureTypesenseCollection(config, Array.isArray(docsForImport[0]?.embedding) ? docsForImport[0].embedding.length : void 0, context.signal);
2725
- if (docsForImport.length > 0) await ensureTypesenseImportSucceeded(await fetch(`${getTypesenseSearchBase(config)}/collections/${encodeURIComponent(config.collection)}/documents/import?action=upsert`, {
2726
- method: "POST",
2727
- headers: {
2728
- "X-TYPESENSE-API-KEY": adminApiKey,
2729
- "Content-Type": "text/plain"
2730
- },
2731
- body: docsForImport.map((document) => JSON.stringify(document)).join("\n"),
2732
- signal: context.signal
2733
- }), docsForImport.length);
2734
- },
2735
- async search(query, context) {
2736
- const corpusId = resolveHostedHumanCorpusId(config.syncNamespace, context);
2737
- const scopedDocumentIds = resolveProviderScopeDocumentIds(query, context, corpusId);
2738
- if (scopedDocumentIds?.length === 0) return [];
2739
- const params = new URLSearchParams({
2740
- q: query.query,
2741
- query_by: (config.queryBy ?? [
2742
- "title",
2743
- "section",
2744
- "content",
2745
- "description"
2746
- ]).join(","),
2747
- per_page: String(query.limit ?? config.maxResults ?? DEFAULT_SEARCH_LIMIT),
2748
- prioritize_exact_match: "true",
2749
- num_typos: "2",
2750
- highlight_fields: "content,title,section,description"
2751
- });
2752
- if (config.mode === "hybrid" && config.embeddings) {
2753
- const vector = await createOllamaEmbedding(query.query, config.embeddings, context.signal);
2754
- params.set("vector_query", `embedding:([${vector.join(",")}],k:${Math.max((query.limit ?? 10) * 4, 20)})`);
2755
- }
2756
- const filterClauses = [corpusId ? `source_corpus_id:=${quoteTypesenseFilterValue(corpusId)}` : void 0, scopedDocumentIds ? `id:=[${scopedDocumentIds.map(quoteTypesenseFilterValue).join(",")}]` : void 0].filter((value) => Boolean(value));
2757
- const filterBy = filterClauses.length > 0 ? filterClauses.join(" && ") : void 0;
2758
- if (filterBy && filterBy.length > MAX_PROVIDER_SCOPE_FILTER_CHARS) return [];
2759
- const response = filterBy ? await fetch(`${getTypesenseSearchBase(config)}/multi_search`, {
2760
- method: "POST",
2761
- headers: {
2762
- "Content-Type": "application/json",
2763
- "X-TYPESENSE-API-KEY": config.apiKey
2764
- },
2765
- body: JSON.stringify({ searches: [{
2766
- collection: config.collection,
2767
- ...Object.fromEntries(params),
2768
- filter_by: filterBy
2769
- }] }),
2770
- signal: context.signal
2771
- }) : await fetch(`${getTypesenseSearchBase(config)}/collections/${encodeURIComponent(config.collection)}/documents/search?${params.toString()}`, {
2772
- headers: { "X-TYPESENSE-API-KEY": config.apiKey },
2773
- signal: context.signal
2774
- });
2775
- ensureOk(response, "Typesense search failed");
2776
- const rawPayload = await readResponseJson(response);
2777
- return ((filterBy ? rawPayload.results?.[0] ?? {} : rawPayload).hits ?? []).flatMap((hit) => {
2778
- const document = hit.document ?? {};
2779
- if (!hostedRecordMatchesCorpus(document, corpusId)) return [];
2780
- const section = typeof document.section === "string" ? document.section : void 0;
2781
- const content = typeof document.title === "string" ? section ? `${document.title} — ${section}` : document.title : typeof document.content === "string" ? document.content : "Untitled result";
2782
- const description = hit.highlights?.find((item) => item.field === "content")?.snippet ?? hit.highlights?.find((item) => item.field === "description")?.snippet ?? (typeof document.description === "string" ? document.description : void 0);
2783
- const source = readHostedRetrievalSource(document);
2784
- if (hasHostedRetrievalSource(document) && !source) return [];
2785
- return [{
2786
- id: readHostedSourceDocumentId(document) ?? (typeof document.id === "string" ? document.id : String(document.url ?? content)),
2787
- url: typeof document.url === "string" ? document.url : "/docs",
2788
- content: cleanSearchResultText(content) ?? content,
2789
- description: cleanSearchResultText(description),
2790
- type: typeof document.type === "string" && [
2791
- "page",
2792
- "heading",
2793
- "text"
2794
- ].includes(document.type) ? document.type : section ? "heading" : "page",
2795
- score: hit.text_match,
2796
- section,
2797
- ...source ? { source } : {}
2798
- }];
2799
- });
2800
- }
2801
- };
2802
- }
2803
- function resolveSearchRequestConfig(search, requestUrl) {
2804
- if (!search || search === true || typeof search !== "object" || search.provider !== "mcp") return search;
2805
- if (!requestUrl) return search;
2806
- const resolvedEndpoint = new URL(search.endpoint, requestUrl);
2807
- const usesDefaultSearchTool = (search.toolName ?? "search_docs") === "search_docs";
2808
- const isSameOrigin = resolvedEndpoint.origin === new URL(requestUrl).origin;
2809
- return {
2810
- ...search,
2811
- endpoint: resolvedEndpoint.toString(),
2812
- forwardAudience: search.forwardAudience ?? (usesDefaultSearchTool && isSameOrigin)
2813
- };
2814
- }
2815
- /**
2816
- * Resolve the public search audience without allowing malformed values to opt into agent content.
2817
- * Human search remains the default for omitted, legacy, and unknown query values.
2818
- */
2819
- function resolveDocsSearchAudience(value) {
2820
- return value === "agent" ? "agent" : "human";
2821
- }
2822
- function resolveAskAISearchRequestConfig(options) {
2823
- if (!options.useMcp) return resolveSearchRequestConfig(options.search, options.requestUrl);
2824
- if (typeof options.useMcp === "object") return resolveSearchRequestConfig({
2825
- ...options.useMcp,
2826
- provider: "mcp"
2827
- }, options.requestUrl);
2828
- if (options.mcpEnabled === false || options.mcpSearchEnabled === false || !options.mcpEndpoint) return resolveSearchRequestConfig(options.search, options.requestUrl);
2829
- return resolveSearchRequestConfig({
2830
- provider: "mcp",
2831
- endpoint: options.mcpEndpoint
2832
- }, options.requestUrl);
2833
- }
2834
- function createMcpSearchAdapter(config) {
2835
- return {
2836
- name: "mcp",
2837
- async search(query, context) {
2838
- const endpoint = resolveMcpEndpoint(config.endpoint);
2839
- const protocolVersion = config.protocolVersion ?? DEFAULT_MCP_PROTOCOL_VERSION;
2840
- const toolName = config.toolName ?? "search_docs";
2841
- const forwardAudience = config.forwardAudience === true;
2842
- const audience = resolveDocsSearchAudience(query.audience);
2843
- const baseHeaders = config.headers ?? {};
2844
- if (audience === "human" && !forwardAudience) throw new Error("MCP human-projection search requires forwardAudience: true on an audience-aware tool.");
2845
- const initializeResponse = await fetch(endpoint, {
2846
- method: "POST",
2847
- headers: {
2848
- ...baseHeaders,
2849
- "Content-Type": "application/json",
2850
- accept: "application/json, text/event-stream",
2851
- "mcp-protocol-version": protocolVersion
2852
- },
2853
- body: JSON.stringify({
2854
- jsonrpc: "2.0",
2855
- id: 1,
2856
- method: "initialize",
2857
- params: {
2858
- protocolVersion,
2859
- capabilities: {},
2860
- clientInfo: {
2861
- name: "@farming-labs/docs-search",
2862
- version: "0.1.2"
2863
- }
2864
- }
2865
- }),
2866
- signal: context.signal
2867
- });
2868
- const initializePayload = await readMcpResponsePayload(initializeResponse);
2869
- ensureOk(initializeResponse, "MCP search initialization failed");
2870
- ensureJsonRpcOk(initializePayload, "MCP search initialization failed");
2871
- const sessionId = initializeResponse.headers.get("mcp-session-id") ?? void 0;
2872
- try {
2873
- const searchResponse = await fetch(endpoint, {
2874
- method: "POST",
2875
- headers: {
2876
- ...baseHeaders,
2877
- "Content-Type": "application/json",
2878
- accept: "application/json, text/event-stream",
2879
- "mcp-protocol-version": protocolVersion,
2880
- ...sessionId ? { "mcp-session-id": sessionId } : {}
2881
- },
2882
- body: JSON.stringify({
2883
- jsonrpc: "2.0",
2884
- id: 2,
2885
- method: "tools/call",
2886
- params: {
2887
- name: toolName,
2888
- arguments: {
2889
- query: query.query,
2890
- limit: query.limit ?? config.maxResults ?? DEFAULT_SEARCH_LIMIT,
2891
- locale: query.locale,
2892
- ...forwardAudience ? { audience } : {},
2893
- ...query.filters?.framework ? { framework: query.filters.framework } : {},
2894
- ...query.filters?.version ? { version: query.filters.version } : {},
2895
- ...query.filters?.package ? { package: query.filters.package } : {},
2896
- ...query.filters?.tags ? { tags: query.filters.tags } : {}
2897
- }
2898
- }
2899
- }),
2900
- signal: context.signal
2901
- });
2902
- const payload = await readMcpResponsePayload(searchResponse);
2903
- ensureOk(searchResponse, "MCP search request failed");
2904
- ensureJsonRpcOk(payload, "MCP search request failed");
2905
- const parsed = readMcpSearchToolPayload(payload);
2906
- if (!parsed) return [];
2907
- if (hasDocsSearchFilters(query.filters ?? {})) {
2908
- if (Array.isArray(parsed) || parsed.format !== "docs-search.v1") return [];
2909
- const echoedFilters = parseVerifiedMcpSearchFilters(parsed.filters);
2910
- if (!echoedFilters || !docsSearchFiltersMatch(query.filters ?? {}, echoedFilters)) return [];
2911
- }
2912
- return (Array.isArray(parsed) ? parsed : Array.isArray(parsed.results) ? parsed.results : Array.isArray(parsed.pages) ? parsed.pages : []).map((result) => mapMcpSearchResult(result, endpoint)).filter((result) => Boolean(result));
2913
- } finally {
2914
- if (sessionId) {
2915
- const cleanupController = new AbortController();
2916
- const cleanupTimeout = setTimeout(() => cleanupController.abort(), MCP_SESSION_CLEANUP_TIMEOUT_MS);
2917
- try {
2918
- await fetch(endpoint, {
2919
- method: "DELETE",
2920
- headers: {
2921
- ...baseHeaders,
2922
- "mcp-protocol-version": protocolVersion,
2923
- "mcp-session-id": sessionId
2924
- },
2925
- signal: cleanupController.signal
2926
- });
2927
- } catch {} finally {
2928
- clearTimeout(cleanupTimeout);
2929
- }
2930
- }
2931
- }
2932
- }
2933
- };
2934
- }
2935
- function getAlgoliaSearchBase(config) {
2936
- return `https://${config.appId}-dsn.algolia.net`;
2937
- }
2938
- function getAlgoliaAdminBase(config) {
2939
- return `https://${config.appId}.algolia.net`;
2940
- }
2941
- function getAlgoliaAdminHeaders(config) {
2942
- return {
2943
- "Content-Type": "application/json",
2944
- "X-Algolia-Application-Id": config.appId,
2945
- "X-Algolia-API-Key": config.adminApiKey ?? config.searchApiKey
2946
- };
2947
- }
2948
- async function waitForAlgoliaTask(config, taskId, signal) {
2949
- const deadline = Date.now() + 6e4;
2950
- for (let attempt = 0; Date.now() < deadline; attempt += 1) {
2951
- const response = await fetch(`${getAlgoliaAdminBase(config)}/1/indexes/${encodeURIComponent(config.indexName)}/task/${encodeURIComponent(String(taskId))}`, {
2952
- headers: getAlgoliaAdminHeaders(config),
2953
- signal
2954
- });
2955
- ensureOk(response, "Failed to inspect Algolia sync task");
2956
- const payload = await readResponseJson(response);
2957
- if (payload?.status === "published") return;
2958
- if (payload?.status !== "notPublished") throw new Error("Algolia sync task returned an unknown status.");
2959
- const delay = Math.min(100 * 2 ** Math.min(attempt, 4), 2e3);
2960
- await new Promise((resolve, reject) => {
2961
- let timer;
2962
- const onAbort = () => {
2963
- clearTimeout(timer);
2964
- reject(signal?.reason ?? /* @__PURE__ */ new Error("Algolia sync was aborted."));
2965
- };
2966
- timer = setTimeout(() => {
2967
- signal?.removeEventListener("abort", onAbort);
2968
- resolve();
2969
- }, Math.min(delay, Math.max(1, deadline - Date.now())));
2970
- if (signal?.aborted) onAbort();
2971
- else signal?.addEventListener("abort", onAbort, { once: true });
2972
- });
2973
- }
2974
- throw new Error("Timed out waiting for Algolia to publish the synced index.");
2975
- }
2976
- async function executeAlgoliaBatch(config, requests, message, signal) {
2977
- for (let offset = 0; offset < requests.length; offset += ALGOLIA_BATCH_OPERATIONS) {
2978
- const batch = requests.slice(offset, offset + ALGOLIA_BATCH_OPERATIONS);
2979
- const response = await fetch(`${getAlgoliaAdminBase(config)}/1/indexes/${encodeURIComponent(config.indexName)}/batch`, {
2980
- method: "POST",
2981
- headers: getAlgoliaAdminHeaders(config),
2982
- body: JSON.stringify({ requests: batch }),
2983
- signal
2984
- });
2985
- ensureOk(response, message);
2986
- const payload = await readResponseJson(response);
2987
- if (typeof payload?.taskID !== "string" && typeof payload?.taskID !== "number") throw new Error("Algolia sync response did not include a task ID.");
2988
- await waitForAlgoliaTask(config, payload.taskID, signal);
2989
- }
2990
- }
2991
- function quoteAlgoliaFilterValue(value) {
2992
- return `"${value.replace(/\\/gu, "\\\\").replace(/"/gu, "\\\"")}"`;
2993
- }
2994
- function createAlgoliaSearchAdapter(config) {
2995
- return {
2996
- name: "algolia",
2997
- async index(context) {
2998
- if (!config.adminApiKey) return;
2999
- const corpusId = resolveHostedHumanCorpusId(config.syncNamespace, context);
3000
- await executeAlgoliaBatch(config, (await enrichDocsSearchDocumentsWithSources({
3001
- documents: context.documents,
3002
- pages: context.pages,
3003
- audience: resolveDocsSearchAudience(context.audience),
3004
- chunking: context.chunking ?? { strategy: "section" },
3005
- locale: context.locale,
3006
- baseUrl: context.baseUrl,
3007
- indexGeneration: context.indexGeneration
3008
- })).map((document) => ({
3009
- action: "addObject",
3010
- body: buildAlgoliaRecord(document, corpusId)
3011
- })), "Failed to sync documents to Algolia", context.signal);
3012
- },
3013
- async search(query, context) {
3014
- const corpusId = resolveHostedHumanCorpusId(config.syncNamespace, context);
3015
- const scopedDocumentIds = resolveProviderScopeDocumentIds(query, context, corpusId);
3016
- if (scopedDocumentIds?.length === 0) return [];
3017
- const scopedIdsFilter = scopedDocumentIds?.map((id) => `objectID:${quoteAlgoliaFilterValue(id)}`).join(" OR ");
3018
- const filterClauses = [corpusId ? `_tags:${quoteAlgoliaFilterValue(corpusId)}` : void 0, scopedIdsFilter ? corpusId ? `(${scopedIdsFilter})` : scopedIdsFilter : void 0].filter((value) => Boolean(value));
3019
- const filters = filterClauses.length > 0 ? filterClauses.join(" AND ") : void 0;
3020
- if (filters && filters.length > MAX_PROVIDER_SCOPE_FILTER_CHARS) return [];
3021
- const response = await fetch(`${getAlgoliaSearchBase(config)}/1/indexes/${encodeURIComponent(config.indexName)}/query`, {
3022
- method: "POST",
3023
- headers: {
3024
- "Content-Type": "application/json",
3025
- "X-Algolia-Application-Id": config.appId,
3026
- "X-Algolia-API-Key": config.searchApiKey
3027
- },
3028
- body: JSON.stringify({
3029
- query: query.query,
3030
- hitsPerPage: query.limit ?? config.maxResults ?? DEFAULT_SEARCH_LIMIT,
3031
- restrictSearchableAttributes: [
3032
- "title",
3033
- "section",
3034
- "content",
3035
- "description"
3036
- ],
3037
- attributesToSnippet: ["content:20"],
3038
- ...filters ? { filters } : {}
3039
- }),
3040
- signal: context.signal
3041
- });
3042
- ensureOk(response, "Algolia search failed");
3043
- return ((await readResponseJson(response)).hits ?? []).flatMap((hit) => {
3044
- if (!hostedRecordMatchesCorpus(hit, corpusId)) return [];
3045
- const title = typeof hit.title === "string" ? hit.title : "Untitled result";
3046
- const section = typeof hit.section === "string" ? hit.section : void 0;
3047
- const source = readHostedRetrievalSource(hit);
3048
- if (hasHostedRetrievalSource(hit) && !source) return [];
3049
- return [{
3050
- id: readHostedSourceDocumentId(hit) ?? hit.objectID ?? String(hit.url ?? title),
3051
- url: typeof hit.url === "string" ? hit.url : "/docs",
3052
- content: cleanSearchResultText(section ? `${title} — ${section}` : title) ?? title,
3053
- description: cleanSearchResultText(hit._snippetResult?.content?.value ?? hit._snippetResult?.description?.value ?? (typeof hit.description === "string" ? hit.description : void 0)),
3054
- type: typeof hit.type === "string" && [
3055
- "page",
3056
- "heading",
3057
- "text"
3058
- ].includes(hit.type) ? hit.type : section ? "heading" : "page",
3059
- score: hit._rankingInfo?.nbTypos != null ? 100 - hit._rankingInfo.nbTypos : void 0,
3060
- section,
3061
- ...source ? { source } : {}
3062
- }];
3063
- });
3064
- }
3065
- };
3066
- }
3067
- async function resolveSearchAdapter(search, context) {
3068
- const raw = search.raw;
3069
- if (search.provider === "custom" && raw?.provider === "custom") return typeof raw.adapter === "function" ? await raw.adapter(context) : raw.adapter;
3070
- if (search.provider === "typesense" && raw?.provider === "typesense") return createTypesenseSearchAdapter(raw);
3071
- if (search.provider === "mcp" && raw?.provider === "mcp") return createMcpSearchAdapter(raw);
3072
- if (search.provider === "algolia" && raw?.provider === "algolia") return createAlgoliaSearchAdapter(raw);
3073
- return createSimpleSearchAdapter();
3074
- }
3075
- function shouldSyncOnSearch(search) {
3076
- const raw = search.raw;
3077
- if (search.provider === "algolia" && raw?.provider === "algolia") return (raw.syncOnSearch ?? Boolean(raw.adminApiKey)) && Boolean(raw.adminApiKey);
3078
- if (search.provider === "typesense" && raw?.provider === "typesense") return (raw.syncOnSearch ?? Boolean(raw.adminApiKey)) && Boolean(raw.adminApiKey);
3079
- return false;
3080
- }
3081
- function getSyncKey(search, context) {
3082
- const raw = search.raw;
3083
- if (search.provider === "algolia" && raw?.provider === "algolia") {
3084
- const corpusId = resolveHostedHumanCorpusId(raw.syncNamespace, context) ?? "__unowned__";
3085
- return `algolia:${raw.appId}:${raw.indexName}:${corpusId}:${context.locale ?? "__default__"}`;
3086
- }
3087
- if (search.provider === "typesense" && raw?.provider === "typesense") {
3088
- const corpusId = resolveHostedHumanCorpusId(raw.syncNamespace, context) ?? "__unowned__";
3089
- return `typesense:${raw.baseUrl}:${raw.collection}:${corpusId}:${context.locale ?? "__default__"}`;
3090
- }
3091
- if (search.provider === "mcp" && raw?.provider === "mcp") return `mcp:${raw.endpoint}:${context.locale ?? "__default__"}`;
3092
- return `${search.provider}:${context.locale ?? "__default__"}`;
3093
- }
3094
- function getSearchSyncFingerprint(search, context, indexGeneration) {
3095
- const raw = search.raw;
3096
- const providerConfig = search.provider === "typesense" && raw?.provider === "typesense" ? {
3097
- syncNamespace: raw.syncNamespace?.trim() || void 0,
3098
- mode: raw.mode ?? "keyword",
3099
- embeddings: raw.embeddings ? {
3100
- provider: raw.embeddings.provider,
3101
- model: raw.embeddings.model,
3102
- baseUrl: raw.embeddings.baseUrl
3103
- } : void 0
3104
- } : search.provider === "algolia" && raw?.provider === "algolia" ? { syncNamespace: raw.syncNamespace?.trim() || void 0 } : void 0;
3105
- return hashDocsRetrievalValue(JSON.stringify({
3106
- format: "docs-search-sync.v1",
3107
- indexGeneration,
3108
- canonicalBaseUrl: context.baseUrl,
3109
- provider: search.provider,
3110
- providerConfig
3111
- }));
3112
- }
3113
- async function maybeSyncSearchIndex(adapter, search, context) {
3114
- if (!shouldSyncOnSearch(search) || typeof adapter.index !== "function") return;
3115
- const syncKey = getSyncKey(search, context);
3116
- const indexGeneration = context.indexGeneration ?? await buildDocsSearchIndexGeneration(context.pages, {
3117
- audience: resolveDocsSearchAudience(context.audience),
3118
- chunking: context.chunking ?? search.chunking,
3119
- locale: context.locale,
3120
- baseUrl: context.baseUrl
3121
- });
3122
- context.indexGeneration = indexGeneration;
3123
- const syncFingerprint = getSearchSyncFingerprint(search, context, indexGeneration);
3124
- while (true) {
3125
- const inFlight = syncingIndexes.get(syncKey);
3126
- if (inFlight) {
3127
- try {
3128
- await inFlight.promise;
3129
- } catch (error) {
3130
- if (inFlight.fingerprint === syncFingerprint) throw error;
3131
- }
3132
- continue;
3133
- }
3134
- if (syncedIndexes.get(syncKey) === syncFingerprint) return;
3135
- const sync = adapter.index(context).then(() => {
3136
- syncedIndexes.set(syncKey, syncFingerprint);
3137
- });
3138
- syncingIndexes.set(syncKey, {
3139
- fingerprint: syncFingerprint,
3140
- promise: sync
3141
- });
3142
- try {
3143
- await sync;
3144
- } finally {
3145
- if (syncingIndexes.get(syncKey)?.promise === sync) syncingIndexes.delete(syncKey);
3146
- }
3147
- }
3148
- }
3149
- async function performDocsSearch(options) {
3150
- const search = normalizeDocsSearchConfig(options.search);
3151
- if (!search.enabled) return [];
3152
- const audience = resolveDocsSearchAudience(options.audience);
3153
- const filters = normalizeDocsSearchFilters(options.filters);
3154
- const hasFilters = hasDocsSearchFilters(filters);
3155
- const corpusPages = options.pages.map((page) => localizeDocsSearchPage(page, options.locale));
3156
- const indexBaseUrl = options.syncBaseUrl === null ? void 0 : options.syncBaseUrl ?? options.baseUrl;
3157
- const scopedPages = hasFilters ? corpusPages.filter((page) => docsSearchPageMatchesFilters(resolveDocsSearchPageScope(page), filters)) : corpusPages;
3158
- let resolvedDocuments;
3159
- const getDocuments = () => {
3160
- if (!resolvedDocuments) resolvedDocuments = buildDocsSearchDocuments(scopedPages, search.chunking, audience);
3161
- return resolvedDocuments;
3162
- };
3163
- const context = {
3164
- pages: scopedPages,
3165
- get documents() {
3166
- return getDocuments();
3167
- },
3168
- set documents(documents) {
3169
- resolvedDocuments = documents;
3170
- },
3171
- audience,
3172
- locale: options.locale,
3173
- pathname: options.pathname,
3174
- siteTitle: options.siteTitle,
3175
- baseUrl: options.baseUrl,
3176
- indexBaseUrl,
3177
- chunking: search.chunking,
3178
- deferSourceProvenance: true,
3179
- signal: options.signal
3180
- };
3181
- const query = {
3182
- query: options.query,
3183
- limit: options.limit ?? search.maxResults,
3184
- locale: options.locale,
3185
- pathname: options.pathname,
3186
- audience,
3187
- ...hasFilters ? { filters } : {}
3188
- };
3189
- let currentIndexGeneration;
3190
- const getCurrentIndexGeneration = () => {
3191
- currentIndexGeneration ??= options.indexGeneration ? Promise.resolve(options.indexGeneration) : buildDocsSearchIndexGeneration(options.generationPages ?? options.pages, {
3192
- audience,
3193
- chunking: search.chunking,
3194
- locale: options.locale,
3195
- baseUrl: options.baseUrl
3196
- });
3197
- return currentIndexGeneration;
3198
- };
3199
- const requireCurrentIndexGeneration = search.provider === "algolia" || search.provider === "typesense";
3200
- const finalizeResults = async (results) => enrichDocsSearchResultsWithSources({
3201
- results,
3202
- pages: scopedPages,
3203
- generationPages: options.generationPages ?? options.pages,
3204
- audience,
3205
- chunking: search.chunking,
3206
- locale: options.locale,
3207
- baseUrl: options.baseUrl,
3208
- indexGeneration: options.indexGeneration,
3209
- strictExternalOrigins: options.strictExternalOrigins,
3210
- filters,
3211
- requireCurrentIndexGeneration
3212
- });
3213
- try {
3214
- const adapter = await resolveSearchAdapter(search, context);
3215
- if (shouldSyncOnSearch(search) && typeof adapter.index === "function") {
3216
- const syncPages = audience === "agent" || hasFilters ? corpusPages : scopedPages;
3217
- await maybeSyncSearchIndex(adapter, search, {
3218
- pages: syncPages,
3219
- documents: buildDocsSearchDocuments(syncPages, search.chunking, "human"),
3220
- audience: "human",
3221
- locale: options.locale,
3222
- pathname: options.pathname,
3223
- siteTitle: options.siteTitle,
3224
- baseUrl: indexBaseUrl,
3225
- indexBaseUrl,
3226
- chunking: search.chunking,
3227
- signal: options.signal
3228
- });
3229
- }
3230
- const adapterSearch = adapter.search(query, context);
3231
- let documents;
3232
- try {
3233
- documents = getDocuments();
3234
- } catch (error) {
3235
- adapterSearch.catch(() => void 0);
3236
- throw error;
3237
- }
3238
- const results = await adapterSearch;
3239
- if (search.provider === "simple") return finalizeResults(results);
3240
- const localAudienceProjectionResults = buildAudienceProjectionSearchResults(documents, options.query);
3241
- const localPagePaths = new Set(scopedPages.map((page) => normalizeUrlRouteKey(page.url)));
3242
- const allLocalPagePaths = hasFilters && search.provider === "mcp" ? new Set(options.pages.map((page) => normalizeUrlRouteKey(page.url))) : localPagePaths;
3243
- const preserveUnmatched = !hasFilters && audience === "human" || search.provider === "mcp" ? (result) => shouldPreserveUnmatchedExternalResult({
3244
- result,
3245
- localPagePaths: allLocalPagePaths,
3246
- baseUrl: options.baseUrl
3247
- }) : void 0;
3248
- const isKnownLocalProviderResult = (result) => isLocalProviderResult(result, options.baseUrl) && localPagePaths.has(normalizeUrlRouteKey(result.url));
3249
- const expectedProviderGeneration = requireCurrentIndexGeneration ? audience === "human" ? await getCurrentIndexGeneration() : await buildDocsSearchIndexGeneration(options.generationPages ?? options.pages, {
3250
- audience: "human",
3251
- chunking: search.chunking,
3252
- locale: options.locale,
3253
- baseUrl: indexBaseUrl
3254
- }) : void 0;
3255
- const providerAudience = requireCurrentIndexGeneration ? "human" : audience;
3256
- const safeAdapterResults = sanitizeExternalAudienceSearchResults(results.filter((result) => {
3257
- if (result.source === void 0) return !requireCurrentIndexGeneration || isKnownLocalProviderResult(result);
3258
- const source = parseDocsRetrievalSource(result.source, { allowRootRelativeCanonical: requireCurrentIndexGeneration || isKnownLocalProviderResult(result) });
3259
- const knownLocal = isKnownLocalProviderResult(result);
3260
- return Boolean(source && docsRetrievalSourceMatchesRequest(source, providerAudience, knownLocal ? void 0 : filters, options.locale) && (!expectedProviderGeneration || source.indexGeneration === expectedProviderGeneration));
3261
- }).filter((result) => requireCurrentIndexGeneration && isKnownLocalProviderResult(result) || !hasOppositeAudienceEvidence({
3262
- result,
3263
- pages: scopedPages,
3264
- query: options.query,
3265
- audience,
3266
- baseUrl: options.baseUrl
3267
- })), localAudienceProjectionResults, options.baseUrl, preserveUnmatched, requireCurrentIndexGeneration && audience === "agent");
3268
- if (options.supplementExternalResults === false) return finalizeResults(safeAdapterResults.slice(0, query.limit ?? search.maxResults ?? DEFAULT_SEARCH_LIMIT));
3269
- const simpleAudienceResults = audience === "agent" || results.length === 0 || safeAdapterResults.length < results.length ? await createSimpleSearchAdapter().search(query, context) : [];
3270
- const combinedResults = mergeSearchResults([
3271
- buildExactPageSearchResults(options.query, scopedPages, audience),
3272
- safeAdapterResults,
3273
- simpleAudienceResults
3274
- ], audience === "agent" ? (result) => getAskAIResultKey(result, options.baseUrl, options.strictExternalOrigins) : void 0);
3275
- return finalizeResults(prioritizeLiteralInsideResults(options.query, combinedResults).slice(0, query.limit ?? search.maxResults ?? DEFAULT_SEARCH_LIMIT));
3276
- } catch (error) {
3277
- if (options.failureMode === "throw") throw error;
3278
- return finalizeResults(await createSimpleSearchAdapter().search(query, context));
3279
- }
3280
- }
3281
- function compareSearchMetadataValues(left, right) {
3282
- if (left === right) return 0;
3283
- return left < right ? -1 : 1;
3284
- }
3285
- function boundedSearchWarningValues(values) {
3286
- const bounded = Array.from(new Set(values)).sort(compareSearchMetadataValues).slice(0, MAX_SEARCH_WARNING_VALUES);
3287
- return bounded.length > 0 ? bounded : void 0;
3288
- }
3289
- function boundedSearchWarningPageUrls(pages) {
3290
- const bounded = Array.from(new Set(pages.map((page) => page.url))).sort(compareSearchMetadataValues).slice(0, MAX_SEARCH_WARNING_PAGE_URLS);
3291
- return bounded.length > 0 ? bounded : void 0;
3292
- }
3293
- function findSearchResultPages(pages, results, baseUrl) {
3294
- const pagesByPath = new Map(pages.map((page) => [normalizeUrlRouteKey(page.url), page]));
3295
- const found = /* @__PURE__ */ new Map();
3296
- for (const result of results) {
3297
- if (!isLocalProviderResult(result, baseUrl)) continue;
3298
- const page = pagesByPath.get(normalizeUrlRouteKey(result.url));
3299
- if (page) found.set(page.url, page);
3300
- }
3301
- return [...found.values()].sort((left, right) => compareSearchMetadataValues(left.url, right.url));
3302
- }
3303
- function searchScopeIsAmbiguous(field, values) {
3304
- if (values.length < 2) return false;
3305
- if (field !== "version") return true;
3306
- return values.some((left, index) => values.slice(index + 1).some((right) => !agentVersionConstraintsOverlap(left, right)));
3307
- }
3308
- function buildDocsSearchWarnings(options) {
3309
- const pageScopes = options.pages.map((page) => ({
3310
- page,
3311
- scope: resolveDocsSearchPageScope(page)
3312
- }));
3313
- const relevantPageScopes = pageScopes.filter(({ page }) => scoreDocument(options.query, pageToSearchDocument(page, options.audience)) > 0);
3314
- const warnings = [];
3315
- for (const field of SEARCH_FILTER_FIELDS) {
3316
- const conflicting = relevantPageScopes.filter(({ scope }) => scope.conflicts.includes(field));
3317
- if (conflicting.length > 0) warnings.push({
3318
- code: "conflicting_scope_metadata",
3319
- field,
3320
- message: `${conflicting.length} page${conflicting.length === 1 ? "" : "s"} declare conflicting ${field} metadata and cannot be selected safely by scoped search.`,
3321
- pageUrls: boundedSearchWarningPageUrls(conflicting.map(({ page }) => page)),
3322
- count: conflicting.length
3323
- });
3324
- const requested = options.filters[field];
3325
- if (!requested || requested.length === 0) continue;
3326
- const missing = relevantPageScopes.filter(({ scope }) => scope.declarations[field].length === 0);
3327
- if (missing.length > 0) warnings.push({
3328
- code: "missing_scope_metadata",
3329
- field,
3330
- message: `${missing.length} page${missing.length === 1 ? "" : "s"} lack ${field} metadata and were excluded by the strict scope filter.`,
3331
- pageUrls: boundedSearchWarningPageUrls(missing.map(({ page }) => page)),
3332
- count: missing.length
3333
- });
3334
- const unknown = requested.filter((value) => !pageScopes.some(({ scope }) => !scope.conflicts.includes(field) && docsSearchScopeFieldMatches(scope, field, [value])));
3335
- if (unknown.length > 0) warnings.push({
3336
- code: "unknown_filter_value",
3337
- field,
3338
- message: `No page metadata matches ${field} filter value${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}.`,
3339
- values: boundedSearchWarningValues(unknown),
3340
- count: unknown.length
3341
- });
3342
- }
3343
- const resultPages = findSearchResultPages(options.pages, options.results, options.baseUrl);
3344
- for (const field of SEARCH_AMBIGUITY_FIELDS) {
3345
- if ((options.filters[field]?.length ?? 0) > 0) continue;
3346
- const values = resultPages.flatMap((page) => {
3347
- const scope = resolveDocsSearchPageScope(page);
3348
- return scope.conflicts.includes(field) ? [] : scope[field];
3349
- });
3350
- const unique = Array.from(new Set(values)).sort(compareSearchMetadataValues);
3351
- if (!searchScopeIsAmbiguous(field, unique)) continue;
3352
- const contributingPages = resultPages.filter((page) => {
3353
- const scope = resolveDocsSearchPageScope(page);
3354
- return !scope.conflicts.includes(field) && scope[field].length > 0;
3355
- });
3356
- warnings.push({
3357
- code: "ambiguous_scope",
3358
- field,
3359
- message: `Search results span multiple ${field} scopes; add a ${field} filter before acting on scope-specific guidance.`,
3360
- values: boundedSearchWarningValues(unique),
3361
- pageUrls: boundedSearchWarningPageUrls(contributingPages),
3362
- count: unique.length
3363
- });
3364
- }
3365
- const codeOrder = {
3366
- ambiguous_scope: 0,
3367
- unknown_filter_value: 1,
3368
- missing_scope_metadata: 2,
3369
- conflicting_scope_metadata: 3
3370
- };
3371
- return warnings.sort((left, right) => {
3372
- const codeDelta = codeOrder[left.code] - codeOrder[right.code];
3373
- if (codeDelta !== 0) return codeDelta;
3374
- return SEARCH_FILTER_FIELDS.indexOf(left.field) - SEARCH_FILTER_FIELDS.indexOf(right.field);
3375
- }).slice(0, MAX_SEARCH_WARNINGS);
3376
- }
3377
- async function performDocsSearchWithMetadata(options) {
3378
- const audience = resolveDocsSearchAudience(options.audience);
3379
- const filters = normalizeDocsSearchFilters(options.filters);
3380
- const search = normalizeDocsSearchConfig(options.search);
3381
- const indexGeneration = options.indexGeneration ?? await buildDocsSearchIndexGeneration(options.generationPages ?? options.pages, {
3382
- audience,
3383
- chunking: search.chunking,
3384
- locale: options.locale,
3385
- baseUrl: options.baseUrl
3386
- });
3387
- const results = options.query.trim() ? await performDocsSearch({
3388
- ...options,
3389
- audience,
3390
- filters,
3391
- indexGeneration
3392
- }) : [];
3393
- return {
3394
- format: "docs-search.v1",
3395
- query: options.query,
3396
- audience,
3397
- filters,
3398
- indexGeneration,
3399
- resultCount: results.length,
3400
- results,
3401
- warnings: buildDocsSearchWarnings({
3402
- pages: options.pages,
3403
- results,
3404
- filters,
3405
- query: options.query,
3406
- audience,
3407
- baseUrl: options.baseUrl
3408
- })
3409
- };
3410
- }
3411
- async function buildDocsAskAIContext(options) {
3412
- const limit = options.limit ?? 5;
3413
- const searchLimit = Math.max(limit * 2, limit);
3414
- const initialSearch = options.search === false ? true : options.search;
3415
- const primarySearch = normalizeDocsSearchConfig(initialSearch).enabled ? initialSearch : true;
3416
- const searchResults = await performDocsSearch({
3417
- pages: options.pages,
3418
- query: options.query,
3419
- search: primarySearch,
3420
- audience: "agent",
3421
- locale: options.locale,
3422
- pathname: options.pathname,
3423
- siteTitle: options.siteTitle,
3424
- baseUrl: options.baseUrl,
3425
- syncBaseUrl: options.syncBaseUrl,
3426
- limit: searchLimit,
3427
- filters: options.filters,
3428
- failureMode: options.searchFailureMode,
3429
- strictExternalOrigins: options.strictExternalOrigins,
3430
- signal: options.signal
3431
- });
3432
- const seen = /* @__PURE__ */ new Set();
3433
- const maxResultChars = options.maxResultChars ?? DEFAULT_ASK_AI_RESULT_CHARS;
3434
- const rankedResults = searchResults.map((result, index) => {
3435
- const formatted = formatAskAIContextResult({
3436
- result,
3437
- page: findPageForSearchResult(options.pages, result, options.baseUrl),
3438
- maxChars: maxResultChars,
3439
- baseUrl: options.baseUrl
3440
- });
3441
- return {
3442
- result,
3443
- formatted,
3444
- index,
3445
- rank: rankAskAIContextResult(options.query, formatted)
3446
- };
3447
- }).sort((a, b) => b.rank - a.rank || a.index - b.index);
3448
- const formattedResults = rankedResults.map((item) => item.formatted);
3449
- const sectionResultPaths = new Set(formattedResults.filter((result) => result.section).map((result) => getAskAIResultPageKey(result.url, options.baseUrl, options.strictExternalOrigins)));
3450
- const results = formattedResults.filter((result) => result.section || !sectionResultPaths.has(getAskAIResultPageKey(result.url, options.baseUrl, options.strictExternalOrigins))).filter((result) => {
3451
- const key = getAskAIResultKey(result, options.baseUrl, options.strictExternalOrigins);
3452
- if (seen.has(key)) return false;
3453
- seen.add(key);
3454
- return result.contextContent.length > 0;
3455
- }).slice(0, limit);
3456
- const maxContextChars = options.maxContextChars ?? DEFAULT_ASK_AI_CONTEXT_CHARS;
3457
- const blocks = [];
3458
- let usedChars = 0;
3459
- for (const result of results) {
3460
- const block = buildAskAIContextBlock(result);
3461
- const separatorChars = blocks.length === 0 ? 0 : 7;
3462
- if (usedChars + separatorChars + block.length > maxContextChars) {
3463
- const remaining = maxContextChars - usedChars - separatorChars;
3464
- if (remaining > 400) blocks.push(clampText(block, remaining));
3465
- break;
3466
- }
3467
- blocks.push(block);
3468
- usedChars += separatorChars + block.length;
3469
- }
3470
- const context = blocks.join("\n\n---\n\n");
3471
- return {
3472
- context,
3473
- blocks: blocks.map((text, index) => ({
3474
- text,
3475
- result: results[index]
3476
- })),
3477
- results: results.slice(0, blocks.length),
3478
- searchResults: rankedResults.map((item) => item.result),
3479
- packageHints: inferDocsAskAIPackageHints(context)
3480
- };
3481
- }
3482
- function createCustomSearchAdapter(adapter) {
3483
- return {
3484
- provider: "custom",
3485
- adapter
3486
- };
3487
- }
3488
-
3489
- //#endregion
3490
- export { GENERATED_AGENT_PROVENANCE_MARKER as A, resolveSidebarFolderIndexBehaviorForPath as B, agentVersionConstraintsOverlap as C, normalizeAgentVersion as D, normalizeAgentScopeValues as E, serializeGeneratedAgentDocument as F, getDocsTelemetryFeatures as G, emitDocsTelemetryEvent as H, stripGeneratedAgentProvenance as I, normalizeDocsTelemetryOrigin as J, inferDocsTelemetryAgentSurface as K, applySidebarFolderIndexBehavior as L, hashGeneratedAgentContent as M, normalizeGeneratedAgentContent as N, digestDocsRetrievalContent as O, parseGeneratedAgentDocument as P, resolvePageSidebarFolderIndexBehavior as R, agentVersionConstraintMatches as S, normalizeAgentLocale as T, emitDocsTelemetryMcpToolEvent as U, emitDocsTelemetryAgentSurfaceEvent as V, emitDocsTelemetryProjectEvent as W, resolveDocsTelemetryConfig as Y, resolveDocsSearchAudience as _, createCustomSearchAdapter as a, resolveSearchRequestConfig as b, createTypesenseSearchAdapter as c, inferDocsAskAIPackageHints as d, normalizeDocsSearchFilters as f, resolveDocsRetrievalLastModified as g, resolveAskAISearchRequestConfig as h, createAlgoliaSearchAdapter as i, GENERATED_AGENT_PROVENANCE_VERSION as j, isDocsRetrievalCanonicalUrl as k, enrichDocsSearchDocumentsWithProvenance as l, performDocsSearchWithMetadata as m, buildDocsRetrievalDigestProjection as n, createMcpSearchAdapter as o, performDocsSearch as p, isLocalDocsTelemetryOrigin as q, buildDocsSearchDocuments as r, createSimpleSearchAdapter as s, buildDocsAskAIContext as t, formatDocsAskAIPackageHints as u, resolveDocsSearchFilters as v, normalizeAgentFramework as w, agentVersionConstraintGroupsOverlap as x, resolveDocsSearchRequest as y, resolveSidebarFolderIndexBehavior as z };