@functional-systems/srcx 0.1.0-dev.16 → 0.1.0-dev.18
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/CHANGELOG.md +6 -0
- package/DESIGN.md +36 -22
- package/README.md +82 -21
- package/RELEASING.md +15 -3
- package/VALIDATION.md +126 -13
- package/dist/cli.js +14 -1
- package/dist/cli.js.map +1 -1
- package/dist/rerank.d.ts +16 -0
- package/dist/rerank.js +68 -0
- package/dist/rerank.js.map +1 -0
- package/dist/search.d.ts +3 -1
- package/dist/search.js +80 -22
- package/dist/search.js.map +1 -1
- package/package.json +4 -2
- package/runtime/qwen.py +97 -0
- package/runtime/requirements.txt +34 -0
- package/src/cli.ts +26 -0
- package/src/rerank.ts +115 -0
- package/src/search.ts +99 -24
package/src/search.ts
CHANGED
|
@@ -6,6 +6,13 @@ import { stateRoot, type Settings } from "./settings.js";
|
|
|
6
6
|
import type { Repository } from "./repository.js";
|
|
7
7
|
import type { Published } from "./publish.js";
|
|
8
8
|
import { PRESET, recordHash, type InventoryItem } from "./build.js";
|
|
9
|
+
import {
|
|
10
|
+
candidateLimit,
|
|
11
|
+
qwenScores,
|
|
12
|
+
QWEN_MODEL,
|
|
13
|
+
QWEN_REVISION,
|
|
14
|
+
type RerankOptions,
|
|
15
|
+
} from "./rerank.js";
|
|
9
16
|
export type Handle = {
|
|
10
17
|
id: string;
|
|
11
18
|
endpoint: string;
|
|
@@ -120,22 +127,27 @@ export async function search(
|
|
|
120
127
|
size = 10,
|
|
121
128
|
filters: { path?: string; language?: string } = {},
|
|
122
129
|
mode: SearchMode = "lexical",
|
|
130
|
+
options: RerankOptions = {},
|
|
123
131
|
): Promise<unknown[]> {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"Limit must be an integer from 1 to 100.",
|
|
127
|
-
);
|
|
132
|
+
const started = performance.now();
|
|
133
|
+
const count = candidateLimit(size, options);
|
|
128
134
|
invariant(
|
|
129
135
|
mode === "lexical" || r.preset?.embedding?.managed,
|
|
130
136
|
"Semantic/hybrid search requires a managed embedding Collection; register with --embedding text-embedding-3-small or text-embedding-3-large.",
|
|
131
137
|
);
|
|
132
|
-
const request = retrievalQuery(query,
|
|
138
|
+
const request = retrievalQuery(query, count, filters, mode);
|
|
133
139
|
const entries = await inventoryAt(store, r, v);
|
|
134
140
|
const files = new Map(
|
|
135
141
|
entries.filter((e) => e.status === "included").map((e) => [e.fileId, e]),
|
|
136
142
|
);
|
|
137
|
-
const
|
|
138
|
-
|
|
143
|
+
const candidates = [];
|
|
144
|
+
const hits = await store.query(v.tagName, request, count);
|
|
145
|
+
invariant(
|
|
146
|
+
hits.length <= count &&
|
|
147
|
+
new Set(hits.map((h) => h.doc.id)).size === hits.length,
|
|
148
|
+
"Invalid search candidate count or duplicate IDs.",
|
|
149
|
+
);
|
|
150
|
+
for (const hit of hits) {
|
|
139
151
|
const d = hit.doc,
|
|
140
152
|
e = files.get(d.fileId as string);
|
|
141
153
|
invariant(
|
|
@@ -162,23 +174,73 @@ export async function search(
|
|
|
162
174
|
endLine: Number(d.endLine),
|
|
163
175
|
};
|
|
164
176
|
// Check the exact source/ranges before persisting an evidence handle.
|
|
165
|
-
const evidence = await readHandle(store, s, handle
|
|
166
|
-
|
|
177
|
+
const evidence = await readHandle(store, s, handle, {
|
|
178
|
+
exactChunk: !!options.rerank,
|
|
179
|
+
});
|
|
180
|
+
candidates.push({
|
|
181
|
+
handle,
|
|
182
|
+
text: evidence.sourceText,
|
|
183
|
+
result: {
|
|
184
|
+
resultId: id,
|
|
185
|
+
repository: r.repoKey,
|
|
186
|
+
commitOid: v.commitOid,
|
|
187
|
+
version: v.tagName,
|
|
188
|
+
snapshotId: v.snapshotId,
|
|
189
|
+
path: d.path,
|
|
190
|
+
startLine: d.startLine,
|
|
191
|
+
endLine: d.endLine,
|
|
192
|
+
symbol: d.symbol,
|
|
193
|
+
score: hit.score,
|
|
194
|
+
excerpt: evidence.sourceText.slice(0, 600),
|
|
195
|
+
citation: evidence.citation,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const retrievalMs = performance.now() - started;
|
|
200
|
+
let rerankMs = 0;
|
|
201
|
+
let ranked = candidates.map((candidate, index) => ({
|
|
202
|
+
...candidate,
|
|
203
|
+
index,
|
|
204
|
+
rerankScore: 0,
|
|
205
|
+
}));
|
|
206
|
+
if (options.rerank && candidates.length) {
|
|
207
|
+
const response = await qwenScores(
|
|
208
|
+
query,
|
|
209
|
+
candidates.map((c) => ({ id: c.handle.chunkId!, text: c.text })),
|
|
210
|
+
);
|
|
211
|
+
rerankMs = response.elapsedMs;
|
|
212
|
+
ranked = ranked
|
|
213
|
+
.map((c, index) => ({ ...c, rerankScore: response.scores[index]! }))
|
|
214
|
+
.sort((a, b) => b.rerankScore - a.rerankScore || a.index - b.index);
|
|
215
|
+
}
|
|
216
|
+
if (options.rerank)
|
|
217
|
+
invariant(
|
|
218
|
+
(await store.tags()).find((t) => t.name === v.tagName)?.snapshotId ===
|
|
219
|
+
v.snapshotId,
|
|
220
|
+
"Pinned Tag was deleted or recreated during reranking.",
|
|
221
|
+
);
|
|
222
|
+
const result = [];
|
|
223
|
+
for (const c of ranked.slice(0, size)) {
|
|
224
|
+
await atomic(join(stateRoot(), "results", c.handle.id + ".json"), c.handle);
|
|
167
225
|
result.push({
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
symbol: d.symbol,
|
|
177
|
-
score: hit.score,
|
|
178
|
-
excerpt: evidence.sourceText.slice(0, 600),
|
|
179
|
-
citation: evidence.citation,
|
|
226
|
+
...c.result,
|
|
227
|
+
...(options.rerank
|
|
228
|
+
? {
|
|
229
|
+
rerankScore: c.rerankScore,
|
|
230
|
+
retrievalRank: c.index + 1,
|
|
231
|
+
reranker: { model: QWEN_MODEL, revision: QWEN_REVISION },
|
|
232
|
+
}
|
|
233
|
+
: {}),
|
|
180
234
|
});
|
|
181
235
|
}
|
|
236
|
+
if (options.rerank)
|
|
237
|
+
options.onTiming?.({
|
|
238
|
+
candidates: candidates.length,
|
|
239
|
+
returned: result.length,
|
|
240
|
+
retrievalMs,
|
|
241
|
+
rerankMs,
|
|
242
|
+
searchMs: performance.now() - started,
|
|
243
|
+
});
|
|
182
244
|
return result;
|
|
183
245
|
}
|
|
184
246
|
export async function loadHandle(id: string): Promise<Handle> {
|
|
@@ -220,6 +282,7 @@ export async function readHandle(
|
|
|
220
282
|
context?: number;
|
|
221
283
|
fullFile?: boolean;
|
|
222
284
|
lines?: [number, number];
|
|
285
|
+
exactChunk?: boolean;
|
|
223
286
|
} = {},
|
|
224
287
|
) {
|
|
225
288
|
invariant(
|
|
@@ -247,6 +310,7 @@ export async function readHandle(
|
|
|
247
310
|
raw.length === 0
|
|
248
311
|
? 0
|
|
249
312
|
: lineAt(raw, raw.length - (raw.at(-1) === 10 ? 1 : 0));
|
|
313
|
+
let chunkSource: string | undefined;
|
|
250
314
|
if (h.chunkId) {
|
|
251
315
|
const d = await one(store, tag(v.tagName), h.chunkId);
|
|
252
316
|
invariant(
|
|
@@ -268,6 +332,7 @@ export async function readHandle(
|
|
|
268
332
|
lineAt(raw, z - 1) === h.endLine,
|
|
269
333
|
"Invalid source range.",
|
|
270
334
|
);
|
|
335
|
+
chunkSource = raw.subarray(a, z).toString("utf8");
|
|
271
336
|
}
|
|
272
337
|
const context = options.context ?? 0;
|
|
273
338
|
invariant(
|
|
@@ -293,9 +358,19 @@ export async function readHandle(
|
|
|
293
358
|
end = Math.min(total, end + context);
|
|
294
359
|
}
|
|
295
360
|
const lines = file.sourceText.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
361
|
+
invariant(
|
|
362
|
+
!options.exactChunk ||
|
|
363
|
+
(chunkSource !== undefined &&
|
|
364
|
+
!options.fullFile &&
|
|
365
|
+
!options.lines &&
|
|
366
|
+
!options.context),
|
|
367
|
+
"Exact chunk reads require a chunk handle without range overrides.",
|
|
368
|
+
);
|
|
369
|
+
const sourceText = options.exactChunk
|
|
370
|
+
? chunkSource!
|
|
371
|
+
: options.fullFile
|
|
372
|
+
? file.sourceText
|
|
373
|
+
: lines.slice(start - 1, end).join("");
|
|
299
374
|
return {
|
|
300
375
|
repository: h.repository.repoKey,
|
|
301
376
|
commitOid: v.commitOid,
|