@farming-labs/docs 0.2.120 → 0.2.121

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