@yadsh/dsh-qa-surface 0.11.1 → 0.12.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.
Files changed (70) hide show
  1. package/lib/access/service.js +2 -1
  2. package/lib/access/service.js.map +1 -1
  3. package/lib/client/QaSessionController.js +24 -2
  4. package/lib/client/QaSessionController.js.map +1 -1
  5. package/lib/client/QaSurface.js +9 -2
  6. package/lib/client/QaSurface.js.map +1 -1
  7. package/lib/client/components/QaChangelog.js +46 -11
  8. package/lib/client/components/QaChangelog.js.map +1 -1
  9. package/lib/client/components/QaSidebar.js +13 -15
  10. package/lib/client/components/QaSidebar.js.map +1 -1
  11. package/lib/client/settings/card.js +2 -2
  12. package/lib/client/settings/card.js.map +1 -1
  13. package/lib/client/settings/sections/docs.js +41 -0
  14. package/lib/client/settings/sections/docs.js.map +1 -0
  15. package/lib/client/settings/sections.js +1 -0
  16. package/lib/client/settings/sections.js.map +1 -1
  17. package/lib/client/slash/SlashPalette.js +4 -13
  18. package/lib/client/slash/SlashPalette.js.map +1 -1
  19. package/lib/client/slash/palette-rows.js +45 -0
  20. package/lib/client/slash/palette-rows.js.map +1 -0
  21. package/lib/client/slash/useSlashNavigation.js +8 -2
  22. package/lib/client/slash/useSlashNavigation.js.map +1 -1
  23. package/lib/client/styles.js +1 -2
  24. package/lib/client/styles.js.map +1 -1
  25. package/lib/client/turn-sources.js +20 -0
  26. package/lib/client/turn-sources.js.map +1 -1
  27. package/lib/client.js +398 -206
  28. package/lib/client.js.map +1 -1
  29. package/lib/config-resolvers/defaults.js +7 -0
  30. package/lib/config-resolvers/defaults.js.map +1 -1
  31. package/lib/config-resolvers/integration.js +7 -1
  32. package/lib/config-resolvers/integration.js.map +1 -1
  33. package/lib/config-resolvers/shared.js +12 -0
  34. package/lib/config-resolvers/shared.js.map +1 -1
  35. package/lib/config-resolvers/tools.js +26 -0
  36. package/lib/config-resolvers/tools.js.map +1 -1
  37. package/lib/config.js +21 -0
  38. package/lib/config.js.map +1 -1
  39. package/lib/index.js +10 -2
  40. package/lib/index.js.map +1 -1
  41. package/lib/launch-token.js +22 -17
  42. package/lib/launch-token.js.map +1 -1
  43. package/lib/qa-tools/catalog.js +13 -2
  44. package/lib/qa-tools/catalog.js.map +1 -1
  45. package/lib/qa-tools/docs-tools.js +334 -32
  46. package/lib/qa-tools/docs-tools.js.map +1 -1
  47. package/lib/qa-tools/index.js +2 -0
  48. package/lib/qa-tools/index.js.map +1 -1
  49. package/lib/secure-session.js +8 -0
  50. package/lib/secure-session.js.map +1 -1
  51. package/lib/shared/docs-version.js +19 -0
  52. package/lib/shared/docs-version.js.map +1 -0
  53. package/lib/typert.host.js +71 -68
  54. package/lib/typert.remote-client.js +70 -67
  55. package/lib/types/client/QaSessionController.d.ts +14 -0
  56. package/lib/types/client/components/QaWidthHandle.d.ts +0 -1
  57. package/lib/types/client/settings/sections/docs.d.ts +15 -0
  58. package/lib/types/client/settings/sections.d.ts +1 -0
  59. package/lib/types/client/slash/palette-rows.d.ts +43 -0
  60. package/lib/types/client/turn-sources.d.ts +14 -0
  61. package/lib/types/config-resolvers/integration.d.ts +7 -1
  62. package/lib/types/config-resolvers/shared.d.ts +8 -0
  63. package/lib/types/config-resolvers/tools.d.ts +9 -0
  64. package/lib/types/launch-token.d.ts +10 -3
  65. package/lib/types/qa-tools/catalog.d.ts +11 -1
  66. package/lib/types/qa-tools/docs-tools.d.ts +38 -5
  67. package/lib/types/qa-tools/index.d.ts +7 -0
  68. package/lib/types/shared/docs-version.d.ts +15 -0
  69. package/lib/types/types.d.ts +34 -0
  70. package/package.json +5 -5
@@ -10,6 +10,9 @@ import { createDocsReadTool, createDocsSearchTool } from "./docs-tools.js";
10
10
  * Bump it whenever the catalog's tool set or their schemas change.
11
11
  */
12
12
  export const QA_TOOL_CATALOG_VERSION = "3";
13
+ function current(value) {
14
+ return typeof value === "function" ? value() : (value ?? "");
15
+ }
13
16
  function createSelfcheckTool(deps) {
14
17
  return defineTool({
15
18
  name: "qa_tools_selfcheck",
@@ -75,6 +78,14 @@ function createSelfcheckTool(deps) {
75
78
  * activation manager owns every registration.
76
79
  */
77
80
  export function createQaToolCatalog(deps) {
81
+ const docsOptions = {
82
+ get root() {
83
+ return current(deps.docsRoot);
84
+ },
85
+ get defaultVersion() {
86
+ return current(deps.docsDefaultVersion);
87
+ },
88
+ };
78
89
  return [
79
90
  {
80
91
  definition: createSelfcheckTool(deps),
@@ -82,12 +93,12 @@ export function createQaToolCatalog(deps) {
82
93
  tags: ["activation", "diagnostics"],
83
94
  },
84
95
  {
85
- definition: createDocsSearchTool(),
96
+ definition: createDocsSearchTool(docsOptions),
86
97
  group: "documentation",
87
98
  tags: ["docs", "search"],
88
99
  },
89
100
  {
90
- definition: createDocsReadTool(),
101
+ definition: createDocsReadTool(docsOptions),
91
102
  group: "documentation",
92
103
  tags: ["docs", "read"],
93
104
  },
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/qa-tools/catalog.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/qa-tools/catalog.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { lstat, readFile, readdir } from "node:fs/promises";
2
- import { isAbsolute, join, relative, resolve } from "node:path";
2
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { defineTool } from "@deepseek-ai/dsh-tools";
4
+ import { isDocumentationVersion } from "../shared/docs-version.js";
4
5
  import { canonicalCandidate, pathIsInside } from "../user-workspace.js";
5
6
  /**
6
7
  * `docs_search` and `docs_read` — the documentation surface of the QA catalog.
@@ -22,6 +23,14 @@ import { canonicalCandidate, pathIsInside } from "../user-workspace.js";
22
23
  * names are accepted as filters. A path the convention does not describe is
23
24
  * still searched and still read — it simply carries no identity.
24
25
  *
26
+ * A real corpus also carries its own working material: the stand publishes an
27
+ * asset tree, inventories and triage notes beside the documents, under leading
28
+ * underscores. None of that is what a question is asked about, and an inventory
29
+ * names every module and version there is, so a walk that met it first spent the
30
+ * whole answer on it. The walk therefore visits the documented modules first,
31
+ * loose files next, and the underscored service material last — searched, but
32
+ * never at the expense of the documents.
33
+ *
25
34
  * Nothing here is destructive and building it registers nothing: like the rest
26
35
  * of the catalog these are side-effect-free definitions, and the workspace root
27
36
  * is read from `exec.agent` at call time.
@@ -36,10 +45,76 @@ export const QA_DOCS_READ_DEFAULT_LINES = 120;
36
45
  export const QA_DOCS_READ_MAX_LINES = 400;
37
46
  /** One file larger than this is not documentation a chat needs to search. */
38
47
  const MAX_FILE_BYTES = 2 * 1024 * 1024;
39
- /** Bound on the walk itself, so a huge tree cannot stall the turn. */
48
+ /** Bound on how many documents one search reads. */
40
49
  const MAX_FILES_SCANNED = 2_000;
50
+ /**
51
+ * Bound on how many entries one walk may look at, so a corpus with a huge asset
52
+ * tree cannot stall the turn even though nothing in it is read.
53
+ */
54
+ const MAX_WALK_ENTRIES = 50_000;
41
55
  /** A matched line is trimmed to this many characters before it is reported. */
42
56
  const MAX_LINE_CHARS = 240;
57
+ /**
58
+ * Extensions of material that is not documentation text: pictures, audio and
59
+ * video, archives, office binaries, fonts. A published corpus keeps them beside
60
+ * its prose — four fifths of the stand's own tree is images — and learning that
61
+ * by reading each one spends a search's whole document budget before it reaches
62
+ * a document. These are counted as skipped material, never as documents read.
63
+ */
64
+ const NON_TEXT_EXTENSIONS = new Set([
65
+ "avif",
66
+ "bmp",
67
+ "bz2",
68
+ "class",
69
+ "dll",
70
+ "doc",
71
+ "docx",
72
+ "dylib",
73
+ "eot",
74
+ "exe",
75
+ "flac",
76
+ "gif",
77
+ "gz",
78
+ "heic",
79
+ "ico",
80
+ "jar",
81
+ "jpeg",
82
+ "jfif",
83
+ "jpg",
84
+ "m4a",
85
+ "m4v",
86
+ "mkv",
87
+ "mov",
88
+ "mp3",
89
+ "mp4",
90
+ "odp",
91
+ "ods",
92
+ "odt",
93
+ "ogg",
94
+ "otf",
95
+ "pdf",
96
+ "png",
97
+ "ppt",
98
+ "pptx",
99
+ "rar",
100
+ "sqlite",
101
+ "svg",
102
+ "tar",
103
+ "tgz",
104
+ "tif",
105
+ "tiff",
106
+ "ttf",
107
+ "wav",
108
+ "wasm",
109
+ "webm",
110
+ "webp",
111
+ "woff",
112
+ "woff2",
113
+ "xls",
114
+ "xlsx",
115
+ "xz",
116
+ "zip",
117
+ ]);
43
118
  const TEXT_DECODER = new TextDecoder("utf-8", { fatal: true });
44
119
  /**
45
120
  * A typed refusal of one documentation call.
@@ -61,14 +136,38 @@ const DOCS_UNAVAILABLE = `this chat's workspace has no ${QA_DOCS_DIRECTORY}/ dir
61
136
  const DOCS_NOT_A_DIRECTORY = `the ${QA_DOCS_DIRECTORY}/ entry in this chat's workspace is not a real directory (it is a file or a link), and documentation is never followed through a link.`;
62
137
  const DOCS_ESCAPE = `the ${QA_DOCS_DIRECTORY}/ directory resolves outside the calling chat's workspace, so the documentation tree is refused.`;
63
138
  const INVALID_QUERY = "no query was given: pass the phrase to look for.";
64
- const OUTSIDE_DOCS = `this path resolves outside the documentation directory (${QA_DOCS_DIRECTORY}/) of the calling chat's workspace. Documentation is only read where the stand publishes it; pass a path inside ${QA_DOCS_DIRECTORY}/.`;
139
+ const OUTSIDE_DOCS = `this path resolves outside the documentation tree (${QA_DOCS_DIRECTORY}/) this stand reads. Documentation is only read where the stand publishes it; pass a path inside ${QA_DOCS_DIRECTORY}/.`;
140
+ /**
141
+ * Refusals of a configured root. The operator named the path, so the message
142
+ * names it too: a typo in a deployment file is not something the model can
143
+ * diagnose from "documentation is unavailable".
144
+ */
145
+ const configuredMissing = (root) => `this deployment publishes its documentation at ${root}, and nothing is there. Report that documentation is unavailable rather than guessing its contents.`;
146
+ const configuredNotADirectory = (root) => `this deployment publishes its documentation at ${root}, which is not a real directory (it is a file or a link), and documentation is never followed through a link.`;
147
+ const CONFIGURED_NOT_ABSOLUTE = "the configured documentation root is not an absolute path, so the documentation tree cannot be located. Report that documentation is unavailable rather than guessing its contents.";
65
148
  const SYMLINK_ESCAPE = "this path leaves the documentation tree through a symbolic link, and links are not followed out of it.";
66
149
  const NOT_FOUND = "no documentation file or directory exists at this path. Check the spelling against a path docs_search reported.";
67
150
  const NOT_A_FILE = "this path is a directory, not a documentation file: pass the file to read, or use docs_search to find one.";
68
151
  const NOT_TEXT = "this path does not read as UTF-8 text (or is larger than the readable limit), so it is not documentation this tool can return.";
69
- const VERSION_SEGMENT = /^v?\d+(?:\.\d+)*$/iu;
152
+ /**
153
+ * The version one search runs under: the caller's own, the deployment's
154
+ * default, or none.
155
+ *
156
+ * An explicit `version` is the model saying which edition it means. An explicit
157
+ * `path` names a subtree the default has no business overriding — a corpus
158
+ * scoped to `platform/3.5` stays reachable on a stand whose default is `3.8` —
159
+ * so a call that named a path is left to that path alone.
160
+ */
161
+ function versionFor(options, args) {
162
+ const asked = (args.version ?? "").trim();
163
+ if (asked !== "")
164
+ return asked;
165
+ if ((args.path ?? "").trim() !== "")
166
+ return "";
167
+ return (options.defaultVersion ?? "").trim();
168
+ }
70
169
  function normalizeVersion(value) {
71
- return value.replace(/^v/u, "").toLowerCase();
170
+ return value.toLowerCase().replace(/^v/u, "");
72
171
  }
73
172
  /**
74
173
  * Identity of one documentation path, read relative to the documentation root.
@@ -86,7 +185,7 @@ export function docsIdentityOf(pathFromDocsRoot) {
86
185
  .filter((segment) => segment !== "");
87
186
  if (segments.length === 0)
88
187
  return identityOf(undefined, undefined);
89
- const versionIndex = segments.findIndex((segment) => VERSION_SEGMENT.test(segment));
188
+ const versionIndex = segments.findIndex((segment) => isDocumentationVersion(segment));
90
189
  if (versionIndex === -1)
91
190
  return identityOf(undefined, segments[0]);
92
191
  return identityOf(segments[versionIndex], versionIndex > 0 ? segments[versionIndex - 1] : undefined);
@@ -113,8 +212,15 @@ function moduleMatches(identity, wanted) {
113
212
  * The root is a real directory inside the canonical workspace: a `docs` entry
114
213
  * that is a link or a file is refused rather than followed, because the fence
115
214
  * is the point of the tool and a link is how a fence is left.
215
+ *
216
+ * A deployment that names its corpus once (`QaDocsOptions.root`) skips the
217
+ * per-chat lookup entirely — that layout is exactly why it configures the root —
218
+ * and is held to the same rules: absolute, a real directory, no links.
116
219
  */
117
- export async function docsRootOf(exec) {
220
+ export async function docsRootOf(exec, options = {}) {
221
+ const configured = (options.root ?? "").trim();
222
+ if (configured !== "")
223
+ return await configuredDocsRoot(configured);
118
224
  const cwd = exec.agent?.session.header.cwd;
119
225
  if (typeof cwd !== "string" || cwd.trim() === "") {
120
226
  throw new QaDocsError("workspace-unavailable", WORKSPACE_UNAVAILABLE);
@@ -149,6 +255,37 @@ export async function docsRootOf(exec) {
149
255
  }
150
256
  return { workspace, docs };
151
257
  }
258
+ /**
259
+ * The documentation root a deployment named.
260
+ *
261
+ * The parent directory plays the part the chat's workspace plays in the
262
+ * per-chat layout: `docs/`-relative reporting, the fence and every path rule
263
+ * below are unchanged, so a configured corpus is read by the same rules as a
264
+ * published-into-the-workspace one.
265
+ */
266
+ async function configuredDocsRoot(configured) {
267
+ if (!isAbsolute(configured)) {
268
+ throw new QaDocsError("docs-unavailable", CONFIGURED_NOT_ABSOLUTE);
269
+ }
270
+ const entry = await lstat(configured).catch((error) => error);
271
+ if (entry instanceof Error) {
272
+ if (entry.code === "ENOENT") {
273
+ throw new QaDocsError("docs-unavailable", configuredMissing(configured));
274
+ }
275
+ throw new QaDocsError("workspace-unavailable", WORKSPACE_UNAVAILABLE);
276
+ }
277
+ if (entry.isSymbolicLink() || !entry.isDirectory()) {
278
+ throw new QaDocsError("docs-unavailable", configuredNotADirectory(configured));
279
+ }
280
+ let docs;
281
+ try {
282
+ docs = canonicalCandidate(configured);
283
+ }
284
+ catch {
285
+ throw new QaDocsError("workspace-unavailable", WORKSPACE_UNAVAILABLE);
286
+ }
287
+ return { workspace: dirname(docs), docs };
288
+ }
152
289
  /**
153
290
  * Resolve one tool path against the documentation root.
154
291
  *
@@ -163,11 +300,15 @@ async function docsTargetOf(root, input) {
163
300
  throw new QaDocsError("outside-docs", OUTSIDE_DOCS);
164
301
  }
165
302
  const normalized = trimmed.replace(/\\/gu, "/").replace(/^\.\//u, "");
166
- const inside = normalized === QA_DOCS_DIRECTORY ||
167
- normalized.startsWith(`${QA_DOCS_DIRECTORY}/`)
168
- ? normalized
169
- : `${QA_DOCS_DIRECTORY}/${normalized}`;
170
- const spelled = resolve(root.workspace, inside);
303
+ // `docs/` is the stable public spelling, not necessarily the configured
304
+ // directory's basename. Resolve both accepted spellings from the canonical
305
+ // root itself so `/srv/published-corpus` still reads `docs/module/file.md`.
306
+ const pathFromDocs = normalized === QA_DOCS_DIRECTORY
307
+ ? ""
308
+ : normalized.startsWith(`${QA_DOCS_DIRECTORY}/`)
309
+ ? normalized.slice(QA_DOCS_DIRECTORY.length + 1)
310
+ : normalized;
311
+ const spelled = resolve(root.docs, pathFromDocs);
171
312
  if (!pathIsInside(spelled, root.docs)) {
172
313
  throw new QaDocsError("outside-docs", OUTSIDE_DOCS);
173
314
  }
@@ -232,20 +373,95 @@ function clamp(value, low, high) {
232
373
  return Math.min(Math.max(Math.trunc(value), low), high);
233
374
  }
234
375
  /**
235
- * Visit every documentation file of one target, in path order.
376
+ * Whether a name is the corpus's own working material: the asset tree, the
377
+ * inventories, the triage notes and anything else a publisher underscored or
378
+ * dotted.
379
+ */
380
+ function isServiceName(name) {
381
+ return name.startsWith("_") || name.startsWith(".");
382
+ }
383
+ /** Whether any segment of a path is such material. */
384
+ function isServicePath(pathFromDocs) {
385
+ return pathFromDocs.split(/[\\/]/u).some(isServiceName);
386
+ }
387
+ /**
388
+ * How the entries of one directory are ordered: documents before service
389
+ * material.
390
+ *
391
+ * A corpus keeps its own working material beside the documents, and the stand's
392
+ * names it with a leading underscore. Name order puts that material first (`_`
393
+ * sorts before letters), which is exactly where a bounded answer must not spend
394
+ * itself: an inventory names every module and version the corpus has, so it
395
+ * matches the question and consumes the budget before a document is opened.
396
+ * Module directories come first, then loose files at the same level, then
397
+ * everything underscored or dotted — which is still searched, just after the
398
+ * documentation. Material inside a service directory stays service material,
399
+ * however its own entries are named.
400
+ */
401
+ function entryRank(parentIsService, entry) {
402
+ if (parentIsService || isServiceName(entry.name))
403
+ return 2;
404
+ return entry.isDirectory() ? 0 : 1;
405
+ }
406
+ function entryComparator(parentIsService) {
407
+ return (left, right) => {
408
+ const rank = entryRank(parentIsService, left) - entryRank(parentIsService, right);
409
+ if (rank !== 0)
410
+ return rank;
411
+ return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
412
+ };
413
+ }
414
+ /**
415
+ * Which queued directory the walk opens next.
236
416
  *
237
- * Symbolic links are counted as skipped and never followed. The visitor
238
- * returns `stop` to end the walk, which is how the output budget ends it
239
- * early.
417
+ * Rank and path decide, not the order directories happened to be queued in: the
418
+ * corpus's own `_meta` at the top of the tree must not be read before a
419
+ * documented module's edition one level below it, which is what a breadth-first
420
+ * queue did — the inventories answered every question about the product before
421
+ * a document was reached.
240
422
  */
241
- async function visitDocumentation(target, visit) {
423
+ function scopeComparator(left, right) {
424
+ const rank = Number(isServicePath(left.pathFromDocs)) -
425
+ Number(isServicePath(right.pathFromDocs));
426
+ if (rank !== 0)
427
+ return rank;
428
+ if (left.pathFromDocs === right.pathFromDocs)
429
+ return 0;
430
+ return left.pathFromDocs < right.pathFromDocs ? -1 : 1;
431
+ }
432
+ /**
433
+ * Whether a name is material the walk never reads: pictures, media, archives,
434
+ * office binaries and fonts, recognised by extension.
435
+ */
436
+ function isNonTextName(name) {
437
+ const dot = name.lastIndexOf(".");
438
+ if (dot <= 0)
439
+ return false;
440
+ return NON_TEXT_EXTENSIONS.has(name.slice(dot + 1).toLowerCase());
441
+ }
442
+ /**
443
+ * Visit every documentation file of one target, in walk order.
444
+ *
445
+ * Symbolic links are counted as skipped and never followed. A file the caller's
446
+ * scope excludes is neither read nor counted, and material that is not text is
447
+ * counted as skipped rather than read, so the document budget below is spent on
448
+ * documents. The visitor returns `stop` to end the walk, which is how the output
449
+ * budget ends it early.
450
+ */
451
+ async function visitDocumentation(target, accept, descend, visit) {
242
452
  let filesScanned = 0;
453
+ let entriesSeen = 0;
243
454
  let skipped = 0;
244
455
  const info = await lstat(target.absolute).catch(() => undefined);
245
456
  if (info === undefined) {
246
457
  throw new QaDocsError("not-found", NOT_FOUND);
247
458
  }
248
459
  if (info.isFile()) {
460
+ // A path the caller named explicitly is read as it is: out of scope means
461
+ // no hits, not a refusal, and the refusal it may get comes from the reader.
462
+ if (!accept(target.pathFromDocs)) {
463
+ return { filesScanned, skipped, stopped: false };
464
+ }
249
465
  return {
250
466
  filesScanned: 1,
251
467
  skipped,
@@ -257,24 +473,47 @@ async function visitDocumentation(target, visit) {
257
473
  }
258
474
  const pending = [target];
259
475
  while (pending.length > 0) {
476
+ // Ranked by path, so the queue's order is the walk's order.
477
+ pending.sort(scopeComparator);
260
478
  const scope = pending.shift();
479
+ const serviceScope = isServicePath(scope.pathFromDocs);
261
480
  const entries = await readdir(scope.absolute, {
262
481
  withFileTypes: true,
263
482
  }).catch(() => []);
264
- entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
483
+ entries.sort(entryComparator(serviceScope));
265
484
  for (const entry of entries) {
266
485
  const child = join(scope.absolute, entry.name);
267
486
  const pathFromDocs = join(scope.pathFromDocs, entry.name);
268
487
  if (entry.isSymbolicLink()) {
488
+ entriesSeen += 1;
489
+ if (entriesSeen > MAX_WALK_ENTRIES) {
490
+ return { filesScanned, skipped, stopped: true };
491
+ }
269
492
  skipped += 1;
270
493
  continue;
271
494
  }
272
495
  if (entry.isDirectory()) {
496
+ if (!descend(pathFromDocs))
497
+ continue;
498
+ entriesSeen += 1;
499
+ if (entriesSeen > MAX_WALK_ENTRIES) {
500
+ return { filesScanned, skipped, stopped: true };
501
+ }
273
502
  pending.push({ absolute: child, pathFromDocs });
274
503
  continue;
275
504
  }
276
505
  if (!entry.isFile())
277
506
  continue;
507
+ if (!accept(pathFromDocs))
508
+ continue;
509
+ entriesSeen += 1;
510
+ if (entriesSeen > MAX_WALK_ENTRIES) {
511
+ return { filesScanned, skipped, stopped: true };
512
+ }
513
+ if (isNonTextName(entry.name)) {
514
+ skipped += 1;
515
+ continue;
516
+ }
278
517
  filesScanned += 1;
279
518
  if (filesScanned > MAX_FILES_SCANNED) {
280
519
  return { filesScanned, skipped, stopped: true };
@@ -321,9 +560,24 @@ export async function searchDocumentation(root, request) {
321
560
  return false;
322
561
  return true;
323
562
  };
563
+ const wantedScope = (pathFromDocs) => {
564
+ // `docsIdentityOf` expects a file path. A synthetic leaf makes the current
565
+ // directory participate as a scope while still allowing an ancestor that
566
+ // has not reached its module/version segment yet.
567
+ const identity = docsIdentityOf(join(pathFromDocs, "__scope__"));
568
+ if (version !== "" &&
569
+ identity.version !== undefined &&
570
+ !versionMatches(identity, version)) {
571
+ return false;
572
+ }
573
+ if (module !== "" &&
574
+ identity.module !== undefined &&
575
+ !moduleMatches(identity, module)) {
576
+ return false;
577
+ }
578
+ return true;
579
+ };
324
580
  const visit = async (file, pathFromDocs, explicit) => {
325
- if (!wanted(pathFromDocs))
326
- return "continue";
327
581
  let lines;
328
582
  try {
329
583
  lines = await readDocLines(file);
@@ -356,7 +610,7 @@ export async function searchDocumentation(root, request) {
356
610
  }
357
611
  return "continue";
358
612
  };
359
- const walk = await visitDocumentation(scope, visit);
613
+ const walk = await visitDocumentation(scope, wanted, wantedScope, visit);
360
614
  return {
361
615
  ...identityOf(version === "" ? undefined : version, module === "" ? undefined : module),
362
616
  query,
@@ -397,7 +651,12 @@ export async function readDocumentation(root, request) {
397
651
  text: lines.slice(from - 1, Math.max(to, from - 1)).join("\n"),
398
652
  };
399
653
  }
400
- function filtersOf(identity) {
654
+ /**
655
+ * The facets an answer ran under, as one leading-space clause the callers
656
+ * interpolate. `note` rides beside them in a report's header — never on a hit's
657
+ * own label, where it would repeat on every line.
658
+ */
659
+ function filtersOf(identity, note = "") {
401
660
  const parts = [];
402
661
  if (identity.version !== undefined && identity.version !== "") {
403
662
  parts.push(`version ${identity.version}`);
@@ -405,7 +664,9 @@ function filtersOf(identity) {
405
664
  if (identity.module !== undefined && identity.module !== "") {
406
665
  parts.push(`module ${identity.module}`);
407
666
  }
408
- return parts.length === 0 ? "" : ` (${parts.join(", ")})`;
667
+ const facets = parts.join(", ");
668
+ const described = note === "" ? facets : facets === "" ? note : `${facets}, ${note}`;
669
+ return described === "" ? "" : ` (${described})`;
409
670
  }
410
671
  function identityLabel(identity) {
411
672
  return filtersOf(identity);
@@ -418,10 +679,10 @@ function identityLabel(identity) {
418
679
  * version/module facets exist so a model that has been told "3.8" stops
419
680
  * sweeping the whole tree.
420
681
  */
421
- export function createDocsSearchTool() {
682
+ export function createDocsSearchTool(options = {}) {
422
683
  return defineTool({
423
684
  name: "docs_search",
424
- description: `Search the stand's documentation, which lives in the ${QA_DOCS_DIRECTORY}/ directory of this chat's workspace. Use it — not memory and not a glob sweep over guessed paths — whenever a question is about the product, a version or a module. Hits are lines, tagged with the module and version parsed from the path (${QA_DOCS_DIRECTORY}/<module>/<version>/…); pass version and module to stay inside one edition, and path to stay inside one subtree. Output is bounded by limit and by a byte budget, and a truncated result says so.`,
685
+ description: `Search the stand's documentation, which lives ${docsLayout(options)}. Use it — not memory and not a glob sweep over guessed paths — whenever a question is about the product, a version or a module. Hits are lines, tagged with the module and version parsed from the path (${QA_DOCS_DIRECTORY}/<module>/<version>/…); pass version and module to stay inside one edition, and path to stay inside one subtree. Output is bounded by limit and by a byte budget, and a truncated result says so.${defaultVersionNote(options)}`,
425
686
  parameters: {
426
687
  query: {
427
688
  type: "string",
@@ -470,11 +731,15 @@ export function createDocsSearchTool() {
470
731
  truncated: { type: "boolean", required: true },
471
732
  filesScanned: { type: "integer", required: true },
472
733
  skipped: { type: "integer", required: true },
734
+ // A search answers with the facets it ran under, so a filtered result
735
+ // says which edition of which module it is speaking about.
736
+ version: { type: "string" },
737
+ module: { type: "string" },
473
738
  },
474
739
  },
475
- render: (_args, value) => {
740
+ render: (args, value) => {
476
741
  const result = value;
477
- const filters = filtersOf(result);
742
+ const filters = filtersOf(result, defaultedVersion(options, args) ? "the stand's default" : "");
478
743
  if (result.hits.length === 0) {
479
744
  return [
480
745
  {
@@ -493,10 +758,10 @@ export function createDocsSearchTool() {
493
758
  },
494
759
  },
495
760
  execute: async (args, exec) => {
496
- const root = await docsRootOf(exec);
761
+ const root = await docsRootOf(exec, options);
497
762
  return searchDocumentation(root, {
498
763
  query: args.query,
499
- version: args.version,
764
+ version: versionFor(options, args),
500
765
  module: args.module,
501
766
  path: args.path,
502
767
  limit: args.limit,
@@ -504,11 +769,48 @@ export function createDocsSearchTool() {
504
769
  },
505
770
  });
506
771
  }
772
+ /**
773
+ * Whether the version in an answer is the deployment's default rather than one
774
+ * the caller named — the difference between "I looked in 3.8" and "the stand
775
+ * looked in 3.8", which the report has to keep.
776
+ */
777
+ function defaultedVersion(options, args) {
778
+ const request = (args ?? {});
779
+ const asked = request.version;
780
+ if (typeof asked === "string" && asked.trim() !== "")
781
+ return false;
782
+ if (typeof request.path === "string" && request.path.trim() !== "") {
783
+ return false;
784
+ }
785
+ return (options.defaultVersion ?? "").trim() !== "";
786
+ }
787
+ /**
788
+ * The sentence that tells the model which edition a search it did not scope
789
+ * itself will stay inside. Without it the model reads a filtered answer as the
790
+ * whole corpus and reports a missing document rather than a narrowed search.
791
+ */
792
+ function defaultVersionNote(options) {
793
+ const version = (options.defaultVersion ?? "").trim();
794
+ return version === ""
795
+ ? ""
796
+ : ` This stand documents version ${version} by default: a search without version and without path stays inside that edition, and pass version or path to look at another one.`;
797
+ }
798
+ /**
799
+ * How the tool description names the tree: the model has to know where the
800
+ * documentation is before it decides to look, and a configured corpus is named
801
+ * by the stand rather than by the chat, so the sentence differs.
802
+ */
803
+ function docsLayout(options) {
804
+ const root = (options.root ?? "").trim();
805
+ return root === ""
806
+ ? `in the ${QA_DOCS_DIRECTORY}/ directory of this chat's workspace`
807
+ : `in the ${QA_DOCS_DIRECTORY}/ tree this stand publishes (reported as ${QA_DOCS_DIRECTORY}/<module>/<version>/…)`;
808
+ }
507
809
  /** `docs_read` — open one documentation file at a bounded window of lines. */
508
- export function createDocsReadTool() {
810
+ export function createDocsReadTool(options = {}) {
509
811
  return defineTool({
510
812
  name: "docs_read",
511
- description: `Read one documentation file of this chat's workspace, ${QA_DOCS_DIRECTORY}/, at a bounded window of lines. Use it after docs_search reports a path, to read the passage a hit belongs to. Only files inside ${QA_DOCS_DIRECTORY}/ can be read; directories, paths outside the tree and paths that leave it through a symbolic link are refused with the reason.`,
813
+ description: `Read one documentation file of the stand's documentation, which lives ${docsLayout(options)}, at a bounded window of lines. Use it after docs_search reports a path, to read the passage a hit belongs to. Only files inside ${QA_DOCS_DIRECTORY}/ can be read; directories, paths outside the tree and paths that leave it through a symbolic link are refused with the reason.`,
512
814
  parameters: {
513
815
  path: {
514
816
  type: "string",
@@ -551,7 +853,7 @@ export function createDocsReadTool() {
551
853
  },
552
854
  },
553
855
  execute: async (args, exec) => {
554
- const root = await docsRootOf(exec);
856
+ const root = await docsRootOf(exec, options);
555
857
  return readDocumentation(root, {
556
858
  path: args.path,
557
859
  from: args.from,