@joycodetech/qmd-ja 2.5.3-ja.3

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 (48) hide show
  1. package/CHANGELOG.md +821 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1143 -0
  4. package/bin/qmd-ja +162 -0
  5. package/dist/ast.d.ts +65 -0
  6. package/dist/ast.js +334 -0
  7. package/dist/bench/bench.d.ts +23 -0
  8. package/dist/bench/bench.js +280 -0
  9. package/dist/bench/score.d.ts +33 -0
  10. package/dist/bench/score.js +88 -0
  11. package/dist/bench/types.d.ts +80 -0
  12. package/dist/bench/types.js +8 -0
  13. package/dist/cli/formatter.d.ts +120 -0
  14. package/dist/cli/formatter.js +355 -0
  15. package/dist/cli/qmd.d.ts +43 -0
  16. package/dist/cli/qmd.js +4179 -0
  17. package/dist/collections.d.ts +166 -0
  18. package/dist/collections.js +410 -0
  19. package/dist/db.d.ts +44 -0
  20. package/dist/db.js +75 -0
  21. package/dist/index.d.ts +230 -0
  22. package/dist/index.js +242 -0
  23. package/dist/llm.d.ts +500 -0
  24. package/dist/llm.js +1615 -0
  25. package/dist/maintenance.d.ts +23 -0
  26. package/dist/maintenance.js +37 -0
  27. package/dist/mcp/server.d.ts +24 -0
  28. package/dist/mcp/server.js +702 -0
  29. package/dist/paths.d.ts +1 -0
  30. package/dist/paths.js +4 -0
  31. package/dist/store.d.ts +1002 -0
  32. package/dist/store.js +4208 -0
  33. package/models/vaporetto-bccwj.model +0 -0
  34. package/package.json +130 -0
  35. package/scripts/build.mjs +30 -0
  36. package/scripts/check-package-grammars.mjs +29 -0
  37. package/scripts/package-smoke.mjs +65 -0
  38. package/scripts/test-all.mjs +38 -0
  39. package/skills/qmd/SKILL.md +295 -0
  40. package/skills/qmd/references/mcp-setup.md +102 -0
  41. package/skills/release/SKILL.md +139 -0
  42. package/skills/release/scripts/install-hooks.sh +38 -0
  43. package/vendor/vaporetto-node-wasm/LICENSE +22 -0
  44. package/vendor/vaporetto-node-wasm/package.json +11 -0
  45. package/vendor/vaporetto-node-wasm/vaporetto_node_wasm.d.ts +19 -0
  46. package/vendor/vaporetto-node-wasm/vaporetto_node_wasm.js +202 -0
  47. package/vendor/vaporetto-node-wasm/vaporetto_node_wasm_bg.wasm +0 -0
  48. package/vendor/vaporetto-node-wasm/vaporetto_node_wasm_bg.wasm.d.ts +13 -0
@@ -0,0 +1,280 @@
1
+ /**
2
+ * QMD Benchmark Harness
3
+ *
4
+ * Runs queries from a fixture file against multiple search backends
5
+ * and measures precision@k, recall, MRR, F1, and latency.
6
+ *
7
+ * Usage:
8
+ * qmd bench <fixture.json> [--json] [--collection <name>]
9
+ *
10
+ * Backends tested:
11
+ * - bm25: BM25 keyword search (searchLex)
12
+ * - vector: Vector similarity search (searchVector)
13
+ * - hybrid: BM25 + vector RRF fusion without reranking
14
+ * - full: Full hybrid pipeline with LLM reranking
15
+ */
16
+ import { readFileSync } from "node:fs";
17
+ import { resolve } from "node:path";
18
+ import { createStore, getDefaultDbPath, } from "../index.js";
19
+ import { scoreResults } from "./score.js";
20
+ function parseStructuredQuery(query) {
21
+ const lines = query.split("\n").map((line, idx) => ({
22
+ trimmed: line.trim(),
23
+ number: idx + 1,
24
+ })).filter(line => line.trimmed.length > 0);
25
+ if (lines.length === 0)
26
+ return undefined;
27
+ const prefixRe = /^(lex|vec|hyde):\s*/i;
28
+ const intentRe = /^intent:\s*/i;
29
+ const searches = [];
30
+ let intent;
31
+ for (const line of lines) {
32
+ if (intentRe.test(line.trimmed)) {
33
+ if (intent !== undefined) {
34
+ throw new Error(`Line ${line.number}: only one intent: line is allowed per benchmark query.`);
35
+ }
36
+ intent = line.trimmed.replace(intentRe, "").trim();
37
+ if (!intent) {
38
+ throw new Error(`Line ${line.number}: intent: must include text.`);
39
+ }
40
+ continue;
41
+ }
42
+ const match = line.trimmed.match(prefixRe);
43
+ if (match) {
44
+ const type = match[1].toLowerCase();
45
+ const text = line.trimmed.slice(match[0].length).trim();
46
+ if (!text) {
47
+ throw new Error(`Line ${line.number} (${type}:) must include text.`);
48
+ }
49
+ searches.push({ type, query: text, line: line.number });
50
+ continue;
51
+ }
52
+ if (lines.length === 1) {
53
+ return undefined;
54
+ }
55
+ throw new Error(`Line ${line.number} is missing a lex:/vec:/hyde:/intent: prefix.`);
56
+ }
57
+ if (intent && searches.length === 0) {
58
+ throw new Error("intent: cannot appear alone. Add at least one lex:, vec:, or hyde: line.");
59
+ }
60
+ return searches.length > 0 ? { searches, intent } : undefined;
61
+ }
62
+ function uniqueFiles(files, limit) {
63
+ const seen = new Set();
64
+ const out = [];
65
+ for (const file of files) {
66
+ if (seen.has(file))
67
+ continue;
68
+ seen.add(file);
69
+ out.push(file);
70
+ if (out.length >= limit)
71
+ break;
72
+ }
73
+ return out;
74
+ }
75
+ const BACKENDS = [
76
+ {
77
+ name: "bm25",
78
+ run: async (store, query, limit, collection) => {
79
+ const structured = parseStructuredQuery(query.query);
80
+ const lexQueries = structured?.searches.filter(q => q.type === "lex");
81
+ if (structured) {
82
+ const files = [];
83
+ for (const lex of lexQueries ?? []) {
84
+ const results = await store.searchLex(lex.query, { limit, collection });
85
+ files.push(...results.map((r) => r.filepath));
86
+ }
87
+ return uniqueFiles(files, limit);
88
+ }
89
+ const results = await store.searchLex(query.query, { limit, collection });
90
+ return results.map((r) => r.filepath);
91
+ },
92
+ },
93
+ {
94
+ name: "vector",
95
+ run: async (store, query, limit, collection) => {
96
+ const structured = parseStructuredQuery(query.query);
97
+ const vectorQueries = structured?.searches.filter(q => q.type === "vec" || q.type === "hyde");
98
+ if (structured) {
99
+ const files = [];
100
+ for (const vectorQuery of vectorQueries ?? []) {
101
+ const results = await store.searchVector(vectorQuery.query, { limit, collection });
102
+ files.push(...results.map((r) => r.filepath));
103
+ }
104
+ return uniqueFiles(files, limit);
105
+ }
106
+ const results = await store.searchVector(query.query, { limit, collection });
107
+ return results.map((r) => r.filepath);
108
+ },
109
+ },
110
+ {
111
+ name: "hybrid",
112
+ run: async (store, query, limit, collection) => {
113
+ const structured = parseStructuredQuery(query.query);
114
+ const results = structured
115
+ ? await store.search({ queries: structured.searches, intent: structured.intent, limit, collection, rerank: false })
116
+ : await store.search({ query: query.query, limit, collection, rerank: false });
117
+ return results.map((r) => r.file);
118
+ },
119
+ },
120
+ {
121
+ name: "full",
122
+ run: async (store, query, limit, collection) => {
123
+ const structured = parseStructuredQuery(query.query);
124
+ const results = structured
125
+ ? await store.search({ queries: structured.searches, intent: structured.intent, limit, collection, rerank: true })
126
+ : await store.search({ query: query.query, limit, collection, rerank: true });
127
+ return results.map((r) => r.file);
128
+ },
129
+ },
130
+ ];
131
+ async function runQuery(store, backend, query, collection) {
132
+ const limit = Math.max(query.expected_in_top_k, 10);
133
+ const start = Date.now();
134
+ let resultFiles;
135
+ try {
136
+ resultFiles = await backend.run(store, query, limit, collection);
137
+ }
138
+ catch {
139
+ // Backend may not be available (e.g., no embeddings for vector search)
140
+ return {
141
+ precision_at_k: 0,
142
+ recall: 0,
143
+ recall_at_1: 0,
144
+ recall_at_3: 0,
145
+ recall_at_5: 0,
146
+ mrr: 0,
147
+ f1: 0,
148
+ hits_at_k: 0,
149
+ total_expected: query.expected_files.length,
150
+ latency_ms: Date.now() - start,
151
+ top_files: [],
152
+ matched_files: [],
153
+ unmatched_expected_files: query.expected_files,
154
+ };
155
+ }
156
+ const latency_ms = Date.now() - start;
157
+ const scores = scoreResults(resultFiles, query.expected_files, query.expected_in_top_k);
158
+ return {
159
+ ...scores,
160
+ total_expected: query.expected_files.length,
161
+ latency_ms,
162
+ top_files: resultFiles.slice(0, 10),
163
+ };
164
+ }
165
+ function formatTable(results) {
166
+ const lines = [];
167
+ const pad = (s, n) => s.slice(0, n).padEnd(n);
168
+ const num = (n) => n.toFixed(2).padStart(5);
169
+ lines.push(`${pad("Query", 25)} ${pad("Backend", 8)} ${pad("P@k", 6)} ${pad("R@1", 6)} ${pad("R@3", 6)} ${pad("R@5", 6)} ${pad("MRR", 6)} ${pad("F1", 6)} ${pad("ms", 8)}`);
170
+ lines.push("-".repeat(88));
171
+ for (const r of results) {
172
+ for (const [backend, br] of Object.entries(r.backends)) {
173
+ lines.push(`${pad(r.id, 25)} ${pad(backend, 8)} ${num(br.precision_at_k)} ${num(br.recall_at_1)} ${num(br.recall_at_3)} ${num(br.recall_at_5)} ${num(br.mrr)} ${num(br.f1)} ${String(Math.round(br.latency_ms)).padStart(7)}ms`);
174
+ }
175
+ lines.push("");
176
+ }
177
+ return lines.join("\n");
178
+ }
179
+ function computeSummary(results) {
180
+ const summary = {};
181
+ // Collect all backend names
182
+ const backendNames = new Set();
183
+ for (const r of results) {
184
+ for (const name of Object.keys(r.backends)) {
185
+ backendNames.add(name);
186
+ }
187
+ }
188
+ for (const name of Array.from(backendNames)) {
189
+ let totalP = 0, totalR = 0, totalR1 = 0, totalR3 = 0, totalR5 = 0, totalMrr = 0, totalF1 = 0, totalLat = 0, count = 0;
190
+ for (const r of results) {
191
+ const br = r.backends[name];
192
+ if (!br)
193
+ continue;
194
+ totalP += br.precision_at_k;
195
+ totalR += br.recall;
196
+ totalR1 += br.recall_at_1;
197
+ totalR3 += br.recall_at_3;
198
+ totalR5 += br.recall_at_5;
199
+ totalMrr += br.mrr;
200
+ totalF1 += br.f1;
201
+ totalLat += br.latency_ms;
202
+ count++;
203
+ }
204
+ if (count > 0) {
205
+ summary[name] = {
206
+ avg_precision: totalP / count,
207
+ avg_recall: totalR / count,
208
+ avg_recall_at_1: totalR1 / count,
209
+ avg_recall_at_3: totalR3 / count,
210
+ avg_recall_at_5: totalR5 / count,
211
+ avg_mrr: totalMrr / count,
212
+ avg_f1: totalF1 / count,
213
+ avg_latency_ms: totalLat / count,
214
+ };
215
+ }
216
+ }
217
+ return summary;
218
+ }
219
+ export async function runBenchmark(fixturePath, options = {}) {
220
+ // Load fixture
221
+ const raw = readFileSync(resolve(fixturePath), "utf-8");
222
+ const fixture = JSON.parse(raw);
223
+ if (!fixture.queries || !Array.isArray(fixture.queries)) {
224
+ throw new Error("Invalid fixture: missing 'queries' array");
225
+ }
226
+ // Open store
227
+ const store = await createStore({
228
+ dbPath: options.dbPath ?? getDefaultDbPath(),
229
+ ...(options.configPath ? { configPath: options.configPath } : {}),
230
+ });
231
+ // Filter backends if requested
232
+ const activeBackends = options.backends
233
+ ? BACKENDS.filter(b => options.backends.includes(b.name))
234
+ : BACKENDS;
235
+ const collection = options.collection ?? fixture.collection;
236
+ // Run queries
237
+ const results = [];
238
+ for (const query of fixture.queries) {
239
+ const backends = {};
240
+ for (const backend of activeBackends) {
241
+ if (!options.json) {
242
+ process.stderr.write(` ${query.id} / ${backend.name}...`);
243
+ }
244
+ backends[backend.name] = await runQuery(store, backend, query, collection);
245
+ if (!options.json) {
246
+ process.stderr.write(` ${Math.round(backends[backend.name].latency_ms)}ms\n`);
247
+ }
248
+ }
249
+ results.push({
250
+ id: query.id,
251
+ query: query.query,
252
+ type: query.type,
253
+ backends,
254
+ });
255
+ }
256
+ await store.close();
257
+ const summary = computeSummary(results);
258
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "").slice(0, 15);
259
+ const benchResult = {
260
+ timestamp,
261
+ fixture: fixturePath,
262
+ results,
263
+ summary,
264
+ };
265
+ // Output
266
+ if (options.json) {
267
+ console.log(JSON.stringify(benchResult, null, 2));
268
+ }
269
+ else {
270
+ console.log("\n" + formatTable(results));
271
+ console.log("Summary:");
272
+ console.log("-".repeat(70));
273
+ const pad = (s, n) => s.slice(0, n).padEnd(n);
274
+ const num = (n) => n.toFixed(3).padStart(6);
275
+ for (const [name, s] of Object.entries(summary)) {
276
+ console.log(` ${pad(name, 8)} P@k=${num(s.avg_precision)} R@1=${num(s.avg_recall_at_1)} R@3=${num(s.avg_recall_at_3)} R@5=${num(s.avg_recall_at_5)} MRR=${num(s.avg_mrr)} F1=${num(s.avg_f1)} Avg=${Math.round(s.avg_latency_ms)}ms`);
277
+ }
278
+ }
279
+ return benchResult;
280
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Scoring functions for the QMD benchmark harness.
3
+ *
4
+ * Computes precision@k, recall, MRR, and F1 for search results
5
+ * against ground-truth expected files.
6
+ */
7
+ /**
8
+ * Normalize a file path for comparison.
9
+ * Strips qmd:// prefix, lowercases, removes leading/trailing slashes.
10
+ */
11
+ export declare function normalizePath(p: string): string;
12
+ /**
13
+ * Check if two paths refer to the same file.
14
+ * Handles different path formats by comparing normalized suffixes.
15
+ */
16
+ export declare function pathsMatch(result: string, expected: string): boolean;
17
+ type ScoreMetrics = {
18
+ precision_at_k: number;
19
+ recall: number;
20
+ recall_at_1: number;
21
+ recall_at_3: number;
22
+ recall_at_5: number;
23
+ mrr: number;
24
+ f1: number;
25
+ hits_at_k: number;
26
+ matched_files: string[];
27
+ unmatched_expected_files: string[];
28
+ };
29
+ /**
30
+ * Score a set of search results against expected files.
31
+ */
32
+ export declare function scoreResults(resultFiles: string[], expectedFiles: string[], topK: number): ScoreMetrics;
33
+ export {};
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Scoring functions for the QMD benchmark harness.
3
+ *
4
+ * Computes precision@k, recall, MRR, and F1 for search results
5
+ * against ground-truth expected files.
6
+ */
7
+ /**
8
+ * Normalize a file path for comparison.
9
+ * Strips qmd:// prefix, lowercases, removes leading/trailing slashes.
10
+ */
11
+ export function normalizePath(p) {
12
+ if (p.startsWith("qmd://")) {
13
+ // qmd://collection/docs/readme.md → docs/readme.md
14
+ const withoutScheme = p.slice("qmd://".length);
15
+ const slashIdx = withoutScheme.indexOf("/");
16
+ p = slashIdx >= 0 ? withoutScheme.slice(slashIdx + 1) : withoutScheme;
17
+ }
18
+ return p.toLowerCase().replace(/^\/+|\/+$/g, "");
19
+ }
20
+ /**
21
+ * Check if two paths refer to the same file.
22
+ * Handles different path formats by comparing normalized suffixes.
23
+ */
24
+ export function pathsMatch(result, expected) {
25
+ const nr = normalizePath(result);
26
+ const ne = normalizePath(expected);
27
+ if (nr === ne)
28
+ return true;
29
+ if (nr.endsWith(ne) || ne.endsWith(nr))
30
+ return true;
31
+ return false;
32
+ }
33
+ function hitsWithin(resultFiles, expectedFiles, k) {
34
+ const topKResults = resultFiles.slice(0, k);
35
+ let hits = 0;
36
+ for (const expected of expectedFiles) {
37
+ if (topKResults.some(r => pathsMatch(r, expected))) {
38
+ hits++;
39
+ }
40
+ }
41
+ return hits;
42
+ }
43
+ /**
44
+ * Score a set of search results against expected files.
45
+ */
46
+ export function scoreResults(resultFiles, expectedFiles, topK) {
47
+ // Count hits in top-k
48
+ const hitsAtK = hitsWithin(resultFiles, expectedFiles, topK);
49
+ const matchedFiles = [];
50
+ const unmatchedExpectedFiles = [];
51
+ for (const expected of expectedFiles) {
52
+ if (resultFiles.some(r => pathsMatch(r, expected))) {
53
+ matchedFiles.push(expected);
54
+ }
55
+ else {
56
+ unmatchedExpectedFiles.push(expected);
57
+ }
58
+ }
59
+ // MRR: reciprocal rank of first relevant result
60
+ let mrr = 0;
61
+ for (let i = 0; i < resultFiles.length; i++) {
62
+ if (expectedFiles.some(e => pathsMatch(resultFiles[i], e))) {
63
+ mrr = 1 / (i + 1);
64
+ break;
65
+ }
66
+ }
67
+ const denominator = Math.min(topK, expectedFiles.length);
68
+ const precision_at_k = denominator > 0 ? hitsAtK / denominator : 0;
69
+ const recall = expectedFiles.length > 0 ? matchedFiles.length / expectedFiles.length : 0;
70
+ const recall_at_1 = expectedFiles.length > 0 ? hitsWithin(resultFiles, expectedFiles, 1) / expectedFiles.length : 0;
71
+ const recall_at_3 = expectedFiles.length > 0 ? hitsWithin(resultFiles, expectedFiles, 3) / expectedFiles.length : 0;
72
+ const recall_at_5 = expectedFiles.length > 0 ? hitsWithin(resultFiles, expectedFiles, 5) / expectedFiles.length : 0;
73
+ const f1 = precision_at_k + recall > 0
74
+ ? 2 * (precision_at_k * recall) / (precision_at_k + recall)
75
+ : 0;
76
+ return {
77
+ precision_at_k,
78
+ recall,
79
+ recall_at_1,
80
+ recall_at_3,
81
+ recall_at_5,
82
+ mrr,
83
+ f1,
84
+ hits_at_k: hitsAtK,
85
+ matched_files: matchedFiles,
86
+ unmatched_expected_files: unmatchedExpectedFiles,
87
+ };
88
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Types for the QMD benchmark harness.
3
+ *
4
+ * A benchmark fixture defines queries with expected results.
5
+ * The harness runs each query through multiple search backends
6
+ * and measures precision, recall, MRR, and latency.
7
+ */
8
+ export interface BenchmarkQuery {
9
+ /** Unique identifier for the query */
10
+ id: string;
11
+ /** The search query text */
12
+ query: string;
13
+ /** Query difficulty/type for grouping results */
14
+ type: "exact" | "semantic" | "topical" | "cross-domain" | "alias";
15
+ /** Human-readable description of what this tests */
16
+ description: string;
17
+ /** File paths (relative to collection) that should appear in results */
18
+ expected_files: string[];
19
+ /** How many of expected_files should appear in top-k results */
20
+ expected_in_top_k: number;
21
+ }
22
+ export interface BenchmarkFixture {
23
+ /** Description of the benchmark */
24
+ description: string;
25
+ /** Fixture format version */
26
+ version: number;
27
+ /** Optional collection to search within */
28
+ collection?: string;
29
+ /** The test queries */
30
+ queries: BenchmarkQuery[];
31
+ }
32
+ export interface BackendResult {
33
+ /** Fraction of top-k results that are relevant */
34
+ precision_at_k: number;
35
+ /** Fraction of expected files found anywhere in results */
36
+ recall: number;
37
+ /** Fraction of expected files found in the first result */
38
+ recall_at_1: number;
39
+ /** Fraction of expected files found in the top 3 results */
40
+ recall_at_3: number;
41
+ /** Fraction of expected files found in the top 5 results */
42
+ recall_at_5: number;
43
+ /** Reciprocal rank of first relevant result (1/rank, 0 if not found) */
44
+ mrr: number;
45
+ /** Harmonic mean of precision_at_k and recall */
46
+ f1: number;
47
+ /** Number of expected files found in top-k */
48
+ hits_at_k: number;
49
+ /** Total expected files */
50
+ total_expected: number;
51
+ /** Wall-clock latency in milliseconds */
52
+ latency_ms: number;
53
+ /** Top result file paths (for inspection) */
54
+ top_files: string[];
55
+ /** Expected files that were found anywhere in the returned result set */
56
+ matched_files: string[];
57
+ /** Expected files missing from the returned result set */
58
+ unmatched_expected_files: string[];
59
+ }
60
+ export interface QueryResult {
61
+ id: string;
62
+ query: string;
63
+ type: string;
64
+ backends: Record<string, BackendResult>;
65
+ }
66
+ export interface BenchmarkResult {
67
+ timestamp: string;
68
+ fixture: string;
69
+ results: QueryResult[];
70
+ summary: Record<string, {
71
+ avg_precision: number;
72
+ avg_recall: number;
73
+ avg_recall_at_1: number;
74
+ avg_recall_at_3: number;
75
+ avg_recall_at_5: number;
76
+ avg_mrr: number;
77
+ avg_f1: number;
78
+ avg_latency_ms: number;
79
+ }>;
80
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Types for the QMD benchmark harness.
3
+ *
4
+ * A benchmark fixture defines queries with expected results.
5
+ * The harness runs each query through multiple search backends
6
+ * and measures precision, recall, MRR, and latency.
7
+ */
8
+ export {};
@@ -0,0 +1,120 @@
1
+ /**
2
+ * formatter.ts - Output formatting utilities for QMD
3
+ *
4
+ * Provides methods to format search results and documents into various output formats:
5
+ * JSON, CSV, XML, Markdown, files list, and CLI (colored terminal output).
6
+ */
7
+ import type { SearchResult, MultiGetResult, DocumentResult } from "../store.js";
8
+ export type { SearchResult, MultiGetResult, DocumentResult };
9
+ export type MultiGetFile = {
10
+ filepath: string;
11
+ displayPath: string;
12
+ title: string;
13
+ body: string;
14
+ context?: string | null;
15
+ skipped: false;
16
+ } | {
17
+ filepath: string;
18
+ displayPath: string;
19
+ title: string;
20
+ body: string;
21
+ context?: string | null;
22
+ skipped: true;
23
+ skipReason: string;
24
+ };
25
+ export type OutputFormat = "cli" | "csv" | "md" | "xml" | "files" | "json";
26
+ export type FormatOptions = {
27
+ full?: boolean;
28
+ query?: string;
29
+ useColor?: boolean;
30
+ lineNumbers?: boolean;
31
+ intent?: string;
32
+ };
33
+ /**
34
+ * Add line numbers to text content.
35
+ * Each line becomes: "{lineNum}: {content}"
36
+ * @param text The text to add line numbers to
37
+ * @param startLine Optional starting line number (default: 1)
38
+ */
39
+ export declare function addLineNumbers(text: string, startLine?: number): string;
40
+ /**
41
+ * Extract short docid from a full hash (first 6 characters).
42
+ */
43
+ export declare function getDocid(hash: string): string;
44
+ export declare function escapeCSV(value: string | null | number): string;
45
+ export declare function escapeXml(str: string): string;
46
+ /**
47
+ * Format search results as JSON
48
+ */
49
+ export declare function searchResultsToJson(results: SearchResult[], opts?: FormatOptions): string;
50
+ /**
51
+ * Format search results as CSV
52
+ */
53
+ export declare function searchResultsToCsv(results: SearchResult[], opts?: FormatOptions): string;
54
+ /**
55
+ * Format search results as simple files list (docid,score,filepath,context)
56
+ */
57
+ export declare function searchResultsToFiles(results: SearchResult[]): string;
58
+ /**
59
+ * Format search results as Markdown
60
+ */
61
+ export declare function searchResultsToMarkdown(results: SearchResult[], opts?: FormatOptions): string;
62
+ /**
63
+ * Format search results as XML
64
+ */
65
+ export declare function searchResultsToXml(results: SearchResult[], opts?: FormatOptions): string;
66
+ /**
67
+ * Format search results for MCP (simpler CSV format with pre-extracted snippets)
68
+ */
69
+ export declare function searchResultsToMcpCsv(results: {
70
+ docid: string;
71
+ file: string;
72
+ title: string;
73
+ score: number;
74
+ context: string | null;
75
+ snippet: string;
76
+ }[]): string;
77
+ /**
78
+ * Format documents as JSON
79
+ */
80
+ export declare function documentsToJson(results: MultiGetFile[]): string;
81
+ /**
82
+ * Format documents as CSV
83
+ */
84
+ export declare function documentsToCsv(results: MultiGetFile[]): string;
85
+ /**
86
+ * Format documents as files list
87
+ */
88
+ export declare function documentsToFiles(results: MultiGetFile[]): string;
89
+ /**
90
+ * Format documents as Markdown
91
+ */
92
+ export declare function documentsToMarkdown(results: MultiGetFile[]): string;
93
+ /**
94
+ * Format documents as XML
95
+ */
96
+ export declare function documentsToXml(results: MultiGetFile[]): string;
97
+ /**
98
+ * Format a single DocumentResult as JSON
99
+ */
100
+ export declare function documentToJson(doc: DocumentResult): string;
101
+ /**
102
+ * Format a single DocumentResult as Markdown
103
+ */
104
+ export declare function documentToMarkdown(doc: DocumentResult): string;
105
+ /**
106
+ * Format a single DocumentResult as XML
107
+ */
108
+ export declare function documentToXml(doc: DocumentResult): string;
109
+ /**
110
+ * Format a single document to the specified format
111
+ */
112
+ export declare function formatDocument(doc: DocumentResult, format: OutputFormat): string;
113
+ /**
114
+ * Format search results to the specified output format
115
+ */
116
+ export declare function formatSearchResults(results: SearchResult[], format: OutputFormat, opts?: FormatOptions): string;
117
+ /**
118
+ * Format documents to the specified output format
119
+ */
120
+ export declare function formatDocuments(results: MultiGetFile[], format: OutputFormat): string;