@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.
- package/package.json +8 -8
- package/src/extensibility/plugins/marketplace/builtin-marketplace-snapshot.ts +1 -1
- package/src/extensibility/plugins/marketplace/builtin-marketplace.provenance.json +3 -3
- package/src/extensibility/slash-commands.ts +21 -0
- package/src/internal-urls/api-catalog-qmd-index.generated.ts +2 -2
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/documentation-metadata.ts +271 -0
- package/src/internal-urls/documentation-repository.ts +191 -54
- package/src/internal-urls/documentation-resolve.ts +123 -9
- package/src/internal-urls/documentation-snapshot.ts +167 -11
- package/src/modes/interactive-mode.ts +2 -5
- package/src/prompts/rules/llms-search.md +5 -3
- package/src/prompts/system/system-prompt.md +4 -2
- package/src/slash-commands/builtin-registry.ts +2 -1
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DOCUMENTATION_CONTENT_TYPES,
|
|
3
|
+
DOCUMENTATION_LIFECYCLES,
|
|
4
|
+
DOCUMENTATION_TASK_TYPES,
|
|
5
|
+
type DocumentationContentType,
|
|
6
|
+
type DocumentationLifecycle,
|
|
7
|
+
type DocumentationRelatedDocument,
|
|
8
|
+
type DocumentationTaskType,
|
|
9
|
+
} from "./documentation-metadata";
|
|
1
10
|
import type { InternalResource, InternalUrl } from "./types";
|
|
2
11
|
|
|
3
12
|
export const DOCUMENTATION_SOURCES = ["docs-cloud-f5-com", "my-f5-com", "www-f5-com"] as const;
|
|
@@ -20,12 +29,34 @@ export interface DocumentationSearchResult {
|
|
|
20
29
|
readonly stablePath: string;
|
|
21
30
|
readonly snippet: string;
|
|
22
31
|
readonly score: number;
|
|
32
|
+
readonly anchor?: string;
|
|
33
|
+
readonly heading?: string;
|
|
34
|
+
readonly canonicalUrl?: string;
|
|
35
|
+
readonly lastUpdated?: string | null;
|
|
36
|
+
readonly product?: string | null;
|
|
37
|
+
readonly contentType?: DocumentationContentType;
|
|
38
|
+
readonly taskType?: DocumentationTaskType;
|
|
39
|
+
readonly language?: string;
|
|
40
|
+
readonly lifecycle?: DocumentationLifecycle;
|
|
41
|
+
readonly replacementUrl?: string | null;
|
|
42
|
+
readonly aliases?: readonly string[];
|
|
43
|
+
readonly relatedDocuments?: readonly DocumentationRelatedDocument[];
|
|
23
44
|
}
|
|
24
45
|
|
|
25
46
|
export interface DocumentationDocument {
|
|
26
47
|
readonly markdown: string;
|
|
27
48
|
readonly title: string;
|
|
28
49
|
readonly originalUrl: string;
|
|
50
|
+
readonly lifecycle?: DocumentationLifecycle;
|
|
51
|
+
readonly replacementUrl?: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DocumentationSearchFilters {
|
|
55
|
+
readonly product?: string;
|
|
56
|
+
readonly contentType?: DocumentationContentType;
|
|
57
|
+
readonly taskType?: DocumentationTaskType;
|
|
58
|
+
readonly language?: string;
|
|
59
|
+
readonly lifecycle?: DocumentationLifecycle;
|
|
29
60
|
}
|
|
30
61
|
|
|
31
62
|
export interface DocumentationAsset {
|
|
@@ -39,8 +70,13 @@ export interface DocumentationRepository {
|
|
|
39
70
|
query: string,
|
|
40
71
|
source: DocumentationSource | undefined,
|
|
41
72
|
limit: number,
|
|
73
|
+
filters?: DocumentationSearchFilters,
|
|
42
74
|
): Promise<readonly DocumentationSearchResult[]>;
|
|
43
|
-
readDocument(
|
|
75
|
+
readDocument(
|
|
76
|
+
source: DocumentationSource,
|
|
77
|
+
stablePath: string,
|
|
78
|
+
anchor?: string,
|
|
79
|
+
): Promise<DocumentationDocument | null>;
|
|
44
80
|
readAsset(source: DocumentationSource, stablePath: string, filename: string): Promise<DocumentationAsset | null>;
|
|
45
81
|
}
|
|
46
82
|
|
|
@@ -53,9 +89,25 @@ const IMAGE_NAME = /^[a-f0-9]{64}\.(?:gif|jpe?g|png|svg|webp)$/;
|
|
|
53
89
|
const SAFE_SEGMENT = /^[A-Za-z0-9._~-]+$/;
|
|
54
90
|
const MAX_QUERY_BYTES = 512;
|
|
55
91
|
const MAX_SNIPPET_CHARS = 600;
|
|
92
|
+
const PRODUCT = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
93
|
+
const LANGUAGE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
56
94
|
|
|
57
|
-
function validateQuery(url: InternalUrl): {
|
|
58
|
-
|
|
95
|
+
function validateQuery(url: InternalUrl): {
|
|
96
|
+
search?: string;
|
|
97
|
+
source?: DocumentationSource;
|
|
98
|
+
limit: number;
|
|
99
|
+
filters: DocumentationSearchFilters;
|
|
100
|
+
} {
|
|
101
|
+
const allowed = new Set([
|
|
102
|
+
"search",
|
|
103
|
+
"source",
|
|
104
|
+
"limit",
|
|
105
|
+
"product",
|
|
106
|
+
"content_type",
|
|
107
|
+
"task_type",
|
|
108
|
+
"language",
|
|
109
|
+
"lifecycle",
|
|
110
|
+
]);
|
|
59
111
|
for (const key of url.searchParams.keys()) {
|
|
60
112
|
if (!allowed.has(key)) throw new Error(`Unknown documentation parameter: ${key}`);
|
|
61
113
|
if (url.searchParams.getAll(key).length !== 1) throw new Error(`Duplicate documentation parameter: ${key}`);
|
|
@@ -73,12 +125,36 @@ function validateQuery(url: InternalUrl): { search?: string; source?: Documentat
|
|
|
73
125
|
throw new Error("Documentation search limit must be an integer from 1 to 10");
|
|
74
126
|
}
|
|
75
127
|
if (!search && (sourceValue !== undefined || limitValue !== null)) {
|
|
76
|
-
throw new Error("Documentation
|
|
128
|
+
throw new Error("Documentation filters and limit require search");
|
|
129
|
+
}
|
|
130
|
+
const product = url.searchParams.get("product") ?? undefined;
|
|
131
|
+
if (product !== undefined && !PRODUCT.test(product)) throw new Error(`Invalid documentation product: ${product}`);
|
|
132
|
+
const contentType = url.searchParams.get("content_type") ?? undefined;
|
|
133
|
+
if (contentType !== undefined && !DOCUMENTATION_CONTENT_TYPES.includes(contentType as DocumentationContentType))
|
|
134
|
+
throw new Error(`Invalid documentation content type: ${contentType}`);
|
|
135
|
+
const taskType = url.searchParams.get("task_type") ?? undefined;
|
|
136
|
+
if (taskType !== undefined && !DOCUMENTATION_TASK_TYPES.includes(taskType as DocumentationTaskType))
|
|
137
|
+
throw new Error(`Invalid documentation task type: ${taskType}`);
|
|
138
|
+
const language = url.searchParams.get("language") ?? undefined;
|
|
139
|
+
if (language !== undefined && !LANGUAGE.test(language))
|
|
140
|
+
throw new Error(`Invalid documentation language: ${language}`);
|
|
141
|
+
const lifecycle = url.searchParams.get("lifecycle") ?? undefined;
|
|
142
|
+
if (lifecycle !== undefined && !DOCUMENTATION_LIFECYCLES.includes(lifecycle as DocumentationLifecycle))
|
|
143
|
+
throw new Error(`Invalid documentation lifecycle: ${lifecycle}`);
|
|
144
|
+
if (!search && [...url.searchParams.keys()].some(key => key !== "search")) {
|
|
145
|
+
throw new Error("Documentation filters and limit require search");
|
|
77
146
|
}
|
|
78
147
|
return {
|
|
79
148
|
search,
|
|
80
149
|
source: sourceValue as DocumentationSource | undefined,
|
|
81
150
|
limit: limitValue === null ? 5 : Number(limitValue),
|
|
151
|
+
filters: {
|
|
152
|
+
product,
|
|
153
|
+
contentType: contentType as DocumentationContentType | undefined,
|
|
154
|
+
taskType: taskType as DocumentationTaskType | undefined,
|
|
155
|
+
language,
|
|
156
|
+
lifecycle: lifecycle as DocumentationLifecycle | undefined,
|
|
157
|
+
},
|
|
82
158
|
};
|
|
83
159
|
}
|
|
84
160
|
|
|
@@ -119,7 +195,7 @@ function inventory(repository: DocumentationRepository): string {
|
|
|
119
195
|
"Sources:",
|
|
120
196
|
...DOCUMENTATION_SOURCES.map(source => `- \`${source}\``),
|
|
121
197
|
"",
|
|
122
|
-
"Search with `xcsh://documentation/?search=<query>&source=<optional-source>&limit=<1-10>`.",
|
|
198
|
+
"Search with `xcsh://documentation/?search=<query>&source=<optional-source>&product=<slug>&content_type=<type>&task_type=<type>&language=<tag>&lifecycle=<state>&limit=<1-10>`.",
|
|
123
199
|
].join("\n");
|
|
124
200
|
}
|
|
125
201
|
|
|
@@ -136,17 +212,42 @@ function renderSearch(
|
|
|
136
212
|
];
|
|
137
213
|
for (const result of results) {
|
|
138
214
|
const snippet = result.snippet.replace(/\s+/g, " ").trim().slice(0, MAX_SNIPPET_CHARS);
|
|
139
|
-
const followUp = `xcsh://documentation/${result.source}/${result.stablePath}/index.md`;
|
|
215
|
+
const followUp = `xcsh://documentation/${result.source}/${result.stablePath}/index.md${result.anchor ? `#${result.anchor}` : ""}`;
|
|
140
216
|
lines.push(
|
|
141
217
|
`## ${result.title}`,
|
|
142
218
|
`- Source: \`${result.source}\``,
|
|
143
219
|
`- Original: ${result.originalUrl}`,
|
|
220
|
+
`- Canonical: ${result.canonicalUrl ?? result.originalUrl}`,
|
|
221
|
+
`- Last updated: ${result.lastUpdated ?? "unknown"}`,
|
|
222
|
+
`- Product: ${result.product ?? "unclassified"}`,
|
|
223
|
+
`- Content type: ${result.contentType}`,
|
|
224
|
+
`- Task type: ${result.taskType}`,
|
|
225
|
+
`- Section: ${result.heading ?? result.anchor ?? "document"}`,
|
|
226
|
+
`- Language: ${result.language}`,
|
|
227
|
+
`- Lifecycle: ${result.lifecycle}`,
|
|
228
|
+
`- Aliases: ${result.aliases?.join(", ") || "none"}`,
|
|
144
229
|
`- Score: ${result.score}`,
|
|
145
230
|
`- Read: \`${followUp}\``,
|
|
146
231
|
"",
|
|
147
232
|
snippet,
|
|
148
233
|
"",
|
|
149
234
|
);
|
|
235
|
+
if (result.lifecycle && result.lifecycle !== "current") {
|
|
236
|
+
lines.push(
|
|
237
|
+
`> Warning: this document is ${result.lifecycle}.${result.replacementUrl ? ` Replacement: ${result.replacementUrl}` : ""}`,
|
|
238
|
+
"",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if ((result.relatedDocuments?.length ?? 0) > 0) {
|
|
242
|
+
lines.push(
|
|
243
|
+
"Related documents:",
|
|
244
|
+
...(result.relatedDocuments ?? []).map(
|
|
245
|
+
related =>
|
|
246
|
+
`- ${related.relation}: [${related.title}](xcsh://documentation/${related.source}/${related.stablePath}/index.md)`,
|
|
247
|
+
),
|
|
248
|
+
"",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
150
251
|
}
|
|
151
252
|
if (results.length === 0) lines.push("No matching documents found.", "");
|
|
152
253
|
return lines.join("\n");
|
|
@@ -159,7 +260,7 @@ export function createDocumentationResolver(repository: DocumentationRepository)
|
|
|
159
260
|
const segments = parseResourcePath(url);
|
|
160
261
|
if (query.search) {
|
|
161
262
|
if (segments.length !== 0) throw new Error("Documentation search is available only at the root");
|
|
162
|
-
const results = await repository.search(query.search, query.source, query.limit);
|
|
263
|
+
const results = await repository.search(query.search, query.source, query.limit, query.filters);
|
|
163
264
|
return textResource(url, renderSearch(repository, query.search, results), "xcsh://documentation/");
|
|
164
265
|
}
|
|
165
266
|
if (segments.length === 0) {
|
|
@@ -171,9 +272,22 @@ export function createDocumentationResolver(repository: DocumentationRepository)
|
|
|
171
272
|
if (rest.at(-1) === "index.md") {
|
|
172
273
|
const stablePath = rest.slice(0, -1).join("/");
|
|
173
274
|
if (!stablePath) throw new Error("Documentation path is missing");
|
|
174
|
-
const
|
|
275
|
+
const rawAnchor = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
276
|
+
if (/%(?:2f|5c|2e)/i.test(rawAnchor)) throw new Error("Invalid documentation anchor");
|
|
277
|
+
const anchor = rawAnchor ? decodeURIComponent(rawAnchor) : undefined;
|
|
278
|
+
if (anchor && (anchor.length > 256 || !/^[\p{L}\p{N}\p{M}_-]+$/u.test(anchor)))
|
|
279
|
+
throw new Error("Invalid documentation anchor");
|
|
280
|
+
const document = await repository.readDocument(source, stablePath, anchor);
|
|
175
281
|
if (!document) throw new Error(`Documentation file not found: ${source}/${stablePath}/index.md`);
|
|
176
|
-
|
|
282
|
+
const warning =
|
|
283
|
+
!document.lifecycle || document.lifecycle === "current"
|
|
284
|
+
? ""
|
|
285
|
+
: `> Warning: this document is ${document.lifecycle}.${document.replacementUrl ? ` Replacement: ${document.replacementUrl}` : ""}\n\n`;
|
|
286
|
+
return textResource(
|
|
287
|
+
url,
|
|
288
|
+
`${warning}${document.markdown}`,
|
|
289
|
+
`xcsh://documentation/${source}/${stablePath}/index.md${anchor ? `#${anchor}` : ""}`,
|
|
290
|
+
);
|
|
177
291
|
}
|
|
178
292
|
const assetsIndex = rest.length - 2;
|
|
179
293
|
if (assetsIndex >= 1 && rest[assetsIndex] === "assets" && IMAGE_NAME.test(rest.at(-1) ?? "")) {
|
|
@@ -8,6 +8,14 @@ import { createGunzip, gunzipSync } from "node:zlib";
|
|
|
8
8
|
import { createStore } from "@tobilu/qmd";
|
|
9
9
|
import tar from "tar-stream";
|
|
10
10
|
import { parse as parseYaml } from "yaml";
|
|
11
|
+
import {
|
|
12
|
+
type DocumentationMetadata,
|
|
13
|
+
type DocumentationPassage,
|
|
14
|
+
parseDocumentationMetadata,
|
|
15
|
+
passageHash,
|
|
16
|
+
splitHeadingPassages,
|
|
17
|
+
validateDocumentationGraph,
|
|
18
|
+
} from "./documentation-metadata";
|
|
11
19
|
import { DOCUMENTATION_SOURCES, type DocumentationSource } from "./documentation-resolve";
|
|
12
20
|
|
|
13
21
|
const MIB = 1024 * 1024;
|
|
@@ -33,6 +41,7 @@ const COMMIT = /^[a-f0-9]{40}$/;
|
|
|
33
41
|
const RELEASE_TAG = /^content-[0-9]{8}T[0-9]{6}Z$/;
|
|
34
42
|
const TIMESTAMP = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/;
|
|
35
43
|
const FIXED_TIME = "2000-01-01T00:00:00.000Z";
|
|
44
|
+
const DOCUMENTATION_INDEX_SCHEMA_VERSION = 2;
|
|
36
45
|
const MANIFEST_KEYS = [
|
|
37
46
|
"asset_count",
|
|
38
47
|
"assets",
|
|
@@ -112,6 +121,8 @@ export interface VerifiedDocumentationDocument {
|
|
|
112
121
|
readonly fileSha256: string;
|
|
113
122
|
readonly sizeBytes: number;
|
|
114
123
|
readonly markdown: string;
|
|
124
|
+
readonly metadata: DocumentationMetadata;
|
|
125
|
+
readonly passages: readonly DocumentationPassage[];
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
export interface VerifiedDocumentationAsset {
|
|
@@ -198,6 +209,39 @@ function validateOriginalUrl(value: unknown, source: DocumentationSource, field:
|
|
|
198
209
|
return result;
|
|
199
210
|
}
|
|
200
211
|
|
|
212
|
+
function validateCorpusUrl(value: unknown, source: DocumentationSource | undefined, field: string): string {
|
|
213
|
+
if (source !== undefined) {
|
|
214
|
+
const result = validateOriginalUrl(value, source, field);
|
|
215
|
+
validateNormalizedUrl(result, field);
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
for (const candidate of DOCUMENTATION_SOURCES) {
|
|
219
|
+
try {
|
|
220
|
+
const result = validateOriginalUrl(value, candidate, field);
|
|
221
|
+
validateNormalizedUrl(result, field);
|
|
222
|
+
return result;
|
|
223
|
+
} catch {
|
|
224
|
+
// Continue until one complete source policy accepts the URL.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw new Error(`${field} is outside the documentation corpus`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function validateNormalizedUrl(value: string, field: string): void {
|
|
231
|
+
const parsed = new URL(value);
|
|
232
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.hash) {
|
|
233
|
+
throw new Error(`${field} is not a normalized HTTPS URL`);
|
|
234
|
+
}
|
|
235
|
+
const entries = [...parsed.searchParams.entries()].sort(
|
|
236
|
+
([leftKey, leftValue], [rightKey, rightValue]) =>
|
|
237
|
+
leftKey.localeCompare(rightKey) || leftValue.localeCompare(rightValue),
|
|
238
|
+
);
|
|
239
|
+
const normalizedQuery = new URLSearchParams(entries).toString();
|
|
240
|
+
const normalizedPath = parsed.pathname === "/" ? "/" : parsed.pathname.replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
|
241
|
+
const normalized = `${parsed.protocol}//${parsed.host}${normalizedPath}${normalizedQuery ? `?${normalizedQuery}` : ""}`;
|
|
242
|
+
if (normalized !== value) throw new Error(`${field} is not normalized`);
|
|
243
|
+
}
|
|
244
|
+
|
|
201
245
|
export function sha256Bytes(value: Uint8Array | string): string {
|
|
202
246
|
return createHash("sha256").update(value).digest("hex");
|
|
203
247
|
}
|
|
@@ -616,6 +660,16 @@ export async function verifyDocumentationRelease(
|
|
|
616
660
|
}
|
|
617
661
|
if (parsed.metadata.sourceId !== source || parsed.metadata.url !== originalUrl)
|
|
618
662
|
throw new Error(`manifest document frontmatter mismatch: ${archiveMemberPath}`);
|
|
663
|
+
const metadata = parseDocumentationMetadata(
|
|
664
|
+
parsed.metadata,
|
|
665
|
+
validateCorpusUrl,
|
|
666
|
+
source,
|
|
667
|
+
`document ${archiveMemberPath}`,
|
|
668
|
+
);
|
|
669
|
+
const passages = splitHeadingPassages(parsed.body);
|
|
670
|
+
if (new Set(passages.map(passage => passage.anchor)).size !== passages.length) {
|
|
671
|
+
throw new Error(`document heading anchors are not unique: ${archiveMemberPath}`);
|
|
672
|
+
}
|
|
619
673
|
const bodySha256 = string(item.body_sha256, "manifest document body hash");
|
|
620
674
|
const fileSha256 = string(item.file_sha256, "manifest document file hash");
|
|
621
675
|
const sizeBytes = count(item.size_bytes, "manifest document size");
|
|
@@ -633,8 +687,11 @@ export async function verifyDocumentationRelease(
|
|
|
633
687
|
fileSha256,
|
|
634
688
|
sizeBytes,
|
|
635
689
|
markdown,
|
|
690
|
+
metadata,
|
|
691
|
+
passages,
|
|
636
692
|
};
|
|
637
693
|
});
|
|
694
|
+
validateDocumentationGraph(documents);
|
|
638
695
|
|
|
639
696
|
const marketingDocumentPaths = new Set(
|
|
640
697
|
documents.filter(document => document.source === "www-f5-com").map(document => document.stablePath),
|
|
@@ -690,6 +747,7 @@ function fingerprint(snapshot: VerifiedDocumentationSnapshot): string {
|
|
|
690
747
|
return sha256Bytes(
|
|
691
748
|
JSON.stringify({
|
|
692
749
|
archiveSha256: snapshot.archiveSha256,
|
|
750
|
+
indexSchema: DOCUMENTATION_INDEX_SCHEMA_VERSION,
|
|
693
751
|
releaseTag: snapshot.releaseTag,
|
|
694
752
|
sourceCommit: snapshot.sourceCommit,
|
|
695
753
|
qmdVersion: "2.8.3",
|
|
@@ -739,8 +797,47 @@ function canonicalizeDatabase(
|
|
|
739
797
|
file_sha256 TEXT NOT NULL,
|
|
740
798
|
size_bytes INTEGER NOT NULL,
|
|
741
799
|
markdown TEXT NOT NULL,
|
|
800
|
+
canonical_url TEXT NOT NULL,
|
|
801
|
+
last_updated TEXT,
|
|
802
|
+
product TEXT,
|
|
803
|
+
content_type TEXT NOT NULL,
|
|
804
|
+
task_type TEXT NOT NULL,
|
|
805
|
+
language TEXT NOT NULL,
|
|
806
|
+
lifecycle TEXT NOT NULL,
|
|
807
|
+
replacement_url TEXT,
|
|
808
|
+
aliases_json TEXT NOT NULL,
|
|
809
|
+
related_documents_json TEXT NOT NULL,
|
|
742
810
|
PRIMARY KEY (source, stable_path)
|
|
743
811
|
)`);
|
|
812
|
+
db.exec(`CREATE TABLE documentation_passages (
|
|
813
|
+
source TEXT NOT NULL,
|
|
814
|
+
stable_path TEXT NOT NULL,
|
|
815
|
+
anchor TEXT NOT NULL,
|
|
816
|
+
heading TEXT NOT NULL,
|
|
817
|
+
markdown TEXT NOT NULL,
|
|
818
|
+
ordinal INTEGER NOT NULL,
|
|
819
|
+
qmd_path TEXT NOT NULL UNIQUE,
|
|
820
|
+
PRIMARY KEY (source, stable_path, anchor)
|
|
821
|
+
)`);
|
|
822
|
+
db.exec(`CREATE INDEX documentation_metadata_filters ON documentation_documents (
|
|
823
|
+
product, content_type, task_type, language, lifecycle, source
|
|
824
|
+
)`);
|
|
825
|
+
db.exec(`CREATE INDEX documentation_passage_page ON documentation_passages (source, stable_path, ordinal)`);
|
|
826
|
+
db.exec(`CREATE TABLE documentation_aliases (
|
|
827
|
+
product TEXT NOT NULL,
|
|
828
|
+
alias TEXT NOT NULL,
|
|
829
|
+
PRIMARY KEY (product, alias)
|
|
830
|
+
)`);
|
|
831
|
+
db.exec(`CREATE TABLE documentation_relationships (
|
|
832
|
+
source TEXT NOT NULL,
|
|
833
|
+
stable_path TEXT NOT NULL,
|
|
834
|
+
relation TEXT NOT NULL,
|
|
835
|
+
target_source TEXT NOT NULL,
|
|
836
|
+
target_stable_path TEXT NOT NULL,
|
|
837
|
+
title TEXT NOT NULL,
|
|
838
|
+
canonical_url TEXT NOT NULL,
|
|
839
|
+
PRIMARY KEY (source, stable_path, canonical_url)
|
|
840
|
+
)`);
|
|
744
841
|
db.exec(`CREATE TABLE documentation_assets (
|
|
745
842
|
source TEXT NOT NULL,
|
|
746
843
|
stable_path TEXT NOT NULL,
|
|
@@ -757,11 +854,17 @@ function canonicalizeDatabase(
|
|
|
757
854
|
asset_count: String(snapshot.assets.length),
|
|
758
855
|
document_count: String(snapshot.documents.length),
|
|
759
856
|
fingerprint: indexFingerprint,
|
|
857
|
+
index_schema: String(DOCUMENTATION_INDEX_SCHEMA_VERSION),
|
|
760
858
|
release_tag: snapshot.releaseTag,
|
|
761
859
|
source_commit: snapshot.sourceCommit,
|
|
762
860
|
}).sort(([left], [right]) => left.localeCompare(right)))
|
|
763
861
|
provenance.run(key, value);
|
|
764
|
-
const insertDocument = db.query(
|
|
862
|
+
const insertDocument = db.query(
|
|
863
|
+
"INSERT INTO documentation_documents VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
864
|
+
);
|
|
865
|
+
const insertPassage = db.query("INSERT INTO documentation_passages VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
866
|
+
const insertAlias = db.query("INSERT OR IGNORE INTO documentation_aliases VALUES (?, ?)");
|
|
867
|
+
const insertRelationship = db.query("INSERT INTO documentation_relationships VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
765
868
|
for (const document of snapshot.documents) {
|
|
766
869
|
insertDocument.run(
|
|
767
870
|
document.source,
|
|
@@ -773,7 +876,42 @@ function canonicalizeDatabase(
|
|
|
773
876
|
document.fileSha256,
|
|
774
877
|
document.sizeBytes,
|
|
775
878
|
document.markdown,
|
|
879
|
+
document.metadata.canonicalUrl,
|
|
880
|
+
document.metadata.lastUpdated,
|
|
881
|
+
document.metadata.product,
|
|
882
|
+
document.metadata.contentType,
|
|
883
|
+
document.metadata.taskType,
|
|
884
|
+
document.metadata.language,
|
|
885
|
+
document.metadata.lifecycle,
|
|
886
|
+
document.metadata.replacementUrl,
|
|
887
|
+
JSON.stringify(document.metadata.aliases),
|
|
888
|
+
JSON.stringify(document.metadata.relatedDocuments),
|
|
776
889
|
);
|
|
890
|
+
for (const passage of document.passages) {
|
|
891
|
+
insertPassage.run(
|
|
892
|
+
document.source,
|
|
893
|
+
document.stablePath,
|
|
894
|
+
passage.anchor,
|
|
895
|
+
passage.heading,
|
|
896
|
+
passage.markdown,
|
|
897
|
+
passage.ordinal,
|
|
898
|
+
`${document.stablePath}/index.md#${passage.anchor}`,
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
if (document.metadata.product) {
|
|
902
|
+
for (const alias of document.metadata.aliases) insertAlias.run(document.metadata.product, alias);
|
|
903
|
+
}
|
|
904
|
+
for (const related of document.metadata.relatedDocuments) {
|
|
905
|
+
insertRelationship.run(
|
|
906
|
+
document.source,
|
|
907
|
+
document.stablePath,
|
|
908
|
+
related.relation,
|
|
909
|
+
related.source,
|
|
910
|
+
related.stablePath,
|
|
911
|
+
related.title,
|
|
912
|
+
related.canonicalUrl,
|
|
913
|
+
);
|
|
914
|
+
}
|
|
777
915
|
}
|
|
778
916
|
const insertAsset = db.query("INSERT INTO documentation_assets VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
779
917
|
for (const asset of snapshot.assets) {
|
|
@@ -810,9 +948,15 @@ function verifyIndexedBodies(databasePath: string, snapshot: VerifiedDocumentati
|
|
|
810
948
|
FROM documents d JOIN content c ON c.hash = d.hash WHERE d.active = 1
|
|
811
949
|
ORDER BY d.collection, d.path`)
|
|
812
950
|
.all() as Array<{ source: string; path: string; markdown: string }>;
|
|
813
|
-
|
|
951
|
+
const passageCount = snapshot.documents.reduce((total, document) => total + document.passages.length, 0);
|
|
952
|
+
if (rows.length !== passageCount) throw new Error("documentation index passage count mismatch");
|
|
814
953
|
const expected = new Map(
|
|
815
|
-
snapshot.documents.
|
|
954
|
+
snapshot.documents.flatMap(document =>
|
|
955
|
+
document.passages.map(passage => [
|
|
956
|
+
`${document.source}/${document.stablePath}/index.md#${passage.anchor}`,
|
|
957
|
+
passage.markdown,
|
|
958
|
+
]),
|
|
959
|
+
),
|
|
816
960
|
);
|
|
817
961
|
for (const row of rows) {
|
|
818
962
|
const key = `${row.source}/${row.path.replaceAll("\\", "/")}`;
|
|
@@ -875,15 +1019,27 @@ export async function buildDocumentationIndex(
|
|
|
875
1019
|
});
|
|
876
1020
|
try {
|
|
877
1021
|
for (const document of snapshot.documents) {
|
|
878
|
-
|
|
879
|
-
store.internal.insertDocument(
|
|
880
|
-
document.source,
|
|
881
|
-
`${document.stablePath}/index.md`,
|
|
1022
|
+
const weightedTitle = [
|
|
882
1023
|
document.title,
|
|
883
|
-
document.
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1024
|
+
document.metadata.product,
|
|
1025
|
+
document.metadata.contentType,
|
|
1026
|
+
document.metadata.taskType,
|
|
1027
|
+
...document.metadata.aliases,
|
|
1028
|
+
]
|
|
1029
|
+
.filter(Boolean)
|
|
1030
|
+
.join(" ");
|
|
1031
|
+
for (const passage of document.passages) {
|
|
1032
|
+
const hash = passageHash(document.fileSha256, passage.anchor, passage.markdown);
|
|
1033
|
+
store.internal.insertContent(hash, passage.markdown, FIXED_TIME);
|
|
1034
|
+
store.internal.insertDocument(
|
|
1035
|
+
document.source,
|
|
1036
|
+
`${document.stablePath}/index.md#${passage.anchor}`,
|
|
1037
|
+
weightedTitle,
|
|
1038
|
+
hash,
|
|
1039
|
+
FIXED_TIME,
|
|
1040
|
+
FIXED_TIME,
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
887
1043
|
}
|
|
888
1044
|
store.internal.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
889
1045
|
store.internal.db.exec("PRAGMA journal_mode = DELETE");
|
|
@@ -22,7 +22,7 @@ import type {
|
|
|
22
22
|
} from "../extensibility/extensions";
|
|
23
23
|
import { discoverAndLoadExtensions } from "../extensibility/extensions";
|
|
24
24
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
25
|
-
import {
|
|
25
|
+
import { getBuiltinSlashCommandDiscoveryCandidates, loadSlashCommands } from "../extensibility/slash-commands";
|
|
26
26
|
import { startSessionBridge } from "../remote-control/bridge";
|
|
27
27
|
import type { ModelResolutionSource } from "../session/active-model";
|
|
28
28
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
@@ -254,10 +254,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
254
254
|
|
|
255
255
|
this.hideThinkingBlock = settings.get("hideThinkingBlock");
|
|
256
256
|
|
|
257
|
-
const builtinCommands
|
|
258
|
-
...command,
|
|
259
|
-
discovery: { behavior: "execution", provenance: "xcsh built-in", scope: "native" },
|
|
260
|
-
}));
|
|
257
|
+
const builtinCommands = getBuiltinSlashCommandDiscoveryCandidates();
|
|
261
258
|
const builtinCommandNames = new Set(builtinCommands.map(c => c.name));
|
|
262
259
|
const extensionCommands: SlashCommandDiscoveryCandidate[] = (
|
|
263
260
|
this.session.extensionRunner?.getRegisteredCommandEntries(builtinCommandNames) ?? []
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Documentation Lookup Hierarchy (Offline QMD First)
|
|
2
2
|
|
|
3
3
|
For conceptual, product, operational, and support questions, search `xcsh://documentation/?search=<query>` first.
|
|
4
|
-
Select the best result and read its exact follow-up URL,
|
|
5
|
-
`xcsh://documentation/<source>/<stable-path>/index.md
|
|
4
|
+
Select the best result and read its exact section-qualified follow-up URL,
|
|
5
|
+
`xcsh://documentation/<source>/<stable-path>/index.md#<anchor>`, before answering. Use the source filter when the question
|
|
6
6
|
clearly targets `docs-cloud-f5-com`, `my-f5-com`, or `www-f5-com`. Prefer `www-f5-com` for conceptual and product
|
|
7
7
|
questions, `docs-cloud-f5-com` for configuration and procedures, and `my-f5-com` for troubleshooting and support.
|
|
8
|
-
An explicit source filter is authoritative.
|
|
8
|
+
An explicit source filter is authoritative. Add `product`, `content_type`, `task_type`, `language`, or `lifecycle`
|
|
9
|
+
filters only when the request makes that constraint explicit. Follow related internal documentation links when they
|
|
10
|
+
are relevant, and always surface deprecated or superseded lifecycle warnings and replacements.
|
|
9
11
|
|
|
10
12
|
Render documentation images only when the user explicitly asks to see them. Extract all unique Markdown image references
|
|
11
13
|
from the exact document, resolve relative references against its exact `xcsh://documentation/` URI, and
|
|
@@ -491,11 +491,13 @@ In `bash`, URIs auto-resolve to filesystem paths (e.g., `python skill://my-skill
|
|
|
491
491
|
# Product and ecosystem knowledge
|
|
492
492
|
|
|
493
493
|
For conceptual, product, operational, or support questions about F5 Distributed Cloud, you **MUST**
|
|
494
|
-
search `xcsh://documentation/?search=<query>` first, then read the exact
|
|
495
|
-
`xcsh://documentation/<source>/<stable-path>/index.md
|
|
494
|
+
search `xcsh://documentation/?search=<query>` first, then read the exact section-qualified
|
|
495
|
+
`xcsh://documentation/<source>/<stable-path>/index.md#<anchor>` result before answering. The search result
|
|
496
496
|
is discovery; the exact Markdown document is the evidence.
|
|
497
497
|
Prefer `www-f5-com` for conceptual and product questions, `docs-cloud-f5-com` for configuration and
|
|
498
498
|
procedural questions, and `my-f5-com` for troubleshooting and support. An explicit source filter is authoritative.
|
|
499
|
+
Use `product`, `content_type`, `task_type`, `language`, and `lifecycle` filters when the request explicitly supplies
|
|
500
|
+
those constraints. Follow relevant typed related-document links and surface lifecycle warnings and replacements.
|
|
499
501
|
|
|
500
502
|
### Documentation images
|
|
501
503
|
|
|
@@ -122,6 +122,7 @@ export interface SubcommandDef {
|
|
|
122
122
|
/** Declarative builtin slash command definition used by autocomplete and help UI. */
|
|
123
123
|
export interface BuiltinSlashCommand {
|
|
124
124
|
name: string;
|
|
125
|
+
aliases?: string[];
|
|
125
126
|
description: string;
|
|
126
127
|
/** Subcommands for dropdown completion. */
|
|
127
128
|
subcommands?: SubcommandDef[];
|
|
@@ -137,7 +138,6 @@ interface ParsedBuiltinSlashCommand {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
interface BuiltinSlashCommandSpec extends BuiltinSlashCommand {
|
|
140
|
-
aliases?: string[];
|
|
141
141
|
allowArgs?: boolean;
|
|
142
142
|
/**
|
|
143
143
|
* Handle the command. Return a string to pass remaining text through as prompt input.
|
|
@@ -2508,6 +2508,7 @@ export function getBuiltinSlashCommandInventory() {
|
|
|
2508
2508
|
export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BUILTIN_SLASH_COMMAND_REGISTRY.map(
|
|
2509
2509
|
command => ({
|
|
2510
2510
|
name: command.name,
|
|
2511
|
+
aliases: command.aliases,
|
|
2511
2512
|
description: command.description,
|
|
2512
2513
|
subcommands: command.subcommands,
|
|
2513
2514
|
inlineHint: command.inlineHint,
|