@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/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
- invariant(
125
- Number.isInteger(size) && size > 0 && size <= 100,
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, size, filters, mode);
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 result = [];
138
- for (const hit of await store.query(v.tagName, request, size)) {
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
- await atomic(join(stateRoot(), "results", id + ".json"), handle);
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
- resultId: id,
169
- repository: r.repoKey,
170
- commitOid: v.commitOid,
171
- version: v.tagName,
172
- snapshotId: v.snapshotId,
173
- path: d.path,
174
- startLine: d.startLine,
175
- endLine: d.endLine,
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
- const sourceText = options.fullFile
297
- ? file.sourceText
298
- : lines.slice(start - 1, end).join("");
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,