@alextis59/athena 1.0.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/LICENSE +15 -0
- package/README.md +147 -0
- package/dist/agent/ask.d.ts +7 -0
- package/dist/agent/ask.d.ts.map +1 -0
- package/dist/agent/ask.js +35 -0
- package/dist/agent/ask.js.map +1 -0
- package/dist/agent/citations.d.ts +27 -0
- package/dist/agent/citations.d.ts.map +1 -0
- package/dist/agent/citations.js +190 -0
- package/dist/agent/citations.js.map +1 -0
- package/dist/agent/fake-provider.d.ts +9 -0
- package/dist/agent/fake-provider.d.ts.map +1 -0
- package/dist/agent/fake-provider.js +51 -0
- package/dist/agent/fake-provider.js.map +1 -0
- package/dist/agent/index.d.ts +10 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/loop.d.ts +11 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +156 -0
- package/dist/agent/loop.js.map +1 -0
- package/dist/agent/providers.d.ts +17 -0
- package/dist/agent/providers.d.ts.map +1 -0
- package/dist/agent/providers.js +171 -0
- package/dist/agent/providers.js.map +1 -0
- package/dist/agent/tools.d.ts +8 -0
- package/dist/agent/tools.d.ts.map +1 -0
- package/dist/agent/tools.js +233 -0
- package/dist/agent/tools.js.map +1 -0
- package/dist/agent/types.d.ts +84 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +2 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +28 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +5 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/options.d.ts +21 -0
- package/dist/cli/options.d.ts.map +1 -0
- package/dist/cli/options.js +97 -0
- package/dist/cli/options.js.map +1 -0
- package/dist/cli/run.d.ts +21 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +144 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +3 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +14 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +352 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +6 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +72 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/types.d.ts +53 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/doctor/doctor.d.ts +47 -0
- package/dist/doctor/doctor.d.ts.map +1 -0
- package/dist/doctor/doctor.js +159 -0
- package/dist/doctor/doctor.js.map +1 -0
- package/dist/embeddings/index.d.ts +117 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +515 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/discovery.d.ts +10 -0
- package/dist/indexer/discovery.d.ts.map +1 -0
- package/dist/indexer/discovery.js +38 -0
- package/dist/indexer/discovery.js.map +1 -0
- package/dist/indexer/hash.d.ts +3 -0
- package/dist/indexer/hash.d.ts.map +1 -0
- package/dist/indexer/hash.js +11 -0
- package/dist/indexer/hash.js.map +1 -0
- package/dist/indexer/index.d.ts +7 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +5 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/kinds.d.ts +4 -0
- package/dist/indexer/kinds.d.ts.map +1 -0
- package/dist/indexer/kinds.js +38 -0
- package/dist/indexer/kinds.js.map +1 -0
- package/dist/indexer/run.d.ts +42 -0
- package/dist/indexer/run.d.ts.map +1 -0
- package/dist/indexer/run.js +563 -0
- package/dist/indexer/run.js.map +1 -0
- package/dist/logging/logger.d.ts +11 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +18 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/mcp/index.d.ts +35 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +392 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/parsers/chunking.d.ts +4 -0
- package/dist/parsers/chunking.d.ts.map +1 -0
- package/dist/parsers/chunking.js +112 -0
- package/dist/parsers/chunking.js.map +1 -0
- package/dist/parsers/index.d.ts +8 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +5 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/line-parser.d.ts +13 -0
- package/dist/parsers/line-parser.d.ts.map +1 -0
- package/dist/parsers/line-parser.js +242 -0
- package/dist/parsers/line-parser.js.map +1 -0
- package/dist/parsers/normalize.d.ts +4 -0
- package/dist/parsers/normalize.d.ts.map +1 -0
- package/dist/parsers/normalize.js +17 -0
- package/dist/parsers/normalize.js.map +1 -0
- package/dist/parsers/parser.d.ts +14 -0
- package/dist/parsers/parser.d.ts.map +1 -0
- package/dist/parsers/parser.js +77 -0
- package/dist/parsers/parser.js.map +1 -0
- package/dist/parsers/pdf2llm.d.ts +14 -0
- package/dist/parsers/pdf2llm.d.ts.map +1 -0
- package/dist/parsers/pdf2llm.js +215 -0
- package/dist/parsers/pdf2llm.js.map +1 -0
- package/dist/parsers/types.d.ts +48 -0
- package/dist/parsers/types.d.ts.map +1 -0
- package/dist/parsers/types.js +2 -0
- package/dist/parsers/types.js.map +1 -0
- package/dist/retrieval/index.d.ts +4 -0
- package/dist/retrieval/index.d.ts.map +1 -0
- package/dist/retrieval/index.js +3 -0
- package/dist/retrieval/index.js.map +1 -0
- package/dist/retrieval/schemas.d.ts +127 -0
- package/dist/retrieval/schemas.d.ts.map +1 -0
- package/dist/retrieval/schemas.js +65 -0
- package/dist/retrieval/schemas.js.map +1 -0
- package/dist/retrieval/tools.d.ts +31 -0
- package/dist/retrieval/tools.d.ts.map +1 -0
- package/dist/retrieval/tools.js +803 -0
- package/dist/retrieval/tools.js.map +1 -0
- package/dist/retrieval/types.d.ts +104 -0
- package/dist/retrieval/types.d.ts.map +1 -0
- package/dist/retrieval/types.js +2 -0
- package/dist/retrieval/types.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/server.d.ts +49 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +517 -0
- package/dist/server/server.js.map +1 -0
- package/dist/shared/types.d.ts +98 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/storage/database.d.ts +18 -0
- package/dist/storage/database.d.ts.map +1 -0
- package/dist/storage/database.js +66 -0
- package/dist/storage/database.js.map +1 -0
- package/dist/storage/index.d.ts +8 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/migrations.d.ts +15 -0
- package/dist/storage/migrations.d.ts.map +1 -0
- package/dist/storage/migrations.js +170 -0
- package/dist/storage/migrations.js.map +1 -0
- package/dist/storage/paths.d.ts +15 -0
- package/dist/storage/paths.d.ts.map +1 -0
- package/dist/storage/paths.js +27 -0
- package/dist/storage/paths.js.map +1 -0
- package/dist/storage/schema.d.ts +11 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +40 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/static-app.d.ts +2 -0
- package/dist/ui/static-app.d.ts.map +1 -0
- package/dist/ui/static-app.js +735 -0
- package/dist/ui/static-app.js.map +1 -0
- package/docs/adr/0001-sqlite-driver.md +23 -0
- package/docs/agentic-rag-specifications.md +659 -0
- package/docs/agentic-rag-study.md +1056 -0
- package/docs/implementation-plan.md +1425 -0
- package/docs/mcp-integration.md +154 -0
- package/docs/validation-corpus.md +71 -0
- package/package.json +79 -0
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createDeterministicEmbeddingVector, decodeVector } from "../embeddings/index.js";
|
|
4
|
+
import { UserFacingError } from "../errors.js";
|
|
5
|
+
import { openDatabase, resolveAthenaPaths } from "../storage/index.js";
|
|
6
|
+
export class RetrievalToolRuntime {
|
|
7
|
+
trace = {
|
|
8
|
+
seenChunkIds: new Set(),
|
|
9
|
+
readLocators: [],
|
|
10
|
+
searchScores: []
|
|
11
|
+
};
|
|
12
|
+
database;
|
|
13
|
+
root;
|
|
14
|
+
constructor(root, dbPath) {
|
|
15
|
+
const paths = resolveAthenaPaths(root, dbPath);
|
|
16
|
+
this.root = paths.root;
|
|
17
|
+
this.database = openDatabase(paths.databasePath, {
|
|
18
|
+
readonly: true,
|
|
19
|
+
fileMustExist: true
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
close() {
|
|
23
|
+
this.database.close();
|
|
24
|
+
}
|
|
25
|
+
searchDocs(input) {
|
|
26
|
+
return this.searchDocsWithMode(input, "hybrid");
|
|
27
|
+
}
|
|
28
|
+
searchDocsWithMode(input, mode) {
|
|
29
|
+
const query = requireNonEmptyString(input.query, "query");
|
|
30
|
+
const limit = clampLimit(input.limit, 10, 50);
|
|
31
|
+
const ftsQuery = toFtsQuery(query);
|
|
32
|
+
const candidates = new Map();
|
|
33
|
+
if (mode !== "vector") {
|
|
34
|
+
for (const candidate of this.searchFtsCandidates(input, query, ftsQuery, limit)) {
|
|
35
|
+
candidates.set(candidate.result.chunkId, candidate);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (mode !== "fts") {
|
|
39
|
+
for (const candidate of this.searchVectorCandidates(input, query, limit)) {
|
|
40
|
+
const existing = candidates.get(candidate.result.chunkId);
|
|
41
|
+
if (existing === undefined) {
|
|
42
|
+
candidates.set(candidate.result.chunkId, candidate);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
existing.vectorScore = Math.max(existing.vectorScore, candidate.vectorScore);
|
|
46
|
+
existing.result.scores.vector = existing.vectorScore;
|
|
47
|
+
existing.result.scores.merged = mergeScoreForLocator(existing.ftsScore, existing.vectorScore, existing.result.locator);
|
|
48
|
+
existing.result.score = existing.result.scores.merged;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const rankedCandidates = [...candidates.values()].sort(compareCandidates);
|
|
52
|
+
const results = this.expandNeighborCandidates(rankedCandidates, limit)
|
|
53
|
+
.sort(compareCandidates)
|
|
54
|
+
.map((candidate) => candidate.result)
|
|
55
|
+
.sort(compareResults)
|
|
56
|
+
.slice(0, limit);
|
|
57
|
+
for (const result of results) {
|
|
58
|
+
this.trace.seenChunkIds.add(result.chunkId);
|
|
59
|
+
this.trace.searchScores.push({
|
|
60
|
+
chunkId: result.chunkId,
|
|
61
|
+
path: result.path,
|
|
62
|
+
ftsScore: result.scores.fts,
|
|
63
|
+
vectorScore: result.scores.vector,
|
|
64
|
+
mergedScore: result.scores.merged,
|
|
65
|
+
...(result.contextOfChunkId === undefined
|
|
66
|
+
? {}
|
|
67
|
+
: { contextOfChunkId: result.contextOfChunkId })
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
readSourceRange(input) {
|
|
73
|
+
const source = this.getSourceByPath(input.path);
|
|
74
|
+
const startLine = requirePositiveInteger(input.startLine, "startLine");
|
|
75
|
+
const endLine = requirePositiveInteger(input.endLine, "endLine");
|
|
76
|
+
if (endLine < startLine) {
|
|
77
|
+
throw new UserFacingError("endLine must be greater than or equal to startLine.");
|
|
78
|
+
}
|
|
79
|
+
return this.readSourceRangeFromSource(source, startLine, endLine);
|
|
80
|
+
}
|
|
81
|
+
readPdfSource(input) {
|
|
82
|
+
const source = this.getSourceByPath(input.path);
|
|
83
|
+
if (source.kind !== "pdf") {
|
|
84
|
+
throw new UserFacingError(`Source is not a PDF: ${source.relative_path}`);
|
|
85
|
+
}
|
|
86
|
+
const requestedPage = input.page === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: requirePositiveInteger(input.page, "page");
|
|
89
|
+
const requestedRegionId = input.regionId === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: requireNonEmptyString(input.regionId, "regionId");
|
|
92
|
+
const artifacts = this.getParserArtifacts(source.id);
|
|
93
|
+
const markdown = readGeneratedPdfMarkdown(artifacts, source.relative_path);
|
|
94
|
+
const sourceMap = readArtifactPayload(artifacts, "source-map");
|
|
95
|
+
const warnings = readArrayArtifactPayload(artifacts, "warnings");
|
|
96
|
+
const confidence = readArtifactPayload(artifacts, "confidence");
|
|
97
|
+
const assets = readArrayArtifactPayload(artifacts, "assets");
|
|
98
|
+
const locators = this.getPdfLocators(source.id);
|
|
99
|
+
const pageCount = inferPdfPageCount(artifacts, locators);
|
|
100
|
+
const locator = selectPdfLocator({
|
|
101
|
+
path: source.relative_path,
|
|
102
|
+
locators,
|
|
103
|
+
page: requestedPage,
|
|
104
|
+
regionId: requestedRegionId,
|
|
105
|
+
pageCount
|
|
106
|
+
});
|
|
107
|
+
const markdownLineCount = countMarkdownLines(markdown);
|
|
108
|
+
const markdownStartLine = clampLine(locator.markdownStartLine ?? 1, markdownLineCount);
|
|
109
|
+
const markdownEndLine = clampLine(locator.markdownEndLine ?? markdownLineCount, markdownLineCount);
|
|
110
|
+
this.trace.readLocators.push(locator);
|
|
111
|
+
return {
|
|
112
|
+
path: source.relative_path,
|
|
113
|
+
page: locator.page,
|
|
114
|
+
markdown: sliceMarkdownLines(markdown, markdownStartLine, markdownEndLine),
|
|
115
|
+
markdownStartLine,
|
|
116
|
+
markdownEndLine,
|
|
117
|
+
locator,
|
|
118
|
+
sourceMapEntries: filterSourceMapEntries(sourceMap, locator),
|
|
119
|
+
warnings,
|
|
120
|
+
confidence,
|
|
121
|
+
assets
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async grepDocs(input) {
|
|
125
|
+
const pattern = requireNonEmptyString(input.pattern, "pattern");
|
|
126
|
+
if (pattern.length > 128) {
|
|
127
|
+
throw new UserFacingError("grep pattern must be 128 characters or fewer.");
|
|
128
|
+
}
|
|
129
|
+
const limit = clampLimit(input.limit, 20, 100);
|
|
130
|
+
const listInput = { limit: 500 };
|
|
131
|
+
if (input.pathPrefix !== undefined) {
|
|
132
|
+
listInput.pathPrefix = input.pathPrefix;
|
|
133
|
+
}
|
|
134
|
+
if (input.kind !== undefined) {
|
|
135
|
+
listInput.kind = input.kind;
|
|
136
|
+
}
|
|
137
|
+
const sources = this.listDocs(listInput);
|
|
138
|
+
const matcher = createMatcher(pattern, input.regex === true);
|
|
139
|
+
const results = [];
|
|
140
|
+
for (const source of sources) {
|
|
141
|
+
const filePath = path.join(this.root, source.path);
|
|
142
|
+
const lines = (await readFile(filePath, "utf8"))
|
|
143
|
+
.replace(/\r\n?/gu, "\n")
|
|
144
|
+
.split("\n");
|
|
145
|
+
for (const [index, line] of lines.entries()) {
|
|
146
|
+
if (!matcher(line)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const locator = {
|
|
150
|
+
type: "text",
|
|
151
|
+
path: source.path,
|
|
152
|
+
startLine: index + 1,
|
|
153
|
+
endLine: index + 1
|
|
154
|
+
};
|
|
155
|
+
this.trace.readLocators.push(locator);
|
|
156
|
+
results.push({
|
|
157
|
+
path: source.path,
|
|
158
|
+
lineNumber: index + 1,
|
|
159
|
+
line,
|
|
160
|
+
locator
|
|
161
|
+
});
|
|
162
|
+
if (results.length >= limit) {
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
listDocs(input = {}) {
|
|
170
|
+
const filters = ["root = ?"];
|
|
171
|
+
const params = [this.root];
|
|
172
|
+
if (input.pathPrefix !== undefined) {
|
|
173
|
+
filters.push("relative_path LIKE ? ESCAPE '\\'");
|
|
174
|
+
params.push(`${escapeLike(input.pathPrefix)}%`);
|
|
175
|
+
}
|
|
176
|
+
if (input.kind !== undefined) {
|
|
177
|
+
filters.push("kind = ?");
|
|
178
|
+
params.push(input.kind);
|
|
179
|
+
}
|
|
180
|
+
params.push(clampLimit(input.limit, 100, 500));
|
|
181
|
+
const whereClause = filters.length === 0 ? "" : `WHERE ${filters.join(" AND ")}`;
|
|
182
|
+
const rows = this.database
|
|
183
|
+
.prepare([
|
|
184
|
+
"SELECT id, root, relative_path, kind, size_bytes, content_hash",
|
|
185
|
+
"FROM sources",
|
|
186
|
+
whereClause,
|
|
187
|
+
"ORDER BY relative_path LIMIT ?"
|
|
188
|
+
].join(" "))
|
|
189
|
+
.all(...params);
|
|
190
|
+
return rows.map(toListDocsResult);
|
|
191
|
+
}
|
|
192
|
+
getDoc(pathValue) {
|
|
193
|
+
const source = this.findSourceByPath(pathValue);
|
|
194
|
+
return source === null ? null : toListDocsResult(source);
|
|
195
|
+
}
|
|
196
|
+
relatedChunks(input) {
|
|
197
|
+
const before = clampLimit(input.before, 1, 10, 0);
|
|
198
|
+
const after = clampLimit(input.after, 1, 10, 0);
|
|
199
|
+
const center = this.getChunkById(input.chunkId);
|
|
200
|
+
const rows = this.database
|
|
201
|
+
.prepare([
|
|
202
|
+
"SELECT chunks.id, chunks.source_id, sources.relative_path, chunks.ordinal,",
|
|
203
|
+
"chunks.text, chunks.locator_json, chunks.previous_chunk_id,",
|
|
204
|
+
"chunks.next_chunk_id",
|
|
205
|
+
"FROM chunks",
|
|
206
|
+
"JOIN sources ON sources.id = chunks.source_id",
|
|
207
|
+
"WHERE chunks.source_id = ?",
|
|
208
|
+
"AND chunks.ordinal BETWEEN ? AND ?",
|
|
209
|
+
"ORDER BY chunks.ordinal"
|
|
210
|
+
].join(" "))
|
|
211
|
+
.all(center.source_id, center.ordinal - before, center.ordinal + after);
|
|
212
|
+
for (const row of rows) {
|
|
213
|
+
this.trace.seenChunkIds.add(row.id);
|
|
214
|
+
}
|
|
215
|
+
return rows.map((row) => ({
|
|
216
|
+
chunkId: row.id,
|
|
217
|
+
path: row.relative_path,
|
|
218
|
+
ordinal: row.ordinal,
|
|
219
|
+
text: row.text,
|
|
220
|
+
locator: parseLocator(row.locator_json)
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
async readSourceRangeFromSource(source, startLine, endLine) {
|
|
224
|
+
const filePath = path.join(this.root, source.relative_path);
|
|
225
|
+
const lines = (await readFile(filePath, "utf8"))
|
|
226
|
+
.replace(/\r\n?/gu, "\n")
|
|
227
|
+
.split("\n");
|
|
228
|
+
if (endLine > lines.length) {
|
|
229
|
+
throw new UserFacingError(`Line range ${String(startLine)}-${String(endLine)} is outside ${source.relative_path}.`);
|
|
230
|
+
}
|
|
231
|
+
const locator = {
|
|
232
|
+
type: "text",
|
|
233
|
+
path: source.relative_path,
|
|
234
|
+
startLine,
|
|
235
|
+
endLine
|
|
236
|
+
};
|
|
237
|
+
this.trace.readLocators.push(locator);
|
|
238
|
+
return {
|
|
239
|
+
path: source.relative_path,
|
|
240
|
+
startLine,
|
|
241
|
+
endLine,
|
|
242
|
+
text: lines.slice(startLine - 1, endLine).join("\n"),
|
|
243
|
+
locator
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
searchFtsCandidates(input, query, ftsQuery, limit) {
|
|
247
|
+
if (ftsQuery === "") {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
const { filters, params } = buildSourceFilters(input, "sources", this.root);
|
|
251
|
+
filters.unshift("chunks_fts MATCH ?");
|
|
252
|
+
params.unshift(ftsQuery);
|
|
253
|
+
params.push(limit * 4);
|
|
254
|
+
const rows = this.database
|
|
255
|
+
.prepare([
|
|
256
|
+
"SELECT chunks.id AS chunk_id, chunks.source_id, sources.relative_path,",
|
|
257
|
+
"sources.kind, chunks.heading_path_json, chunks.locator_json,",
|
|
258
|
+
"chunks.text, bm25(chunks_fts) AS fts_rank,",
|
|
259
|
+
"snippet(chunks_fts, 1, '[', ']', '...', 12) AS snippet",
|
|
260
|
+
"FROM chunks_fts",
|
|
261
|
+
"JOIN chunks ON chunks.id = chunks_fts.chunk_id",
|
|
262
|
+
"JOIN sources ON sources.id = chunks.source_id",
|
|
263
|
+
`WHERE ${filters.join(" AND ")}`,
|
|
264
|
+
"ORDER BY fts_rank LIMIT ?"
|
|
265
|
+
].join(" "))
|
|
266
|
+
.all(...params);
|
|
267
|
+
return rows.map((row) => this.toFtsCandidate(row, query));
|
|
268
|
+
}
|
|
269
|
+
searchVectorCandidates(input, query, limit) {
|
|
270
|
+
const { filters, params } = buildSourceFilters(input, "sources", this.root);
|
|
271
|
+
filters.unshift("embeddings.vector IS NOT NULL");
|
|
272
|
+
const rows = this.database
|
|
273
|
+
.prepare([
|
|
274
|
+
"SELECT chunks.id AS chunk_id, chunks.source_id, sources.relative_path,",
|
|
275
|
+
"sources.kind, chunks.heading_path_json, chunks.locator_json,",
|
|
276
|
+
"chunks.text, embeddings.vector, embeddings.metadata_json,",
|
|
277
|
+
"embeddings.dimensions",
|
|
278
|
+
"FROM embeddings",
|
|
279
|
+
"JOIN chunks ON chunks.id = embeddings.chunk_id",
|
|
280
|
+
"JOIN sources ON sources.id = chunks.source_id",
|
|
281
|
+
`WHERE ${filters.join(" AND ")}`,
|
|
282
|
+
"ORDER BY sources.relative_path, chunks.ordinal"
|
|
283
|
+
].join(" "))
|
|
284
|
+
.all(...params);
|
|
285
|
+
const candidates = [];
|
|
286
|
+
const queryVectors = new Map();
|
|
287
|
+
for (const row of rows) {
|
|
288
|
+
if (row.vector === null) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const metadata = parseEmbeddingMetadata(row.metadata_json, row.dimensions);
|
|
292
|
+
const cacheKey = `${String(metadata.dimensions)}:${String(metadata.normalize)}`;
|
|
293
|
+
let queryVector = queryVectors.get(cacheKey);
|
|
294
|
+
if (queryVector === undefined) {
|
|
295
|
+
queryVector = createDeterministicEmbeddingVector(query, metadata.dimensions);
|
|
296
|
+
if (metadata.normalize) {
|
|
297
|
+
normalizeVector(queryVector);
|
|
298
|
+
}
|
|
299
|
+
queryVectors.set(cacheKey, queryVector);
|
|
300
|
+
}
|
|
301
|
+
const similarity = cosineSimilarity(queryVector, decodeVector(row.vector));
|
|
302
|
+
if (similarity <= 0) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
candidates.push(this.toVectorCandidate(row, similarity, query));
|
|
306
|
+
}
|
|
307
|
+
return candidates
|
|
308
|
+
.sort((left, right) => right.vectorScore - left.vectorScore)
|
|
309
|
+
.slice(0, limit * 4);
|
|
310
|
+
}
|
|
311
|
+
toFtsCandidate(row, query) {
|
|
312
|
+
const headingPath = parseHeadingPath(row.heading_path_json);
|
|
313
|
+
const locator = parseLocator(row.locator_json);
|
|
314
|
+
const ftsScore = 1 / (1 + Math.abs(row.fts_rank)) +
|
|
315
|
+
this.rankingAdjustments(row.relative_path, row.text, headingPath, query);
|
|
316
|
+
const mergedScore = mergeScoreForLocator(ftsScore, 0, locator);
|
|
317
|
+
return {
|
|
318
|
+
ftsScore,
|
|
319
|
+
vectorScore: 0,
|
|
320
|
+
result: {
|
|
321
|
+
chunkId: row.chunk_id,
|
|
322
|
+
sourceId: row.source_id,
|
|
323
|
+
path: row.relative_path,
|
|
324
|
+
kind: row.kind,
|
|
325
|
+
headingPath,
|
|
326
|
+
snippet: row.snippet,
|
|
327
|
+
score: mergedScore,
|
|
328
|
+
scores: {
|
|
329
|
+
fts: ftsScore,
|
|
330
|
+
vector: 0,
|
|
331
|
+
merged: mergedScore
|
|
332
|
+
},
|
|
333
|
+
locator
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
toVectorCandidate(row, similarity, query) {
|
|
338
|
+
const headingPath = parseHeadingPath(row.heading_path_json);
|
|
339
|
+
const locator = parseLocator(row.locator_json);
|
|
340
|
+
const vectorScore = similarity +
|
|
341
|
+
this.rankingAdjustments(row.relative_path, row.text, headingPath, query);
|
|
342
|
+
const mergedScore = mergeScoreForLocator(0, vectorScore, locator);
|
|
343
|
+
return {
|
|
344
|
+
ftsScore: 0,
|
|
345
|
+
vectorScore,
|
|
346
|
+
result: {
|
|
347
|
+
chunkId: row.chunk_id,
|
|
348
|
+
sourceId: row.source_id,
|
|
349
|
+
path: row.relative_path,
|
|
350
|
+
kind: row.kind,
|
|
351
|
+
headingPath,
|
|
352
|
+
snippet: createSnippet(row.text),
|
|
353
|
+
score: mergedScore,
|
|
354
|
+
scores: {
|
|
355
|
+
fts: 0,
|
|
356
|
+
vector: vectorScore,
|
|
357
|
+
merged: mergedScore
|
|
358
|
+
},
|
|
359
|
+
locator
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
expandNeighborCandidates(candidates, limit) {
|
|
364
|
+
const expanded = [...candidates];
|
|
365
|
+
const seenChunkIds = new Set(candidates.map((candidate) => candidate.result.chunkId));
|
|
366
|
+
const seedCandidates = candidates.slice(0, Math.min(limit, 3));
|
|
367
|
+
for (const candidate of seedCandidates) {
|
|
368
|
+
const center = this.getChunkById(candidate.result.chunkId);
|
|
369
|
+
const neighbors = this.getNeighborRows(center);
|
|
370
|
+
for (const neighbor of neighbors) {
|
|
371
|
+
if (neighbor.chunk_id === candidate.result.chunkId) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (seenChunkIds.has(neighbor.chunk_id)) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
seenChunkIds.add(neighbor.chunk_id);
|
|
378
|
+
expanded.push(this.toNeighborCandidate(neighbor, candidate));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return expanded;
|
|
382
|
+
}
|
|
383
|
+
getNeighborRows(center) {
|
|
384
|
+
return this.database
|
|
385
|
+
.prepare([
|
|
386
|
+
"SELECT chunks.id AS chunk_id, chunks.source_id, sources.relative_path,",
|
|
387
|
+
"sources.kind, chunks.heading_path_json, chunks.locator_json,",
|
|
388
|
+
"chunks.ordinal, chunks.text",
|
|
389
|
+
"FROM chunks",
|
|
390
|
+
"JOIN sources ON sources.id = chunks.source_id",
|
|
391
|
+
"WHERE chunks.source_id = ?",
|
|
392
|
+
"AND chunks.ordinal BETWEEN ? AND ?",
|
|
393
|
+
"ORDER BY chunks.ordinal"
|
|
394
|
+
].join(" "))
|
|
395
|
+
.all(center.source_id, center.ordinal - 1, center.ordinal + 1);
|
|
396
|
+
}
|
|
397
|
+
toNeighborCandidate(row, sourceCandidate) {
|
|
398
|
+
const inheritedFtsScore = Math.max(0, sourceCandidate.ftsScore) * 0.65;
|
|
399
|
+
const inheritedVectorScore = Math.max(0, sourceCandidate.vectorScore) * 0.65;
|
|
400
|
+
const mergedScore = mergeScore(inheritedFtsScore, inheritedVectorScore);
|
|
401
|
+
return {
|
|
402
|
+
ftsScore: inheritedFtsScore,
|
|
403
|
+
vectorScore: inheritedVectorScore,
|
|
404
|
+
result: {
|
|
405
|
+
chunkId: row.chunk_id,
|
|
406
|
+
sourceId: row.source_id,
|
|
407
|
+
path: row.relative_path,
|
|
408
|
+
kind: row.kind,
|
|
409
|
+
headingPath: parseHeadingPath(row.heading_path_json),
|
|
410
|
+
snippet: createSnippet(row.text),
|
|
411
|
+
score: mergedScore,
|
|
412
|
+
contextOfChunkId: sourceCandidate.result.chunkId,
|
|
413
|
+
scores: {
|
|
414
|
+
fts: inheritedFtsScore,
|
|
415
|
+
vector: inheritedVectorScore,
|
|
416
|
+
merged: mergedScore
|
|
417
|
+
},
|
|
418
|
+
locator: parseLocator(row.locator_json)
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
rankingAdjustments(relativePath, text, headingPath, query) {
|
|
423
|
+
return rankingAdjustments({
|
|
424
|
+
relativePath,
|
|
425
|
+
text,
|
|
426
|
+
headingPath,
|
|
427
|
+
query,
|
|
428
|
+
recentReadPaths: new Set(this.trace.readLocators.map((locator) => locator.path))
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
getSourceByPath(pathValue) {
|
|
432
|
+
const source = this.findSourceByPath(pathValue);
|
|
433
|
+
if (source === null) {
|
|
434
|
+
throw new UserFacingError(`Unknown indexed source: ${pathValue}`);
|
|
435
|
+
}
|
|
436
|
+
return source;
|
|
437
|
+
}
|
|
438
|
+
findSourceByPath(pathValue) {
|
|
439
|
+
if (pathValue.includes("..") || path.isAbsolute(pathValue)) {
|
|
440
|
+
throw new UserFacingError("Source path must be repository-relative.");
|
|
441
|
+
}
|
|
442
|
+
const source = this.database
|
|
443
|
+
.prepare([
|
|
444
|
+
"SELECT id, root, relative_path, kind, size_bytes, content_hash",
|
|
445
|
+
"FROM sources WHERE root = ? AND relative_path = ?"
|
|
446
|
+
].join(" "))
|
|
447
|
+
.get(this.root, pathValue);
|
|
448
|
+
return source ?? null;
|
|
449
|
+
}
|
|
450
|
+
getChunkById(chunkId) {
|
|
451
|
+
const chunk = this.database
|
|
452
|
+
.prepare([
|
|
453
|
+
"SELECT chunks.id, chunks.source_id, sources.relative_path, chunks.ordinal,",
|
|
454
|
+
"chunks.text, chunks.locator_json, chunks.previous_chunk_id,",
|
|
455
|
+
"chunks.next_chunk_id",
|
|
456
|
+
"FROM chunks",
|
|
457
|
+
"JOIN sources ON sources.id = chunks.source_id",
|
|
458
|
+
"WHERE sources.root = ? AND chunks.id = ?"
|
|
459
|
+
].join(" "))
|
|
460
|
+
.get(this.root, chunkId);
|
|
461
|
+
if (chunk === undefined) {
|
|
462
|
+
throw new UserFacingError(`Unknown chunk: ${chunkId}`);
|
|
463
|
+
}
|
|
464
|
+
return chunk;
|
|
465
|
+
}
|
|
466
|
+
getParserArtifacts(sourceId) {
|
|
467
|
+
return this.database
|
|
468
|
+
.prepare([
|
|
469
|
+
"SELECT kind, payload_json FROM parser_artifacts",
|
|
470
|
+
"WHERE source_id = ? ORDER BY kind, id"
|
|
471
|
+
].join(" "))
|
|
472
|
+
.all(sourceId);
|
|
473
|
+
}
|
|
474
|
+
getPdfLocators(sourceId) {
|
|
475
|
+
const rows = this.database
|
|
476
|
+
.prepare("SELECT locator_json FROM chunks WHERE source_id = ? ORDER BY ordinal")
|
|
477
|
+
.all(sourceId);
|
|
478
|
+
return rows
|
|
479
|
+
.map((row) => parseLocator(row.locator_json))
|
|
480
|
+
.filter((locator) => locator.type === "pdf");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function buildSourceFilters(input, sourceAlias, root) {
|
|
484
|
+
const filters = [`${sourceAlias}.root = ?`];
|
|
485
|
+
const params = [root];
|
|
486
|
+
if (input.pathPrefix !== undefined) {
|
|
487
|
+
filters.push(`${sourceAlias}.relative_path LIKE ? ESCAPE '\\'`);
|
|
488
|
+
params.push(`${escapeLike(input.pathPrefix)}%`);
|
|
489
|
+
}
|
|
490
|
+
if (input.kind !== undefined) {
|
|
491
|
+
filters.push(`${sourceAlias}.kind = ?`);
|
|
492
|
+
params.push(input.kind);
|
|
493
|
+
}
|
|
494
|
+
return { filters, params };
|
|
495
|
+
}
|
|
496
|
+
function toListDocsResult(row) {
|
|
497
|
+
return {
|
|
498
|
+
sourceId: row.id,
|
|
499
|
+
path: row.relative_path,
|
|
500
|
+
kind: row.kind,
|
|
501
|
+
sizeBytes: row.size_bytes,
|
|
502
|
+
contentHash: row.content_hash
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function mergeScore(ftsScore, vectorScore) {
|
|
506
|
+
return ftsScore + vectorScore * 2;
|
|
507
|
+
}
|
|
508
|
+
function mergeScoreForLocator(ftsScore, vectorScore, locator) {
|
|
509
|
+
return mergeScore(ftsScore, vectorScore) - pdfConfidencePenalty(locator);
|
|
510
|
+
}
|
|
511
|
+
function compareCandidates(left, right) {
|
|
512
|
+
return compareResults(left.result, right.result);
|
|
513
|
+
}
|
|
514
|
+
function compareResults(left, right) {
|
|
515
|
+
const scoreDifference = right.score - left.score;
|
|
516
|
+
if (scoreDifference !== 0) {
|
|
517
|
+
return scoreDifference;
|
|
518
|
+
}
|
|
519
|
+
const pathDifference = left.path.localeCompare(right.path);
|
|
520
|
+
if (pathDifference !== 0) {
|
|
521
|
+
return pathDifference;
|
|
522
|
+
}
|
|
523
|
+
return left.chunkId.localeCompare(right.chunkId);
|
|
524
|
+
}
|
|
525
|
+
function pdfConfidencePenalty(locator) {
|
|
526
|
+
if (locator.type !== "pdf") {
|
|
527
|
+
return 0;
|
|
528
|
+
}
|
|
529
|
+
const confidencePenalty = typeof locator.confidence === "number" && locator.confidence < 0.65
|
|
530
|
+
? (0.65 - locator.confidence) * 1.5
|
|
531
|
+
: 0;
|
|
532
|
+
const warningPenalty = locator.warningCodes?.includes("pdf-low-confidence")
|
|
533
|
+
? 0.75
|
|
534
|
+
: 0;
|
|
535
|
+
return confidencePenalty + warningPenalty;
|
|
536
|
+
}
|
|
537
|
+
function rankingAdjustments(input) {
|
|
538
|
+
const normalizedQuery = input.query.toLowerCase();
|
|
539
|
+
const queryTerms = tokenizeRankingText(input.query);
|
|
540
|
+
const queryTermSet = new Set(queryTerms);
|
|
541
|
+
const headingText = input.headingPath.join(" ").toLowerCase();
|
|
542
|
+
const headingTerms = new Set(tokenizeRankingText(headingText));
|
|
543
|
+
const sourceTitleTerms = new Set(tokenizeRankingText(sourceTitle(input.relativePath)));
|
|
544
|
+
const identifierMatches = input.query.match(/[A-Z][A-Z0-9_]{3,}/gu) ?? [];
|
|
545
|
+
const searchableText = `${input.relativePath}\n${headingText}\n${input.text}`;
|
|
546
|
+
const exactBoost = normalizedQuery !== "" && input.text.toLowerCase().includes(normalizedQuery)
|
|
547
|
+
? 2
|
|
548
|
+
: 0;
|
|
549
|
+
const headingBoost = normalizedQuery !== "" && headingText.includes(normalizedQuery)
|
|
550
|
+
? 1.25
|
|
551
|
+
: queryTermsMatch(headingTerms, queryTerms)
|
|
552
|
+
? 0.9
|
|
553
|
+
: queryTermsContainAll(headingTerms, queryTermSet)
|
|
554
|
+
? 0.8
|
|
555
|
+
: 0;
|
|
556
|
+
const sourceTitleBoost = queryTermsMatch(sourceTitleTerms, queryTerms)
|
|
557
|
+
? 1.25
|
|
558
|
+
: queryTermsContainAll(sourceTitleTerms, queryTermSet)
|
|
559
|
+
? 1.1
|
|
560
|
+
: queryTermsOverlap(sourceTitleTerms, queryTerms)
|
|
561
|
+
? 0.4
|
|
562
|
+
: 0;
|
|
563
|
+
const identifierBoost = identifierMatches.some((identifier) => searchableText.includes(identifier))
|
|
564
|
+
? 1.75
|
|
565
|
+
: 0;
|
|
566
|
+
const recentReadBoost = input.recentReadPaths.has(input.relativePath)
|
|
567
|
+
? 0.75
|
|
568
|
+
: 0;
|
|
569
|
+
const genericPenalty = genericChunkPenalty(input, queryTerms);
|
|
570
|
+
const stalePenalty = /(legacy|deprecated|superseded|old)/iu.test(`${input.relativePath}\n${input.text}`)
|
|
571
|
+
? 2
|
|
572
|
+
: 0;
|
|
573
|
+
return (exactBoost +
|
|
574
|
+
headingBoost +
|
|
575
|
+
sourceTitleBoost +
|
|
576
|
+
identifierBoost +
|
|
577
|
+
recentReadBoost -
|
|
578
|
+
genericPenalty -
|
|
579
|
+
stalePenalty);
|
|
580
|
+
}
|
|
581
|
+
function genericChunkPenalty(input, queryTerms) {
|
|
582
|
+
const headings = input.headingPath.map((heading) => heading.toLowerCase());
|
|
583
|
+
const lastHeading = headings.at(-1) ?? "";
|
|
584
|
+
const title = sourceTitle(input.relativePath).toLowerCase();
|
|
585
|
+
const genericHeadingPenalty = /^(overview|introduction|summary|general|notes|misc|reference)$/u.test(lastHeading) ||
|
|
586
|
+
/^(overview|introduction|summary|general|notes|misc|reference)$/u.test(title)
|
|
587
|
+
? 1.2
|
|
588
|
+
: 0;
|
|
589
|
+
const textTerms = tokenizeRankingText(input.text);
|
|
590
|
+
const queryTermSet = new Set(queryTerms);
|
|
591
|
+
const queryDensity = textTerms.length === 0
|
|
592
|
+
? 0
|
|
593
|
+
: textTerms.filter((term) => queryTermSet.has(term)).length /
|
|
594
|
+
textTerms.length;
|
|
595
|
+
const broadLanguagePenalty = /\b(common|general|various|multiple|several|everything|overview)\b/iu.test(input.text) && queryDensity < 0.2
|
|
596
|
+
? 0.4
|
|
597
|
+
: 0;
|
|
598
|
+
return genericHeadingPenalty + broadLanguagePenalty;
|
|
599
|
+
}
|
|
600
|
+
function sourceTitle(relativePath) {
|
|
601
|
+
return path.basename(relativePath).replace(/\.[^.]+$/u, "");
|
|
602
|
+
}
|
|
603
|
+
function tokenizeRankingText(value) {
|
|
604
|
+
return value.toLowerCase().match(/[a-z0-9]+/gu) ?? [];
|
|
605
|
+
}
|
|
606
|
+
function queryTermsMatch(haystackTerms, queryTerms) {
|
|
607
|
+
return (queryTerms.length > 0 && queryTerms.every((term) => haystackTerms.has(term)));
|
|
608
|
+
}
|
|
609
|
+
function queryTermsOverlap(haystackTerms, queryTerms) {
|
|
610
|
+
return queryTerms.some((term) => haystackTerms.has(term));
|
|
611
|
+
}
|
|
612
|
+
function queryTermsContainAll(requiredTerms, queryTerms) {
|
|
613
|
+
return (requiredTerms.size > 1 &&
|
|
614
|
+
[...requiredTerms].every((term) => queryTerms.has(term)));
|
|
615
|
+
}
|
|
616
|
+
function parseEmbeddingMetadata(metadataJson, fallbackDimensions) {
|
|
617
|
+
try {
|
|
618
|
+
const parsed = JSON.parse(metadataJson);
|
|
619
|
+
return {
|
|
620
|
+
dimensions: typeof parsed.dimensions === "number"
|
|
621
|
+
? parsed.dimensions
|
|
622
|
+
: fallbackDimensions,
|
|
623
|
+
normalize: typeof parsed.normalize === "boolean" ? parsed.normalize : true
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return {
|
|
628
|
+
dimensions: fallbackDimensions,
|
|
629
|
+
normalize: true
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function normalizeVector(vector) {
|
|
634
|
+
let magnitude = 0;
|
|
635
|
+
for (const value of vector) {
|
|
636
|
+
magnitude += value * value;
|
|
637
|
+
}
|
|
638
|
+
if (magnitude === 0) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const scale = 1 / Math.sqrt(magnitude);
|
|
642
|
+
for (let index = 0; index < vector.length; index += 1) {
|
|
643
|
+
vector[index] = (vector[index] ?? 0) * scale;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function cosineSimilarity(left, right) {
|
|
647
|
+
const length = Math.min(left.length, right.length);
|
|
648
|
+
let dot = 0;
|
|
649
|
+
let leftMagnitude = 0;
|
|
650
|
+
let rightMagnitude = 0;
|
|
651
|
+
for (let index = 0; index < length; index += 1) {
|
|
652
|
+
const leftValue = left[index] ?? 0;
|
|
653
|
+
const rightValue = right[index] ?? 0;
|
|
654
|
+
dot += leftValue * rightValue;
|
|
655
|
+
leftMagnitude += leftValue * leftValue;
|
|
656
|
+
rightMagnitude += rightValue * rightValue;
|
|
657
|
+
}
|
|
658
|
+
if (leftMagnitude === 0 || rightMagnitude === 0) {
|
|
659
|
+
return 0;
|
|
660
|
+
}
|
|
661
|
+
return dot / (Math.sqrt(leftMagnitude) * Math.sqrt(rightMagnitude));
|
|
662
|
+
}
|
|
663
|
+
function createSnippet(text) {
|
|
664
|
+
const normalized = text.replace(/\s+/gu, " ").trim();
|
|
665
|
+
if (normalized.length <= 240) {
|
|
666
|
+
return normalized;
|
|
667
|
+
}
|
|
668
|
+
return `${normalized.slice(0, 237)}...`;
|
|
669
|
+
}
|
|
670
|
+
function toFtsQuery(query) {
|
|
671
|
+
return (query
|
|
672
|
+
.match(/[A-Za-z0-9_./:-]+/gu)
|
|
673
|
+
?.map((term) => `"${term.replace(/"/gu, '""')}"`)
|
|
674
|
+
.join(" OR ") ?? "");
|
|
675
|
+
}
|
|
676
|
+
function createMatcher(pattern, regex) {
|
|
677
|
+
if (!regex) {
|
|
678
|
+
return (line) => line.includes(pattern);
|
|
679
|
+
}
|
|
680
|
+
if (pattern.length > 128) {
|
|
681
|
+
throw new UserFacingError("regex pattern must be 128 characters or fewer.");
|
|
682
|
+
}
|
|
683
|
+
let expression;
|
|
684
|
+
try {
|
|
685
|
+
expression = new RegExp(pattern, "u");
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
throw new UserFacingError("Invalid regex pattern.", error);
|
|
689
|
+
}
|
|
690
|
+
return (line) => expression.test(line);
|
|
691
|
+
}
|
|
692
|
+
function parseLocator(locatorJson) {
|
|
693
|
+
return JSON.parse(locatorJson);
|
|
694
|
+
}
|
|
695
|
+
function readGeneratedPdfMarkdown(artifacts, sourcePath) {
|
|
696
|
+
const payload = readArtifactPayload(artifacts, "generated-markdown");
|
|
697
|
+
if (!isRecord(payload) || typeof payload.markdown !== "string") {
|
|
698
|
+
throw new UserFacingError(`Generated PDF Markdown is unavailable for ${sourcePath}.`);
|
|
699
|
+
}
|
|
700
|
+
return payload.markdown;
|
|
701
|
+
}
|
|
702
|
+
function readArtifactPayload(artifacts, kind) {
|
|
703
|
+
const row = artifacts.find((artifact) => artifact.kind === kind);
|
|
704
|
+
if (row === undefined) {
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
return JSON.parse(row.payload_json);
|
|
708
|
+
}
|
|
709
|
+
function readArrayArtifactPayload(artifacts, kind) {
|
|
710
|
+
const payload = readArtifactPayload(artifacts, kind);
|
|
711
|
+
return Array.isArray(payload) ? payload : [];
|
|
712
|
+
}
|
|
713
|
+
function inferPdfPageCount(artifacts, locators) {
|
|
714
|
+
const diagnostics = readArtifactPayload(artifacts, "diagnostics");
|
|
715
|
+
const diagnosticPages = isRecord(diagnostics) ? diagnostics.pages : undefined;
|
|
716
|
+
const maxDiagnosticPage = Array.isArray(diagnosticPages) && diagnosticPages.length > 0
|
|
717
|
+
? Math.max(...diagnosticPages.map((page) => isRecord(page) && typeof page.pageIndex === "number"
|
|
718
|
+
? page.pageIndex + 1
|
|
719
|
+
: 0))
|
|
720
|
+
: 0;
|
|
721
|
+
const maxLocatorPage = Math.max(0, ...locators.map((locator) => locator.page));
|
|
722
|
+
return Math.max(maxDiagnosticPage, maxLocatorPage, 1);
|
|
723
|
+
}
|
|
724
|
+
function selectPdfLocator(input) {
|
|
725
|
+
if (input.page !== undefined && input.page > input.pageCount) {
|
|
726
|
+
throw new UserFacingError(`PDF page ${String(input.page)} is outside ${input.path}; page count is ${String(input.pageCount)}.`);
|
|
727
|
+
}
|
|
728
|
+
if (input.regionId !== undefined) {
|
|
729
|
+
const regionLocator = input.locators.find((locator) => locator.regionId === input.regionId);
|
|
730
|
+
if (regionLocator === undefined) {
|
|
731
|
+
throw new UserFacingError(`PDF region is not backed by the source map: ${input.regionId}`);
|
|
732
|
+
}
|
|
733
|
+
if (input.page !== undefined && regionLocator.page !== input.page) {
|
|
734
|
+
throw new UserFacingError(`PDF region ${input.regionId} is on page ${String(regionLocator.page)}, not page ${String(input.page)}.`);
|
|
735
|
+
}
|
|
736
|
+
return regionLocator;
|
|
737
|
+
}
|
|
738
|
+
const page = input.page ?? input.locators[0]?.page ?? 1;
|
|
739
|
+
return (input.locators.find((locator) => locator.page === page) ?? {
|
|
740
|
+
type: "pdf",
|
|
741
|
+
path: input.path,
|
|
742
|
+
page
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function countMarkdownLines(markdown) {
|
|
746
|
+
return markdown.replace(/\r\n?/gu, "\n").split("\n").length;
|
|
747
|
+
}
|
|
748
|
+
function clampLine(line, lineCount) {
|
|
749
|
+
return Math.min(Math.max(line, 1), Math.max(lineCount, 1));
|
|
750
|
+
}
|
|
751
|
+
function sliceMarkdownLines(markdown, startLine, endLine) {
|
|
752
|
+
const lines = markdown.replace(/\r\n?/gu, "\n").split("\n");
|
|
753
|
+
return lines.slice(startLine - 1, endLine).join("\n");
|
|
754
|
+
}
|
|
755
|
+
function filterSourceMapEntries(sourceMap, locator) {
|
|
756
|
+
const entries = isRecord(sourceMap) ? sourceMap.entries : undefined;
|
|
757
|
+
if (!Array.isArray(entries)) {
|
|
758
|
+
return [];
|
|
759
|
+
}
|
|
760
|
+
return entries.filter((entry) => {
|
|
761
|
+
if (!isRecord(entry) || !Array.isArray(entry.regions)) {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
return entry.regions.some((region) => isRecord(region) &&
|
|
765
|
+
typeof region.pageIndex === "number" &&
|
|
766
|
+
region.pageIndex + 1 === locator.page);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
function isRecord(value) {
|
|
770
|
+
return typeof value === "object" && value !== null;
|
|
771
|
+
}
|
|
772
|
+
function parseHeadingPath(headingPathJson) {
|
|
773
|
+
const parsed = JSON.parse(headingPathJson);
|
|
774
|
+
return Array.isArray(parsed) &&
|
|
775
|
+
parsed.every((item) => typeof item === "string")
|
|
776
|
+
? parsed
|
|
777
|
+
: [];
|
|
778
|
+
}
|
|
779
|
+
function requireNonEmptyString(value, fieldName) {
|
|
780
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
781
|
+
throw new UserFacingError(`${fieldName} must be a non-empty string.`);
|
|
782
|
+
}
|
|
783
|
+
return value;
|
|
784
|
+
}
|
|
785
|
+
function requirePositiveInteger(value, fieldName) {
|
|
786
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
787
|
+
throw new UserFacingError(`${fieldName} must be a positive integer.`);
|
|
788
|
+
}
|
|
789
|
+
return value;
|
|
790
|
+
}
|
|
791
|
+
function clampLimit(value, defaultValue, max, min = 1) {
|
|
792
|
+
if (value === undefined) {
|
|
793
|
+
return defaultValue;
|
|
794
|
+
}
|
|
795
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
796
|
+
throw new UserFacingError(`limit must be an integer between ${String(min)} and ${String(max)}.`);
|
|
797
|
+
}
|
|
798
|
+
return value;
|
|
799
|
+
}
|
|
800
|
+
function escapeLike(value) {
|
|
801
|
+
return value.replace(/[%_]/gu, (match) => `\\${match}`);
|
|
802
|
+
}
|
|
803
|
+
//# sourceMappingURL=tools.js.map
|