@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.
Files changed (201) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +147 -0
  3. package/dist/agent/ask.d.ts +7 -0
  4. package/dist/agent/ask.d.ts.map +1 -0
  5. package/dist/agent/ask.js +35 -0
  6. package/dist/agent/ask.js.map +1 -0
  7. package/dist/agent/citations.d.ts +27 -0
  8. package/dist/agent/citations.d.ts.map +1 -0
  9. package/dist/agent/citations.js +190 -0
  10. package/dist/agent/citations.js.map +1 -0
  11. package/dist/agent/fake-provider.d.ts +9 -0
  12. package/dist/agent/fake-provider.d.ts.map +1 -0
  13. package/dist/agent/fake-provider.js +51 -0
  14. package/dist/agent/fake-provider.js.map +1 -0
  15. package/dist/agent/index.d.ts +10 -0
  16. package/dist/agent/index.d.ts.map +1 -0
  17. package/dist/agent/index.js +6 -0
  18. package/dist/agent/index.js.map +1 -0
  19. package/dist/agent/loop.d.ts +11 -0
  20. package/dist/agent/loop.d.ts.map +1 -0
  21. package/dist/agent/loop.js +156 -0
  22. package/dist/agent/loop.js.map +1 -0
  23. package/dist/agent/providers.d.ts +17 -0
  24. package/dist/agent/providers.d.ts.map +1 -0
  25. package/dist/agent/providers.js +171 -0
  26. package/dist/agent/providers.js.map +1 -0
  27. package/dist/agent/tools.d.ts +8 -0
  28. package/dist/agent/tools.d.ts.map +1 -0
  29. package/dist/agent/tools.js +233 -0
  30. package/dist/agent/tools.js.map +1 -0
  31. package/dist/agent/types.d.ts +84 -0
  32. package/dist/agent/types.d.ts.map +1 -0
  33. package/dist/agent/types.js +2 -0
  34. package/dist/agent/types.js.map +1 -0
  35. package/dist/cli/help.d.ts +2 -0
  36. package/dist/cli/help.d.ts.map +1 -0
  37. package/dist/cli/help.js +28 -0
  38. package/dist/cli/help.js.map +1 -0
  39. package/dist/cli/main.d.ts +3 -0
  40. package/dist/cli/main.d.ts.map +1 -0
  41. package/dist/cli/main.js +5 -0
  42. package/dist/cli/main.js.map +1 -0
  43. package/dist/cli/options.d.ts +21 -0
  44. package/dist/cli/options.d.ts.map +1 -0
  45. package/dist/cli/options.js +97 -0
  46. package/dist/cli/options.js.map +1 -0
  47. package/dist/cli/run.d.ts +21 -0
  48. package/dist/cli/run.d.ts.map +1 -0
  49. package/dist/cli/run.js +144 -0
  50. package/dist/cli/run.js.map +1 -0
  51. package/dist/config/index.d.ts +4 -0
  52. package/dist/config/index.d.ts.map +1 -0
  53. package/dist/config/index.js +3 -0
  54. package/dist/config/index.js.map +1 -0
  55. package/dist/config/loader.d.ts +14 -0
  56. package/dist/config/loader.d.ts.map +1 -0
  57. package/dist/config/loader.js +352 -0
  58. package/dist/config/loader.js.map +1 -0
  59. package/dist/config/schema.d.ts +6 -0
  60. package/dist/config/schema.d.ts.map +1 -0
  61. package/dist/config/schema.js +72 -0
  62. package/dist/config/schema.js.map +1 -0
  63. package/dist/config/types.d.ts +53 -0
  64. package/dist/config/types.d.ts.map +1 -0
  65. package/dist/config/types.js +2 -0
  66. package/dist/config/types.js.map +1 -0
  67. package/dist/doctor/doctor.d.ts +47 -0
  68. package/dist/doctor/doctor.d.ts.map +1 -0
  69. package/dist/doctor/doctor.js +159 -0
  70. package/dist/doctor/doctor.js.map +1 -0
  71. package/dist/embeddings/index.d.ts +117 -0
  72. package/dist/embeddings/index.d.ts.map +1 -0
  73. package/dist/embeddings/index.js +515 -0
  74. package/dist/embeddings/index.js.map +1 -0
  75. package/dist/errors.d.ts +19 -0
  76. package/dist/errors.d.ts.map +1 -0
  77. package/dist/errors.js +37 -0
  78. package/dist/errors.js.map +1 -0
  79. package/dist/index.d.ts +17 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +9 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/indexer/discovery.d.ts +10 -0
  84. package/dist/indexer/discovery.d.ts.map +1 -0
  85. package/dist/indexer/discovery.js +38 -0
  86. package/dist/indexer/discovery.js.map +1 -0
  87. package/dist/indexer/hash.d.ts +3 -0
  88. package/dist/indexer/hash.d.ts.map +1 -0
  89. package/dist/indexer/hash.js +11 -0
  90. package/dist/indexer/hash.js.map +1 -0
  91. package/dist/indexer/index.d.ts +7 -0
  92. package/dist/indexer/index.d.ts.map +1 -0
  93. package/dist/indexer/index.js +5 -0
  94. package/dist/indexer/index.js.map +1 -0
  95. package/dist/indexer/kinds.d.ts +4 -0
  96. package/dist/indexer/kinds.d.ts.map +1 -0
  97. package/dist/indexer/kinds.js +38 -0
  98. package/dist/indexer/kinds.js.map +1 -0
  99. package/dist/indexer/run.d.ts +42 -0
  100. package/dist/indexer/run.d.ts.map +1 -0
  101. package/dist/indexer/run.js +563 -0
  102. package/dist/indexer/run.js.map +1 -0
  103. package/dist/logging/logger.d.ts +11 -0
  104. package/dist/logging/logger.d.ts.map +1 -0
  105. package/dist/logging/logger.js +18 -0
  106. package/dist/logging/logger.js.map +1 -0
  107. package/dist/mcp/index.d.ts +35 -0
  108. package/dist/mcp/index.d.ts.map +1 -0
  109. package/dist/mcp/index.js +392 -0
  110. package/dist/mcp/index.js.map +1 -0
  111. package/dist/parsers/chunking.d.ts +4 -0
  112. package/dist/parsers/chunking.d.ts.map +1 -0
  113. package/dist/parsers/chunking.js +112 -0
  114. package/dist/parsers/chunking.js.map +1 -0
  115. package/dist/parsers/index.d.ts +8 -0
  116. package/dist/parsers/index.d.ts.map +1 -0
  117. package/dist/parsers/index.js +5 -0
  118. package/dist/parsers/index.js.map +1 -0
  119. package/dist/parsers/line-parser.d.ts +13 -0
  120. package/dist/parsers/line-parser.d.ts.map +1 -0
  121. package/dist/parsers/line-parser.js +242 -0
  122. package/dist/parsers/line-parser.js.map +1 -0
  123. package/dist/parsers/normalize.d.ts +4 -0
  124. package/dist/parsers/normalize.d.ts.map +1 -0
  125. package/dist/parsers/normalize.js +17 -0
  126. package/dist/parsers/normalize.js.map +1 -0
  127. package/dist/parsers/parser.d.ts +14 -0
  128. package/dist/parsers/parser.d.ts.map +1 -0
  129. package/dist/parsers/parser.js +77 -0
  130. package/dist/parsers/parser.js.map +1 -0
  131. package/dist/parsers/pdf2llm.d.ts +14 -0
  132. package/dist/parsers/pdf2llm.d.ts.map +1 -0
  133. package/dist/parsers/pdf2llm.js +215 -0
  134. package/dist/parsers/pdf2llm.js.map +1 -0
  135. package/dist/parsers/types.d.ts +48 -0
  136. package/dist/parsers/types.d.ts.map +1 -0
  137. package/dist/parsers/types.js +2 -0
  138. package/dist/parsers/types.js.map +1 -0
  139. package/dist/retrieval/index.d.ts +4 -0
  140. package/dist/retrieval/index.d.ts.map +1 -0
  141. package/dist/retrieval/index.js +3 -0
  142. package/dist/retrieval/index.js.map +1 -0
  143. package/dist/retrieval/schemas.d.ts +127 -0
  144. package/dist/retrieval/schemas.d.ts.map +1 -0
  145. package/dist/retrieval/schemas.js +65 -0
  146. package/dist/retrieval/schemas.js.map +1 -0
  147. package/dist/retrieval/tools.d.ts +31 -0
  148. package/dist/retrieval/tools.d.ts.map +1 -0
  149. package/dist/retrieval/tools.js +803 -0
  150. package/dist/retrieval/tools.js.map +1 -0
  151. package/dist/retrieval/types.d.ts +104 -0
  152. package/dist/retrieval/types.d.ts.map +1 -0
  153. package/dist/retrieval/types.js +2 -0
  154. package/dist/retrieval/types.js.map +1 -0
  155. package/dist/server/index.d.ts +3 -0
  156. package/dist/server/index.d.ts.map +1 -0
  157. package/dist/server/index.js +2 -0
  158. package/dist/server/index.js.map +1 -0
  159. package/dist/server/server.d.ts +49 -0
  160. package/dist/server/server.d.ts.map +1 -0
  161. package/dist/server/server.js +517 -0
  162. package/dist/server/server.js.map +1 -0
  163. package/dist/shared/types.d.ts +98 -0
  164. package/dist/shared/types.d.ts.map +1 -0
  165. package/dist/shared/types.js +2 -0
  166. package/dist/shared/types.js.map +1 -0
  167. package/dist/storage/database.d.ts +18 -0
  168. package/dist/storage/database.d.ts.map +1 -0
  169. package/dist/storage/database.js +66 -0
  170. package/dist/storage/database.js.map +1 -0
  171. package/dist/storage/index.d.ts +8 -0
  172. package/dist/storage/index.d.ts.map +1 -0
  173. package/dist/storage/index.js +5 -0
  174. package/dist/storage/index.js.map +1 -0
  175. package/dist/storage/migrations.d.ts +15 -0
  176. package/dist/storage/migrations.d.ts.map +1 -0
  177. package/dist/storage/migrations.js +170 -0
  178. package/dist/storage/migrations.js.map +1 -0
  179. package/dist/storage/paths.d.ts +15 -0
  180. package/dist/storage/paths.d.ts.map +1 -0
  181. package/dist/storage/paths.js +27 -0
  182. package/dist/storage/paths.js.map +1 -0
  183. package/dist/storage/schema.d.ts +11 -0
  184. package/dist/storage/schema.d.ts.map +1 -0
  185. package/dist/storage/schema.js +40 -0
  186. package/dist/storage/schema.js.map +1 -0
  187. package/dist/ui/index.d.ts +2 -0
  188. package/dist/ui/index.d.ts.map +1 -0
  189. package/dist/ui/index.js +2 -0
  190. package/dist/ui/index.js.map +1 -0
  191. package/dist/ui/static-app.d.ts +2 -0
  192. package/dist/ui/static-app.d.ts.map +1 -0
  193. package/dist/ui/static-app.js +735 -0
  194. package/dist/ui/static-app.js.map +1 -0
  195. package/docs/adr/0001-sqlite-driver.md +23 -0
  196. package/docs/agentic-rag-specifications.md +659 -0
  197. package/docs/agentic-rag-study.md +1056 -0
  198. package/docs/implementation-plan.md +1425 -0
  199. package/docs/mcp-integration.md +154 -0
  200. package/docs/validation-corpus.md +71 -0
  201. 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