@gmickel/gno 1.18.0 → 1.19.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 (129) hide show
  1. package/README.md +5 -3
  2. package/assets/skill/SKILL.md +27 -0
  3. package/package.json +2 -1
  4. package/spec/AGENTS.md +83 -0
  5. package/spec/CLAUDE.md +83 -0
  6. package/spec/bench-fixture.schema.json +137 -0
  7. package/spec/cli.md +2894 -0
  8. package/spec/db/schema.sql +442 -0
  9. package/spec/evals-agentic.md +510 -0
  10. package/spec/evals.md +1106 -0
  11. package/spec/mcp.md +2229 -0
  12. package/spec/output-schemas/activation-verification.schema.json +515 -0
  13. package/spec/output-schemas/ask.schema.json +366 -0
  14. package/spec/output-schemas/backlinks.schema.json +131 -0
  15. package/spec/output-schemas/bench-result.schema.json +120 -0
  16. package/spec/output-schemas/capture-receipt.schema.json +143 -0
  17. package/spec/output-schemas/collection-list.schema.json +45 -0
  18. package/spec/output-schemas/context-capsule-v1.schema.json +691 -0
  19. package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
  20. package/spec/output-schemas/context-list.schema.json +21 -0
  21. package/spec/output-schemas/doctor.schema.json +313 -0
  22. package/spec/output-schemas/error.schema.json +30 -0
  23. package/spec/output-schemas/expansion.schema.json +37 -0
  24. package/spec/output-schemas/get.schema.json +140 -0
  25. package/spec/output-schemas/graph-query.schema.json +99 -0
  26. package/spec/output-schemas/graph.schema.json +371 -0
  27. package/spec/output-schemas/links-list.schema.json +186 -0
  28. package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
  29. package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
  30. package/spec/output-schemas/mcp-http-error.schema.json +30 -0
  31. package/spec/output-schemas/mcp-job-list.schema.json +58 -0
  32. package/spec/output-schemas/mcp-job-status.schema.json +224 -0
  33. package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
  34. package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
  35. package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
  36. package/spec/output-schemas/models-list.schema.json +93 -0
  37. package/spec/output-schemas/multi-get.schema.json +103 -0
  38. package/spec/output-schemas/process-status.schema.json +119 -0
  39. package/spec/output-schemas/query-diagnose.schema.json +123 -0
  40. package/spec/output-schemas/resident-status.schema.json +154 -0
  41. package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
  42. package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
  43. package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
  44. package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
  45. package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
  46. package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
  47. package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
  48. package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
  49. package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
  50. package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
  51. package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
  52. package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
  53. package/spec/output-schemas/search-result.schema.json +154 -0
  54. package/spec/output-schemas/search-results.schema.json +338 -0
  55. package/spec/output-schemas/similar.schema.json +84 -0
  56. package/spec/output-schemas/status.schema.json +676 -0
  57. package/spec/output-schemas/tags-list.schema.json +48 -0
  58. package/src/app/context-runtime-types.ts +3 -0
  59. package/src/app/context-runtime.ts +15 -1
  60. package/src/cli/commands/ask.ts +106 -36
  61. package/src/cli/commands/context-build.ts +56 -9
  62. package/src/cli/commands/get.ts +64 -3
  63. package/src/cli/commands/query.ts +62 -23
  64. package/src/cli/commands/replay.ts +140 -0
  65. package/src/cli/commands/search.ts +48 -3
  66. package/src/cli/commands/shared.ts +3 -1
  67. package/src/cli/commands/trace.ts +200 -0
  68. package/src/cli/commands/vsearch.ts +75 -53
  69. package/src/cli/program.ts +255 -0
  70. package/src/config/index.ts +9 -0
  71. package/src/config/retrieval-traces.ts +56 -0
  72. package/src/config/types.ts +4 -0
  73. package/src/core/context-compiler.ts +11 -4
  74. package/src/core/retrieval-qrels.ts +405 -0
  75. package/src/core/retrieval-replay-candidate.ts +368 -0
  76. package/src/core/retrieval-replay-types.ts +109 -0
  77. package/src/core/retrieval-replay-validation.ts +89 -0
  78. package/src/core/retrieval-replay.ts +441 -0
  79. package/src/core/retrieval-trace-evidence-origin.ts +175 -0
  80. package/src/core/retrieval-trace-export.ts +113 -0
  81. package/src/core/retrieval-trace-filter-normalization.ts +27 -0
  82. package/src/core/retrieval-trace-filters.ts +19 -0
  83. package/src/core/retrieval-trace-management-helpers.ts +247 -0
  84. package/src/core/retrieval-trace-management-types.ts +132 -0
  85. package/src/core/retrieval-trace-management.ts +422 -0
  86. package/src/core/retrieval-trace-request.ts +141 -0
  87. package/src/core/retrieval-trace-session.ts +494 -0
  88. package/src/core/retrieval-trace.ts +472 -0
  89. package/src/mcp/tools/context.ts +59 -8
  90. package/src/mcp/tools/get.ts +35 -1
  91. package/src/mcp/tools/index.ts +74 -0
  92. package/src/mcp/tools/query.ts +95 -64
  93. package/src/mcp/tools/search.ts +36 -13
  94. package/src/mcp/tools/trace.ts +143 -0
  95. package/src/mcp/tools/vsearch.ts +71 -38
  96. package/src/pipeline/answer.ts +167 -26
  97. package/src/pipeline/graph-retrieval.ts +15 -1
  98. package/src/pipeline/hybrid.ts +151 -43
  99. package/src/pipeline/search.ts +36 -3
  100. package/src/pipeline/trace-metadata.ts +47 -0
  101. package/src/pipeline/types.ts +43 -0
  102. package/src/pipeline/vsearch.ts +101 -38
  103. package/src/sdk/client.ts +380 -71
  104. package/src/sdk/documents.ts +48 -1
  105. package/src/sdk/index.ts +17 -0
  106. package/src/sdk/types.ts +28 -0
  107. package/src/serve/context-capsule.ts +67 -8
  108. package/src/serve/public/app.tsx +12 -1
  109. package/src/serve/public/globals.built.css +1 -1
  110. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  111. package/src/serve/public/pages/Dashboard.tsx +10 -0
  112. package/src/serve/public/pages/TraceHistory.tsx +478 -0
  113. package/src/serve/public/pages/trace-history-detail.tsx +224 -0
  114. package/src/serve/retrieval-trace.ts +28 -0
  115. package/src/serve/routes/api.ts +366 -72
  116. package/src/serve/routes/traces.ts +156 -0
  117. package/src/serve/server.ts +87 -2
  118. package/src/store/index.ts +31 -0
  119. package/src/store/migrations/014-retrieval-traces.ts +303 -0
  120. package/src/store/migrations/index.ts +2 -0
  121. package/src/store/retrieval-trace-codec.ts +384 -0
  122. package/src/store/sqlite/adapter.ts +153 -1
  123. package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
  124. package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
  125. package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
  126. package/src/store/sqlite/retrieval-trace-store.ts +515 -0
  127. package/src/store/types.ts +297 -0
  128. package/src/store/vector/sqlite-vec.ts +76 -1
  129. package/src/store/vector/types.ts +1 -1
@@ -6,11 +6,21 @@
6
6
 
7
7
  import { join as pathJoin } from "node:path";
8
8
 
9
+ import type { EmbeddingPort } from "../../llm/types";
9
10
  import type { SearchResult, SearchResults } from "../../pipeline/types";
10
11
  import type { ToolContext } from "../server";
11
12
 
12
13
  import { decorateUriForIndex, parseUri } from "../../app/constants";
13
14
  import { createNonTtyProgressRenderer } from "../../cli/progress";
15
+ import {
16
+ finishRetrievalTraceAfterError,
17
+ retrievalTraceFilters,
18
+ startRetrievalTraceRequest,
19
+ } from "../../core/retrieval-trace-request";
20
+ import {
21
+ attachRetrievalTraceMetadata,
22
+ type RetrievalTraceSession,
23
+ } from "../../core/retrieval-trace-session";
14
24
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
15
25
  import { resolveDownloadPolicy } from "../../llm/policy";
16
26
  import { resolveModelUri } from "../../llm/registry";
@@ -125,29 +135,55 @@ export function handleVsearch(
125
135
  undefined,
126
136
  args.collection
127
137
  );
138
+ const options = {
139
+ limit: args.limit ?? 5,
140
+ minScore: args.minScore,
141
+ collection: args.collection,
142
+ intent: args.intent,
143
+ exclude: args.exclude,
144
+ since: args.since,
145
+ until: args.until,
146
+ categories: args.categories,
147
+ author: args.author,
148
+ tagsAll: normalizeTagFilters(args.tagsAll),
149
+ tagsAny: normalizeTagFilters(args.tagsAny),
150
+ };
151
+ let traceSession: RetrievalTraceSession | undefined;
152
+ const traceStart = await startRetrievalTraceRequest({
153
+ store: ctx.store,
154
+ config: ctx.config,
155
+ query: args.query,
156
+ filters: retrievalTraceFilters(options),
157
+ pipeline: "vector",
158
+ indexName: ctx.indexName,
159
+ modelUris: [modelUri],
160
+ });
161
+ if (!traceStart.ok) throw new Error(traceStart.error.message);
162
+ traceSession = traceStart.value ?? undefined;
128
163
 
129
- // Resolve download policy from env (MCP has no CLI flags)
130
- const policy = resolveDownloadPolicy(process.env, {});
164
+ let embedPort: EmbeddingPort | null = null;
131
165
 
132
- // Non-TTY progress for MCP (periodic lines to stderr, not \r)
133
- const downloadProgress = createNonTtyProgressRenderer();
166
+ try {
167
+ // Resolve download policy from env (MCP has no CLI flags)
168
+ const policy = resolveDownloadPolicy(process.env, {});
134
169
 
135
- // Create LLM adapter for embeddings
136
- const llm = new LlmAdapter(ctx.config);
137
- const embedResult = await llm.createEmbeddingPort(modelUri, {
138
- policy,
139
- onProgress: (progress) => downloadProgress("embed", progress),
140
- });
141
- if (!embedResult.ok) {
142
- throw new Error(
143
- `Failed to load embedding model: ${embedResult.error.message}. ` +
144
- "Ensure models are downloaded with: gno models pull"
145
- );
146
- }
170
+ // Non-TTY progress for MCP (periodic lines to stderr, not \r)
171
+ const downloadProgress = createNonTtyProgressRenderer();
147
172
 
148
- const embedPort = embedResult.value;
173
+ // Create LLM adapter for embeddings
174
+ const llm = new LlmAdapter(ctx.config);
175
+ const embedResult = await llm.createEmbeddingPort(modelUri, {
176
+ policy,
177
+ onProgress: (progress) => downloadProgress("embed", progress),
178
+ });
179
+ if (!embedResult.ok) {
180
+ throw new Error(
181
+ `Failed to load embedding model: ${embedResult.error.message}. ` +
182
+ "Ensure models are downloaded with: gno models pull"
183
+ );
184
+ }
185
+ embedPort = embedResult.value;
149
186
 
150
- try {
151
187
  // Embed query with contextual formatting
152
188
  const queryEmbedResult = await embedPort.embed(
153
189
  formatQueryForEmbedding(args.query, embedPort.modelUri)
@@ -196,17 +232,8 @@ export function handleVsearch(
196
232
  args.query,
197
233
  queryEmbedding,
198
234
  {
199
- limit: args.limit ?? 5,
200
- minScore: args.minScore,
201
- collection: args.collection,
202
- intent: args.intent,
203
- exclude: args.exclude,
204
- since: args.since,
205
- until: args.until,
206
- categories: args.categories,
207
- author: args.author,
208
- tagsAll: normalizeTagFilters(args.tagsAll),
209
- tagsAny: normalizeTagFilters(args.tagsAny),
235
+ ...options,
236
+ traceSession,
210
237
  }
211
238
  );
212
239
 
@@ -217,17 +244,23 @@ export function handleVsearch(
217
244
  // Enrich with absPath
218
245
  const enrichedResults = enrichWithAbsPath(result.value.results, ctx);
219
246
 
220
- return {
221
- ...result.value,
222
- results: enrichedResults,
223
- meta: {
224
- ...result.value.meta,
225
- // Add queryLanguage hint if provided (per spec, lang is a hint for vsearch)
226
- ...(args.lang ? { queryLanguage: args.lang } : {}),
247
+ return attachRetrievalTraceMetadata(
248
+ {
249
+ ...result.value,
250
+ results: enrichedResults,
251
+ meta: {
252
+ ...result.value.meta,
253
+ // Add queryLanguage hint if provided (per spec, lang is a hint for vsearch)
254
+ ...(args.lang ? { queryLanguage: args.lang } : {}),
255
+ },
227
256
  },
228
- };
257
+ traceSession
258
+ );
259
+ } catch (cause) {
260
+ await finishRetrievalTraceAfterError(traceSession, cause);
261
+ throw cause;
229
262
  } finally {
230
- await embedPort.dispose();
263
+ await embedPort?.dispose();
231
264
  }
232
265
  },
233
266
  formatSearchResults
@@ -5,6 +5,7 @@
5
5
  * @module src/pipeline/answer
6
6
  */
7
7
 
8
+ import type { RetrievalTraceSession } from "../core/retrieval-trace-session";
8
9
  import type { GenerationPort } from "../llm/types";
9
10
  import type { StorePort } from "../store/types";
10
11
  import type {
@@ -15,6 +16,11 @@ import type {
15
16
  } from "./types";
16
17
 
17
18
  import { buildAnswerPrompt, type AnswerPromptSource } from "./answer-prompt";
19
+ import { attachCitationTraceMetadata } from "./trace-metadata";
20
+ import {
21
+ CITATION_TRACE_METADATA,
22
+ SEARCH_RESULT_PLANNER_METADATA,
23
+ } from "./types";
18
24
 
19
25
  // ─────────────────────────────────────────────────────────────────────────────
20
26
  // Constants
@@ -24,6 +30,11 @@ import { buildAnswerPrompt, type AnswerPromptSource } from "./answer-prompt";
24
30
  export const ABSTENTION_MESSAGE =
25
31
  "I don't have enough information in the provided sources to answer this question.";
26
32
 
33
+ export const answerTraceTerminalStatus = (
34
+ citations: readonly Citation[] | undefined
35
+ ): "completed" | "partial" =>
36
+ citations && citations.length > 0 ? "completed" : "partial";
37
+
27
38
  /** Max characters per document (~8K tokens) */
28
39
  const MAX_DOC_CHARS = 32_000;
29
40
 
@@ -86,6 +97,65 @@ interface SelectedSource {
86
97
  reason: string;
87
98
  }
88
99
 
100
+ interface ExactAnswerPassage {
101
+ text: string;
102
+ startLine: number;
103
+ endLine: number;
104
+ passageHash: string;
105
+ }
106
+
107
+ const sha256 = (value: string): string =>
108
+ new Bun.CryptoHasher("sha256").update(value).digest("hex");
109
+
110
+ const completeLinePrefix = (
111
+ content: string,
112
+ startLine: number,
113
+ maxChars: number
114
+ ): ExactAnswerPassage | null => {
115
+ const lines = content.split("\n");
116
+ if (lines.length === 0) return null;
117
+ const selected: string[] = [];
118
+ let characters = 0;
119
+ for (const line of lines) {
120
+ const addition = selected.length === 0 ? line.length : line.length + 1;
121
+ if (characters + addition > maxChars) break;
122
+ selected.push(line);
123
+ characters += addition;
124
+ }
125
+ if (selected.length === 0) return null;
126
+ const text = selected.join("\n");
127
+ if (!text.trim()) return null;
128
+ return {
129
+ text,
130
+ startLine,
131
+ endLine: startLine + selected.length - 1,
132
+ passageHash: sha256(text),
133
+ };
134
+ };
135
+
136
+ const anchoredCompleteLines = (
137
+ content: string,
138
+ metadata: SearchResult[typeof SEARCH_RESULT_PLANNER_METADATA]
139
+ ): ExactAnswerPassage | null => {
140
+ if (
141
+ !metadata?.passageHash ||
142
+ metadata.startLine === undefined ||
143
+ metadata.endLine === undefined ||
144
+ metadata.endLine < metadata.startLine
145
+ ) {
146
+ return null;
147
+ }
148
+ const lines = content.split("\n");
149
+ const text = lines.slice(metadata.startLine - 1, metadata.endLine).join("\n");
150
+ if (
151
+ text.split("\n").length !== metadata.endLine - metadata.startLine + 1 ||
152
+ sha256(text) !== metadata.passageHash
153
+ ) {
154
+ return null;
155
+ }
156
+ return completeLinePrefix(text, metadata.startLine, MAX_DOC_CHARS);
157
+ };
158
+
89
159
  // ─────────────────────────────────────────────────────────────────────────────
90
160
  // Citation Processing
91
161
  // ─────────────────────────────────────────────────────────────────────────────
@@ -423,53 +493,78 @@ export async function generateGroundedAnswer(
423
493
  ): Promise<AnswerGenerationResult | null> {
424
494
  const { genPort, store } = deps;
425
495
  const sourceSelection = selectAdaptiveSources(query, results);
496
+ const finalRanks = new Map(
497
+ results.map((result, index) => [result, index + 1] as const)
498
+ );
426
499
  const promptSources: AnswerPromptSource[] = [];
427
500
  const citations: Citation[] = [];
428
501
  let citationIndex = 0;
429
502
 
430
503
  for (const r of sourceSelection.selected) {
431
- let content: string | null = null;
432
- let usedFullContent = false;
504
+ let passage: ExactAnswerPassage | null = null;
505
+ const plannerMetadata = r[SEARCH_RESULT_PLANNER_METADATA];
506
+ const sourceHash = r.source.sourceHash;
507
+ const mirrorHash = r.conversion?.mirrorHash;
433
508
 
434
509
  // Try to fetch full document content if store available
435
- if (store && r.conversion?.mirrorHash) {
436
- const contentResult = await store.getContent(r.conversion.mirrorHash);
510
+ if (store && mirrorHash) {
511
+ const contentResult = await store.getContent(mirrorHash);
437
512
  if (contentResult.ok && contentResult.value) {
438
- content = contentResult.value;
439
- usedFullContent = true;
440
- // Truncate to max doc chars
441
- if (content.length > MAX_DOC_CHARS) {
442
- content = `${content.slice(0, MAX_DOC_CHARS)}\n\n[... truncated ...]`;
513
+ if (sha256(contentResult.value) === mirrorHash) {
514
+ passage =
515
+ anchoredCompleteLines(contentResult.value, plannerMetadata) ??
516
+ completeLinePrefix(contentResult.value, 1, MAX_DOC_CHARS);
443
517
  }
444
518
  }
445
519
  }
446
520
 
447
- // Fallback to snippet if full content unavailable
448
- if (!content) {
449
- if (!r.snippet || r.snippet.trim().length === 0) {
450
- continue;
451
- }
452
- content =
453
- r.snippet.length > MAX_SNIPPET_CHARS
454
- ? `${r.snippet.slice(0, MAX_SNIPPET_CHARS)}...`
455
- : r.snippet;
521
+ // Snippet fallback is allowed only when the planner proves it is the exact
522
+ // complete canonical chunk, never an FTS marker/ellipsis presentation.
523
+ if (
524
+ !passage &&
525
+ plannerMetadata?.passageHash &&
526
+ plannerMetadata.startLine !== undefined &&
527
+ plannerMetadata.endLine !== undefined &&
528
+ sha256(r.snippet) === plannerMetadata.passageHash &&
529
+ r.snippet.split("\n").length ===
530
+ plannerMetadata.endLine - plannerMetadata.startLine + 1
531
+ ) {
532
+ passage = completeLinePrefix(
533
+ r.snippet,
534
+ plannerMetadata.startLine,
535
+ MAX_SNIPPET_CHARS
536
+ );
456
537
  }
538
+ if (!(passage && sourceHash && mirrorHash)) continue;
457
539
 
458
540
  citationIndex += 1;
459
541
  promptSources.push({
460
542
  index: citationIndex,
461
543
  docid: r.docid,
462
544
  uri: r.uri,
463
- content,
545
+ content: passage.text,
464
546
  guidance: r.context,
465
547
  });
466
- // Clear line range when citing full content (not a specific snippet)
467
- citations.push({
468
- docid: r.docid,
469
- uri: r.uri,
470
- startLine: usedFullContent ? undefined : r.snippetRange?.startLine,
471
- endLine: usedFullContent ? undefined : r.snippetRange?.endLine,
472
- });
548
+ citations.push(
549
+ attachCitationTraceMetadata(
550
+ {
551
+ docid: r.docid,
552
+ uri: r.uri,
553
+ startLine: passage.startLine,
554
+ endLine: passage.endLine,
555
+ },
556
+ {
557
+ sourceHash,
558
+ mirrorHash,
559
+ passageHash: passage.passageHash,
560
+ seq: plannerMetadata?.seq,
561
+ rank: finalRanks.get(r) ?? 1,
562
+ plannerRank: plannerMetadata?.retrievalRank,
563
+ sources: plannerMetadata?.sources,
564
+ graphExpanded: plannerMetadata?.graphExpanded,
565
+ }
566
+ )
567
+ );
473
568
  }
474
569
 
475
570
  if (promptSources.length === 0) {
@@ -528,3 +623,49 @@ export function processAnswerResult(rawResult: AnswerGenerationResult): {
528
623
  answerContext: rawResult.answerContext,
529
624
  };
530
625
  }
626
+
627
+ /** Process first, then record only citations retained in the final answer. */
628
+ export async function processAnswerResultWithTrace(
629
+ rawResult: AnswerGenerationResult,
630
+ traceSession?: RetrievalTraceSession
631
+ ): Promise<ReturnType<typeof processAnswerResult>> {
632
+ const processed = processAnswerResult(rawResult);
633
+ if (!traceSession || processed.citations.length === 0) return processed;
634
+ const evidence = processed.citations.flatMap((citation) => {
635
+ const metadata = citation[CITATION_TRACE_METADATA];
636
+ if (
637
+ !metadata ||
638
+ citation.startLine === undefined ||
639
+ citation.endLine === undefined
640
+ ) {
641
+ return [];
642
+ }
643
+ return [
644
+ {
645
+ docid: citation.docid,
646
+ uri: citation.uri,
647
+ sourceHash: metadata.sourceHash,
648
+ mirrorHash: metadata.mirrorHash,
649
+ passageHash: metadata.passageHash,
650
+ startLine: citation.startLine,
651
+ endLine: citation.endLine,
652
+ rank: metadata.rank,
653
+ ...(metadata.seq === undefined ? {} : { seq: metadata.seq }),
654
+ ...(metadata.plannerRank === undefined
655
+ ? {}
656
+ : { plannerRank: metadata.plannerRank }),
657
+ ...(metadata.sources === undefined
658
+ ? {}
659
+ : { sources: metadata.sources }),
660
+ ...(metadata.graphExpanded === undefined
661
+ ? {}
662
+ : { graphExpanded: metadata.graphExpanded }),
663
+ },
664
+ ];
665
+ });
666
+ const recorded = await traceSession.recordEvidence("cite", evidence);
667
+ if (!recorded.ok) {
668
+ throw new Error(`Trace recording failed: ${recorded.error.message}`);
669
+ }
670
+ return processed;
671
+ }
@@ -62,6 +62,7 @@ const matchesDocumentFilters = (
62
62
  until?: string;
63
63
  categories?: string[];
64
64
  author?: string;
65
+ relPathPrefix?: string;
65
66
  }
66
67
  ): boolean => {
67
68
  if (
@@ -201,6 +202,7 @@ export async function expandGraphCandidates(
201
202
  until?: string;
202
203
  categories?: string[];
203
204
  author?: string;
205
+ relPathPrefix?: string;
204
206
  } = {}
205
207
  ): Promise<GraphRetrievalResult> {
206
208
  const maxCandidates = Math.max(
@@ -249,6 +251,13 @@ export async function expandGraphCandidates(
249
251
  if (!doc.mirrorHash) {
250
252
  continue;
251
253
  }
254
+ if (
255
+ options.relPathPrefix !== undefined &&
256
+ doc.relPath !== options.relPathPrefix &&
257
+ !doc.relPath.startsWith(`${options.relPathPrefix}/`)
258
+ ) {
259
+ continue;
260
+ }
252
261
  const rank =
253
262
  seedCandidates.findIndex(
254
263
  (candidate) => candidate.mirrorHash === doc.mirrorHash
@@ -325,7 +334,12 @@ export async function expandGraphCandidates(
325
334
  }
326
335
 
327
336
  const metadataFilteredDocs = docsResult.value.filter(
328
- (doc) => doc.mirrorHash && matchesDocumentFilters(doc, options)
337
+ (doc) =>
338
+ doc.mirrorHash &&
339
+ (options.relPathPrefix === undefined ||
340
+ doc.relPath === options.relPathPrefix ||
341
+ doc.relPath.startsWith(`${options.relPathPrefix}/`)) &&
342
+ matchesDocumentFilters(doc, options)
329
343
  );
330
344
  const docs = await filterDocsByTags(store, metadataFilteredDocs, options);
331
345
  const docByDocid = new Map(docs.map((doc) => [doc.docid, doc]));