@f5-sales-demo/xcsh 22.3.3 → 22.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "22.3.3",
21
- "commit": "82917afa08ee15956c363dacfb1c0f4511c74d3e",
22
- "shortCommit": "82917af",
20
+ "version": "22.4.0",
21
+ "commit": "d4a6bd5c3d91616121cb5fb195c0dd0c2feb5bf7",
22
+ "shortCommit": "d4a6bd5",
23
23
  "branch": "main",
24
- "tag": "v22.3.3",
25
- "commitDate": "2026-09-28T13:54:02Z",
26
- "buildDate": "2026-09-28T14:30:23.798Z",
24
+ "tag": "v22.4.0",
25
+ "commitDate": "2026-09-29T03:06:46Z",
26
+ "buildDate": "2026-09-29T03:44:37.730Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5-sales-demo/xcsh",
30
30
  "repoSlug": "f5-sales-demo/xcsh",
31
- "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/82917afa08ee15956c363dacfb1c0f4511c74d3e",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v22.3.3"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/d4a6bd5c3d91616121cb5fb195c0dd0c2feb5bf7",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v22.4.0"
33
33
  };
@@ -0,0 +1,271 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { DocumentationSource } from "./documentation-resolve";
3
+
4
+ export const DOCUMENTATION_CONTENT_TYPES = [
5
+ "product_overview",
6
+ "solution_overview",
7
+ "concept",
8
+ "how_to",
9
+ "reference",
10
+ "knowledge_article",
11
+ ] as const;
12
+ export type DocumentationContentType = (typeof DOCUMENTATION_CONTENT_TYPES)[number];
13
+
14
+ export const DOCUMENTATION_TASK_TYPES = ["concept", "configure", "troubleshoot", "support", "reference"] as const;
15
+ export type DocumentationTaskType = (typeof DOCUMENTATION_TASK_TYPES)[number];
16
+
17
+ export const DOCUMENTATION_LIFECYCLES = ["current", "deprecated", "superseded"] as const;
18
+ export type DocumentationLifecycle = (typeof DOCUMENTATION_LIFECYCLES)[number];
19
+
20
+ export interface DocumentationRelatedDocument {
21
+ readonly relation: DocumentationTaskType;
22
+ readonly title: string;
23
+ readonly canonicalUrl: string;
24
+ readonly source: DocumentationSource;
25
+ readonly stablePath: string;
26
+ }
27
+
28
+ export interface DocumentationMetadata {
29
+ readonly metadataSchema: 1;
30
+ readonly product: string | null;
31
+ readonly contentType: DocumentationContentType;
32
+ readonly taskType: DocumentationTaskType;
33
+ readonly canonicalUrl: string;
34
+ readonly lastUpdated: string | null;
35
+ readonly language: string;
36
+ readonly aliases: readonly string[];
37
+ readonly lifecycle: DocumentationLifecycle;
38
+ readonly replacementUrl: string | null;
39
+ readonly relatedDocuments: readonly DocumentationRelatedDocument[];
40
+ }
41
+
42
+ export interface DocumentationPassage {
43
+ readonly anchor: string;
44
+ readonly heading: string;
45
+ readonly markdown: string;
46
+ readonly ordinal: number;
47
+ }
48
+
49
+ export interface DocumentationGraphNode {
50
+ readonly source: DocumentationSource;
51
+ readonly stablePath: string;
52
+ readonly title: string;
53
+ readonly metadata: DocumentationMetadata;
54
+ }
55
+
56
+ const CONTENT_TYPE_SET = new Set<string>(DOCUMENTATION_CONTENT_TYPES);
57
+ const TASK_TYPE_SET = new Set<string>(DOCUMENTATION_TASK_TYPES);
58
+ const LIFECYCLE_SET = new Set<string>(DOCUMENTATION_LIFECYCLES);
59
+ const LANGUAGE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
60
+ const PRODUCT = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
61
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
62
+ const SAFE_PATH = /^[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/;
63
+ const SOURCE_SET = new Set<DocumentationSource>(["docs-cloud-f5-com", "my-f5-com", "www-f5-com"]);
64
+
65
+ function record(value: unknown, field: string): Record<string, unknown> {
66
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} must be an object`);
67
+ return value as Record<string, unknown>;
68
+ }
69
+
70
+ function nonEmpty(value: unknown, field: string): string {
71
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${field} must be a non-empty string`);
72
+ return value;
73
+ }
74
+
75
+ function nullableDate(value: unknown, field: string): string | null {
76
+ if (value === null) return null;
77
+ const result = nonEmpty(value, field);
78
+ const parsed = new Date(`${result}T00:00:00Z`);
79
+ if (
80
+ result.length !== 10 ||
81
+ !DATE.test(result) ||
82
+ Number.isNaN(parsed.valueOf()) ||
83
+ parsed.toISOString().slice(0, 10) !== result
84
+ )
85
+ throw new Error(`${field} is invalid`);
86
+ return result;
87
+ }
88
+
89
+ function sortedStrings(value: unknown, field: string): readonly string[] {
90
+ if (!Array.isArray(value) || value.some(item => typeof item !== "string" || !item.trim())) {
91
+ throw new Error(`${field} must contain non-empty strings`);
92
+ }
93
+ const result = value as string[];
94
+ const expected = [...new Set(result)].sort((left, right) =>
95
+ compareText(left.toLocaleLowerCase("en-US"), right.toLocaleLowerCase("en-US")),
96
+ );
97
+ if (JSON.stringify(result) !== JSON.stringify(expected)) throw new Error(`${field} must be sorted and deduplicated`);
98
+ return result;
99
+ }
100
+
101
+ function compareText(left: string, right: string): number {
102
+ return left < right ? -1 : left > right ? 1 : 0;
103
+ }
104
+
105
+ export function parseDocumentationMetadata(
106
+ value: Record<string, unknown>,
107
+ validateUrl: (value: unknown, source: DocumentationSource | undefined, field: string) => string,
108
+ source: DocumentationSource,
109
+ field: string,
110
+ ): DocumentationMetadata {
111
+ if (value.metadata_schema !== 1) throw new Error(`${field}.metadata_schema must be 1`);
112
+ const product = value.product;
113
+ if (product !== null && (typeof product !== "string" || !PRODUCT.test(product))) {
114
+ throw new Error(`${field}.product must be a stable slug or null`);
115
+ }
116
+ const contentType = nonEmpty(value.content_type, `${field}.content_type`);
117
+ if (!CONTENT_TYPE_SET.has(contentType)) throw new Error(`${field}.content_type is invalid`);
118
+ const taskType = nonEmpty(value.task_type, `${field}.task_type`);
119
+ if (!TASK_TYPE_SET.has(taskType)) throw new Error(`${field}.task_type is invalid`);
120
+ const canonicalUrl = validateUrl(value.canonical_url, source, `${field}.canonical_url`);
121
+ const lastUpdated = nullableDate(value.last_updated, `${field}.last_updated`);
122
+ const language = nonEmpty(value.language, `${field}.language`);
123
+ if (!LANGUAGE.test(language)) throw new Error(`${field}.language is invalid`);
124
+ const aliases = sortedStrings(value.aliases, `${field}.aliases`);
125
+ const lifecycle = nonEmpty(value.lifecycle, `${field}.lifecycle`);
126
+ if (!LIFECYCLE_SET.has(lifecycle)) throw new Error(`${field}.lifecycle is invalid`);
127
+ const replacementUrl =
128
+ value.replacement_url === null ? null : validateUrl(value.replacement_url, undefined, `${field}.replacement_url`);
129
+ if ((lifecycle === "superseded") !== (replacementUrl !== null)) {
130
+ throw new Error(`${field}.replacement_url must exist only for superseded documents`);
131
+ }
132
+ if (!Array.isArray(value.related_documents)) throw new Error(`${field}.related_documents must be a list`);
133
+ const relatedDocuments = value.related_documents.map((item, position) => {
134
+ const relation = record(item, `${field}.related_documents[${position}]`);
135
+ const relationType = nonEmpty(relation.relation, `${field}.related_documents[${position}].relation`);
136
+ if (!TASK_TYPE_SET.has(relationType))
137
+ throw new Error(`${field}.related_documents[${position}].relation is invalid`);
138
+ const relatedSource = nonEmpty(
139
+ relation.source_id,
140
+ `${field}.related_documents[${position}].source_id`,
141
+ ) as DocumentationSource;
142
+ if (!SOURCE_SET.has(relatedSource))
143
+ throw new Error(`${field}.related_documents[${position}].source_id is invalid`);
144
+ const stablePath = nonEmpty(relation.stable_path, `${field}.related_documents[${position}].stable_path`);
145
+ if (!SAFE_PATH.test(stablePath))
146
+ throw new Error(`${field}.related_documents[${position}].stable_path is invalid`);
147
+ return {
148
+ relation: relationType as DocumentationTaskType,
149
+ title: nonEmpty(relation.title, `${field}.related_documents[${position}].title`),
150
+ canonicalUrl: validateUrl(
151
+ relation.canonical_url,
152
+ relatedSource,
153
+ `${field}.related_documents[${position}].canonical_url`,
154
+ ),
155
+ source: relatedSource,
156
+ stablePath,
157
+ };
158
+ });
159
+ const relatedOrder = [...relatedDocuments].sort(
160
+ (left, right) =>
161
+ compareText(left.relation, right.relation) ||
162
+ compareText(left.title.toLocaleLowerCase("en-US"), right.title.toLocaleLowerCase("en-US")) ||
163
+ compareText(left.canonicalUrl, right.canonicalUrl) ||
164
+ compareText(left.source, right.source) ||
165
+ compareText(left.stablePath, right.stablePath),
166
+ );
167
+ if (JSON.stringify(relatedDocuments) !== JSON.stringify(relatedOrder)) {
168
+ throw new Error(`${field}.related_documents must be sorted`);
169
+ }
170
+ if (new Set(relatedDocuments.map(item => item.canonicalUrl)).size !== relatedDocuments.length) {
171
+ throw new Error(`${field}.related_documents must be deduplicated`);
172
+ }
173
+ return {
174
+ metadataSchema: 1,
175
+ product: product as string | null,
176
+ contentType: contentType as DocumentationContentType,
177
+ taskType: taskType as DocumentationTaskType,
178
+ canonicalUrl,
179
+ lastUpdated,
180
+ language,
181
+ aliases,
182
+ lifecycle: lifecycle as DocumentationLifecycle,
183
+ replacementUrl,
184
+ relatedDocuments,
185
+ };
186
+ }
187
+
188
+ export function githubHeadingAnchor(heading: string, occurrences: Map<string, number>): string {
189
+ const base = heading
190
+ .trim()
191
+ .toLocaleLowerCase("en-US")
192
+ .replace(/<[^>]*>/g, "")
193
+ .replace(/[^\p{L}\p{N}\p{M}\s_-]/gu, "")
194
+ .replace(/\s/g, "-");
195
+ const normalized = base || "section";
196
+ const count = occurrences.get(normalized) ?? 0;
197
+ occurrences.set(normalized, count + 1);
198
+ return count === 0 ? normalized : `${normalized}-${count}`;
199
+ }
200
+
201
+ export function splitHeadingPassages(body: string): readonly DocumentationPassage[] {
202
+ const lines = body.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
203
+ const headings: Array<{ line: number; heading: string; anchor: string }> = [];
204
+ const occurrences = new Map<string, number>();
205
+ let fence: string | undefined;
206
+ for (const [line, value] of lines.entries()) {
207
+ const fenceMatch = /^\s*(`{3,}|~{3,})/.exec(value);
208
+ if (fenceMatch) {
209
+ const marker = fenceMatch[1]![0]!;
210
+ if (!fence) fence = marker;
211
+ else if (fence === marker) fence = undefined;
212
+ continue;
213
+ }
214
+ if (fence) continue;
215
+ const match = /^(#{1,3})\s+(.+?)\s*#*\s*$/.exec(value);
216
+ if (!match) continue;
217
+ const heading = match[2]!.trim();
218
+ headings.push({ line, heading, anchor: githubHeadingAnchor(heading, occurrences) });
219
+ }
220
+ if (headings.length === 0)
221
+ return [{ anchor: "document", heading: "Document", markdown: `${body.trim()}\n`, ordinal: 0 }];
222
+ return headings.map((heading, ordinal) => {
223
+ const start = heading.line;
224
+ const end = headings[ordinal + 1]?.line ?? lines.length;
225
+ return {
226
+ anchor: heading.anchor,
227
+ heading: heading.heading,
228
+ markdown: `${lines.slice(start, end).join("\n").trim()}\n`,
229
+ ordinal,
230
+ };
231
+ });
232
+ }
233
+
234
+ export function passageHash(documentHash: string, anchor: string, markdown: string): string {
235
+ return createHash("sha256").update(`${documentHash}\0${anchor}\0${markdown}`).digest("hex");
236
+ }
237
+
238
+ export function validateDocumentationGraph(documents: readonly DocumentationGraphNode[]): void {
239
+ const canonicalGroups = new Map<string, DocumentationGraphNode[]>();
240
+ for (const document of documents) {
241
+ canonicalGroups.set(document.metadata.canonicalUrl, [
242
+ ...(canonicalGroups.get(document.metadata.canonicalUrl) ?? []),
243
+ document,
244
+ ]);
245
+ }
246
+ const uniqueTargets = new Map(
247
+ [...canonicalGroups]
248
+ .filter(([, matches]) => matches.length === 1)
249
+ .map(([url, matches]) => [url, matches[0]!] as const),
250
+ );
251
+ for (const document of documents) {
252
+ if (document.metadata.replacementUrl !== null && !uniqueTargets.has(document.metadata.replacementUrl)) {
253
+ throw new Error(`replacement target is missing or ambiguous: ${document.metadata.replacementUrl}`);
254
+ }
255
+ for (const related of document.metadata.relatedDocuments) {
256
+ const target = uniqueTargets.get(related.canonicalUrl);
257
+ if (!target) throw new Error(`related target is missing or ambiguous: ${related.canonicalUrl}`);
258
+ if (
259
+ target.source !== related.source ||
260
+ target.stablePath !== related.stablePath ||
261
+ target.title !== related.title ||
262
+ target.metadata.taskType !== related.relation
263
+ ) {
264
+ throw new Error(`related target metadata mismatch: ${related.canonicalUrl}`);
265
+ }
266
+ if (related.canonicalUrl === document.metadata.canonicalUrl) {
267
+ throw new Error(`related target cannot reference itself: ${related.canonicalUrl}`);
268
+ }
269
+ }
270
+ }
271
+ }
@@ -6,14 +6,15 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { gunzipSync } from "node:zlib";
8
8
  import { createStore } from "@tobilu/qmd";
9
- import {
10
- DOCUMENTATION_SOURCES,
11
- type DocumentationAsset,
12
- type DocumentationDocument,
13
- type DocumentationRepository,
14
- type DocumentationSearchResult,
15
- type DocumentationSource,
9
+ import type {
10
+ DocumentationAsset,
11
+ DocumentationDocument,
12
+ DocumentationRepository,
13
+ DocumentationSearchFilters,
14
+ DocumentationSearchResult,
15
+ DocumentationSource,
16
16
  } from "./documentation-resolve";
17
+ import { DOCUMENTATION_SOURCES } from "./documentation-resolve";
17
18
  import { extractVerifiedDocumentationAsset, type VerifiedDocumentationAsset } from "./documentation-snapshot";
18
19
 
19
20
  interface EmbeddedDocumentationAssetMetadata {
@@ -67,6 +68,24 @@ interface DocumentRow {
67
68
  readonly title: string;
68
69
  readonly original_url: string;
69
70
  readonly markdown: string;
71
+ readonly canonical_url: string;
72
+ readonly last_updated: string | null;
73
+ readonly product: string | null;
74
+ readonly content_type: DocumentationSearchResult["contentType"];
75
+ readonly task_type: DocumentationSearchResult["taskType"];
76
+ readonly language: string;
77
+ readonly lifecycle: DocumentationSearchResult["lifecycle"];
78
+ readonly replacement_url: string | null;
79
+ readonly aliases_json: string;
80
+ readonly related_documents_json: string;
81
+ }
82
+
83
+ interface PassageRow extends DocumentRow {
84
+ readonly anchor: string;
85
+ readonly heading: string;
86
+ readonly passage_markdown: string;
87
+ readonly ordinal: number;
88
+ readonly score: number;
70
89
  }
71
90
 
72
91
  interface AssetRow {
@@ -219,6 +238,7 @@ function verifyDatabase(database: Database, assets: EmbeddedDocumentationAssets)
219
238
  asset_count: String(assets.assetCount),
220
239
  document_count: String(assets.documentCount),
221
240
  fingerprint: assets.fingerprint,
241
+ index_schema: "2",
222
242
  release_tag: assets.releaseTag,
223
243
  source_commit: assets.sourceCommit,
224
244
  };
@@ -234,14 +254,6 @@ function verifyDatabase(database: Database, assets: EmbeddedDocumentationAssets)
234
254
  }
235
255
  }
236
256
 
237
- function stablePathFromResult(displayPath: string, source: DocumentationSource): string | null {
238
- const prefix = `${source}/`;
239
- const suffix = "/index.md";
240
- if (!displayPath.startsWith(prefix) || !displayPath.endsWith(suffix)) return null;
241
- const stablePath = displayPath.slice(prefix.length, -suffix.length);
242
- return stablePath || null;
243
- }
244
-
245
257
  function boundedSnippet(markdown: string, query: string): string {
246
258
  const body = markdown
247
259
  .replace(/^---\n.*?\n---\n?/s, "")
@@ -264,6 +276,122 @@ function lexicalDocumentationQuery(query: string): string {
264
276
  return query.replace(/^\s*(?:what|who)\s+(?:is|are)\s+/i, "").trim() || query;
265
277
  }
266
278
 
279
+ function ftsPhrase(value: string): string {
280
+ return value
281
+ .normalize("NFKC")
282
+ .replace(/[^\p{L}\p{N}_-]+/gu, " ")
283
+ .replace(/-/g, " ")
284
+ .trim()
285
+ .replace(/\s+/g, " ");
286
+ }
287
+
288
+ function aliasGroups(database: Database): readonly (readonly string[])[] {
289
+ const rows = database
290
+ .query("SELECT product, alias FROM documentation_aliases ORDER BY product, alias")
291
+ .all() as Array<{
292
+ product: string;
293
+ alias: string;
294
+ }>;
295
+ const groups = new Map<string, string[]>();
296
+ for (const row of rows) groups.set(row.product, [...(groups.get(row.product) ?? []), row.alias]);
297
+ return [...groups.values()].sort(
298
+ (left, right) => Math.max(...right.map(value => value.length)) - Math.max(...left.map(value => value.length)),
299
+ );
300
+ }
301
+
302
+ function escapedFtsQuery(database: Database, query: string): string {
303
+ let remaining = lexicalDocumentationQuery(query);
304
+ const expansions: string[] = [];
305
+ for (const aliases of aliasGroups(database)) {
306
+ const matched = [...aliases]
307
+ .sort((left, right) => right.length - left.length)
308
+ .find(alias =>
309
+ new RegExp(
310
+ `(^|[^\\p{L}\\p{N}])${alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^\\p{L}\\p{N}]|$)`,
311
+ "iu",
312
+ ).test(remaining),
313
+ );
314
+ if (!matched) continue;
315
+ remaining = remaining.replace(new RegExp(matched.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "iu"), " ");
316
+ expansions.push(`(${aliases.map(alias => `"${ftsPhrase(alias)}"`).join(" OR ")})`);
317
+ }
318
+ const terms =
319
+ remaining
320
+ .match(/[\p{L}\p{N}_-]+/gu)
321
+ ?.map(term => ftsPhrase(term))
322
+ .filter(Boolean) ?? [];
323
+ return [...expansions, ...terms.map(term => `"${term}"*`)].join(" AND ");
324
+ }
325
+
326
+ function searchRows(
327
+ database: Database,
328
+ query: string,
329
+ source: DocumentationSource | undefined,
330
+ filters: DocumentationSearchFilters,
331
+ ): PassageRow[] {
332
+ const clauses = ["documents_fts MATCH ?", "d.active = 1"];
333
+ const parameters: Array<string | null> = [escapedFtsQuery(database, query)];
334
+ const add = (column: string, value: string | undefined): void => {
335
+ if (value === undefined) return;
336
+ clauses.push(`${column} = ?`);
337
+ parameters.push(value);
338
+ };
339
+ add("dd.source", source);
340
+ add("dd.product", filters.product);
341
+ add("dd.content_type", filters.contentType);
342
+ add("dd.task_type", filters.taskType);
343
+ add("dd.language", filters.language);
344
+ add("dd.lifecycle", filters.lifecycle);
345
+ if (!parameters[0]) return [];
346
+ return database
347
+ .query(`WITH scored AS (
348
+ SELECT dd.*, p.anchor, p.heading, p.markdown AS passage_markdown, p.ordinal,
349
+ ABS(bm25(documents_fts, 1.5, 4.0, 1.0)) /
350
+ (1.0 + ABS(bm25(documents_fts, 1.5, 4.0, 1.0))) *
351
+ CASE dd.lifecycle WHEN 'deprecated' THEN 0.85 WHEN 'superseded' THEN 0.65 ELSE 1.0 END AS score
352
+ FROM documents_fts
353
+ JOIN documents d ON d.id = documents_fts.rowid
354
+ JOIN documentation_passages p ON p.source = d.collection AND p.qmd_path = d.path
355
+ JOIN documentation_documents dd ON dd.source = p.source AND dd.stable_path = p.stable_path
356
+ WHERE ${clauses.join(" AND ")}
357
+ ), ranked AS (
358
+ SELECT *, ROW_NUMBER() OVER (
359
+ PARTITION BY source, stable_path ORDER BY score DESC, ordinal ASC
360
+ ) AS page_rank FROM scored
361
+ )
362
+ SELECT * FROM ranked WHERE page_rank = 1`)
363
+ .all(...parameters) as PassageRow[];
364
+ }
365
+
366
+ function diversifySources(
367
+ results: readonly PassageRow[],
368
+ preferredSource: DocumentationSource | undefined,
369
+ limit: number,
370
+ ): PassageRow[] {
371
+ const selected: PassageRow[] = [];
372
+ const seen = new Set<string>();
373
+ const sourceOrder = preferredSource
374
+ ? [preferredSource, ...DOCUMENTATION_SOURCES.filter(source => source !== preferredSource)]
375
+ : [...DOCUMENTATION_SOURCES].sort((left, right) => {
376
+ const leftScore = results.find(result => result.source === left)?.score ?? -1;
377
+ const rightScore = results.find(result => result.source === right)?.score ?? -1;
378
+ return rightScore - leftScore || left.localeCompare(right);
379
+ });
380
+ for (const source of sourceOrder) {
381
+ const result = results.find(candidate => candidate.source === source);
382
+ if (!result) continue;
383
+ selected.push(result);
384
+ seen.add(`${result.source}\0${result.stable_path}`);
385
+ }
386
+ for (const result of results) {
387
+ const key = `${result.source}\0${result.stable_path}`;
388
+ if (seen.has(key)) continue;
389
+ selected.push(result);
390
+ seen.add(key);
391
+ }
392
+ return selected.slice(0, limit);
393
+ }
394
+
267
395
  const SUPPORT_QUERY = /\b(?:troubleshoot|support|knowledge[- ]base|error|failure|issue|K[0-9]{6,})\b/i;
268
396
  const CONFIGURATION_QUERY = /\b(?:configure|configuration|set\s*up|procedure|instructions?|how\s+(?:do|can|to))\b/i;
269
397
  const CONCEPTUAL_QUERY = /\b(?:what\s+(?:is|are)|overview|product|solution|capabilities|benefits)\b/i;
@@ -313,58 +441,67 @@ export function createEmbeddedDocumentationRepository(
313
441
  prime: async () => {
314
442
  await state();
315
443
  },
316
- search: async (query, source, limit): Promise<readonly DocumentationSearchResult[]> => {
444
+ search: async (query, source, limit, filters = {}): Promise<readonly DocumentationSearchResult[]> => {
317
445
  const current = await state();
318
- const lexicalQuery = lexicalDocumentationQuery(query);
319
446
  const preferredSource = source ? undefined : preferredDocumentationSource(query);
320
- const results = source
321
- ? await current.store.searchLex(lexicalQuery, { limit, collection: source })
322
- : (
323
- await Promise.all(
324
- DOCUMENTATION_SOURCES.map(collection =>
325
- current.store.searchLex(lexicalQuery, { limit, collection }),
326
- ),
327
- )
328
- ).flat();
447
+ const results = searchRows(current.database, query, source, filters);
329
448
  results.sort((left, right) => {
330
- const sourceOrder =
331
- Number(right.collectionName === preferredSource) - Number(left.collectionName === preferredSource);
449
+ const sourceOrder = Number(right.source === preferredSource) - Number(left.source === preferredSource);
332
450
  return (
333
451
  sourceOrder ||
334
452
  right.score - left.score ||
335
- left.collectionName.localeCompare(right.collectionName) ||
336
- left.displayPath.localeCompare(right.displayPath)
453
+ left.source.localeCompare(right.source) ||
454
+ left.stable_path.localeCompare(right.stable_path)
337
455
  );
338
456
  });
339
- const rows: DocumentationSearchResult[] = [];
340
- const lookup = current.database.query(
341
- "SELECT source, stable_path, title, original_url, markdown FROM documentation_documents WHERE source = ? AND stable_path = ?",
342
- );
343
- for (const result of results) {
344
- const resultSource = result.collectionName as DocumentationSource;
345
- const stablePath = stablePathFromResult(result.displayPath, resultSource);
346
- if (!stablePath) continue;
347
- const row = lookup.get(resultSource, stablePath) as DocumentRow | null;
348
- if (!row) continue;
349
- rows.push({
350
- title: row.title,
351
- source: row.source,
352
- originalUrl: row.original_url,
353
- stablePath: row.stable_path,
354
- snippet: boundedSnippet(row.markdown, query),
355
- score: result.score,
356
- });
357
- }
358
- return rows.slice(0, limit);
457
+ const selected = source ? results.slice(0, limit) : diversifySources(results, preferredSource, limit);
458
+ return selected.map(row => ({
459
+ title: row.title,
460
+ source: row.source,
461
+ originalUrl: row.original_url,
462
+ stablePath: row.stable_path,
463
+ snippet: boundedSnippet(row.passage_markdown, query),
464
+ score: Number(row.score.toFixed(12)),
465
+ anchor: row.anchor,
466
+ heading: row.heading,
467
+ canonicalUrl: row.canonical_url,
468
+ lastUpdated: row.last_updated,
469
+ product: row.product,
470
+ contentType: row.content_type,
471
+ taskType: row.task_type,
472
+ language: row.language,
473
+ lifecycle: row.lifecycle,
474
+ replacementUrl: row.replacement_url,
475
+ aliases: JSON.parse(row.aliases_json),
476
+ relatedDocuments: JSON.parse(row.related_documents_json),
477
+ }));
359
478
  },
360
- readDocument: async (source, stablePath): Promise<DocumentationDocument | null> => {
479
+ readDocument: async (source, stablePath, anchor): Promise<DocumentationDocument | null> => {
361
480
  const current = await state();
362
481
  const row = current.database
363
482
  .query(
364
- "SELECT title, original_url, markdown FROM documentation_documents WHERE source = ? AND stable_path = ?",
483
+ "SELECT title, original_url, markdown, lifecycle, replacement_url FROM documentation_documents WHERE source = ? AND stable_path = ?",
365
484
  )
366
- .get(source, stablePath) as Pick<DocumentRow, "title" | "original_url" | "markdown"> | null;
367
- return row ? { markdown: row.markdown, title: row.title, originalUrl: row.original_url } : null;
485
+ .get(source, stablePath) as Pick<
486
+ DocumentRow,
487
+ "title" | "original_url" | "markdown" | "lifecycle" | "replacement_url"
488
+ > | null;
489
+ if (!row) return null;
490
+ let markdown = row.markdown;
491
+ if (anchor) {
492
+ const passage = current.database
493
+ .query("SELECT markdown FROM documentation_passages WHERE source = ? AND stable_path = ? AND anchor = ?")
494
+ .get(source, stablePath, anchor) as { markdown: string } | null;
495
+ if (!passage) return null;
496
+ markdown = passage.markdown;
497
+ }
498
+ return {
499
+ markdown,
500
+ title: row.title,
501
+ originalUrl: row.original_url,
502
+ lifecycle: row.lifecycle,
503
+ replacementUrl: row.replacement_url,
504
+ };
368
505
  },
369
506
  readAsset: async (source, stablePath, filename): Promise<DocumentationAsset | null> => {
370
507
  const current = await state();