@functional-systems/srcx 0.1.0-dev.3 → 0.1.0-dev.30
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 +132 -1
- package/DESIGN.md +193 -58
- package/README.md +542 -38
- package/RELEASING.md +30 -9
- package/VALIDATION.md +834 -25
- package/dist/build.d.ts +81 -1
- package/dist/build.js +187 -37
- package/dist/build.js.map +1 -1
- package/dist/chunk.d.ts +9 -3
- package/dist/chunk.js +341 -14
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +103 -19
- package/dist/cli.js.map +1 -1
- package/dist/file-policy.d.ts +20 -0
- package/dist/file-policy.js +96 -0
- package/dist/file-policy.js.map +1 -0
- package/dist/implementation.d.ts +11 -0
- package/dist/implementation.js +191 -0
- package/dist/implementation.js.map +1 -0
- package/dist/publish.d.ts +18 -0
- package/dist/publish.js +158 -34
- package/dist/publish.js.map +1 -1
- package/dist/releases.js +21 -3
- package/dist/releases.js.map +1 -1
- package/dist/remote.js +38 -9
- package/dist/remote.js.map +1 -1
- package/dist/repository.d.ts +4 -1
- package/dist/repository.js +15 -11
- package/dist/repository.js.map +1 -1
- package/dist/rerank.d.ts +16 -0
- package/dist/rerank.js +87 -0
- package/dist/rerank.js.map +1 -0
- package/dist/search.d.ts +14 -4
- package/dist/search.js +119 -23
- package/dist/search.js.map +1 -1
- package/dist/skills.d.ts +24 -0
- package/dist/skills.js +155 -0
- package/dist/skills.js.map +1 -0
- package/package.json +16 -5
- package/runtime/grammars/README.md +50 -0
- package/runtime/grammars/tree-sitter-bash.LICENSE +21 -0
- package/runtime/grammars/tree-sitter-bash.wasm +0 -0
- package/runtime/grammars/tree-sitter-sql.LICENSE +21 -0
- package/runtime/grammars/tree-sitter-sql.wasm +0 -0
- package/runtime/grammars/tree-sitter-wasm.LICENSE +21 -0
- package/runtime/qwen.py +132 -0
- package/runtime/requirements.txt +34 -0
- package/skills/srcx-search/SKILL.md +85 -0
- package/src/build.ts +258 -47
- package/src/chunk.ts +360 -12
- package/src/cli.ts +212 -24
- package/src/file-policy.ts +157 -0
- package/src/implementation.ts +253 -0
- package/src/publish.ts +226 -34
- package/src/releases.ts +36 -3
- package/src/remote.ts +59 -13
- package/src/repository.ts +20 -11
- package/src/rerank.ts +131 -0
- package/src/search.ts +154 -28
- package/src/skills.ts +205 -0
package/src/remote.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
tagTarget,
|
|
8
8
|
type IndexConfigsUnion,
|
|
9
9
|
} from "@functional-systems/lambdadb";
|
|
10
|
-
import { invariant, type Doc } from "./common.js";
|
|
10
|
+
import { hash, invariant, type Doc } from "./common.js";
|
|
11
11
|
import type { Settings } from "./settings.js";
|
|
12
12
|
export type Ref = { kind: "branch" | "tag"; name: string };
|
|
13
13
|
export type Snapshot = { name: string; snapshotId: string };
|
|
@@ -96,6 +96,43 @@ export class LambdaRemote {
|
|
|
96
96
|
const c = this.client.collection(name);
|
|
97
97
|
const ref = (r: Ref) =>
|
|
98
98
|
r.kind === "tag" ? tagRef(r.name) : branchRef(r.name);
|
|
99
|
+
// Some deployments omit managed vectors from list/query responses despite
|
|
100
|
+
// includeVectors. Keep the same immutable Tag and exact non-vector payload.
|
|
101
|
+
const hydrate = async (r: Ref, docs: Doc[]): Promise<Doc[]> => {
|
|
102
|
+
const missing = docs.filter(
|
|
103
|
+
(d) => d.embeddingStatus === "managed" && d.embedding === undefined,
|
|
104
|
+
);
|
|
105
|
+
const hydrated = new Map<string, Doc>();
|
|
106
|
+
if (missing.length) {
|
|
107
|
+
invariant(
|
|
108
|
+
r.kind === "tag",
|
|
109
|
+
"Managed vector hydration requires an immutable Tag.",
|
|
110
|
+
);
|
|
111
|
+
const fetched = await request("fetch omitted vectors", () =>
|
|
112
|
+
c.docs.fetch({
|
|
113
|
+
ref: tagRef(r.name),
|
|
114
|
+
ids: missing.map((d) => d.id),
|
|
115
|
+
consistentRead: false,
|
|
116
|
+
includeVectors: true,
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
const expected = new Map(missing.map((d) => [d.id, hash(d)]));
|
|
120
|
+
for (const item of fetched.docs) {
|
|
121
|
+
const doc = item.doc as Doc;
|
|
122
|
+
const { embedding: _, ...source } = doc;
|
|
123
|
+
invariant(
|
|
124
|
+
!hydrated.has(doc.id) && expected.get(doc.id) === hash(source),
|
|
125
|
+
"Returned/fetched managed payloads disagree.",
|
|
126
|
+
);
|
|
127
|
+
hydrated.set(doc.id, doc);
|
|
128
|
+
}
|
|
129
|
+
invariant(
|
|
130
|
+
hydrated.size === expected.size,
|
|
131
|
+
"Returned managed records are missing from fetch.",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return docs.map((doc) => hydrated.get(doc.id) ?? doc);
|
|
135
|
+
};
|
|
99
136
|
return {
|
|
100
137
|
fetch: (r, ids, consistent = false) =>
|
|
101
138
|
request("fetch", async () => {
|
|
@@ -132,7 +169,11 @@ export class LambdaRemote {
|
|
|
132
169
|
pageToken: cursor,
|
|
133
170
|
}),
|
|
134
171
|
);
|
|
135
|
-
for (const
|
|
172
|
+
for (const doc of await hydrate(
|
|
173
|
+
r,
|
|
174
|
+
page.docs.map((d) => d.doc as Doc),
|
|
175
|
+
))
|
|
176
|
+
yield doc;
|
|
136
177
|
cursor = page.nextPageToken || undefined;
|
|
137
178
|
if (cursor) {
|
|
138
179
|
invariant(
|
|
@@ -199,17 +240,22 @@ export class LambdaRemote {
|
|
|
199
240
|
: c.aliases.create({ aliasName: name, target: tagTarget(target) }),
|
|
200
241
|
);
|
|
201
242
|
},
|
|
202
|
-
query: (
|
|
203
|
-
request("query",
|
|
204
|
-
(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
243
|
+
query: async (name, query, size) => {
|
|
244
|
+
const result = await request("query", () =>
|
|
245
|
+
c.query({
|
|
246
|
+
ref: tagRef(name),
|
|
247
|
+
consistentRead: false,
|
|
248
|
+
query,
|
|
249
|
+
size,
|
|
250
|
+
includeVectors: true,
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
const docs = await hydrate(
|
|
254
|
+
{ kind: "tag", name },
|
|
255
|
+
result.docs.map((h) => h.doc as Doc),
|
|
256
|
+
);
|
|
257
|
+
return result.docs.map((h, i) => ({ doc: docs[i]!, score: h.score }));
|
|
258
|
+
},
|
|
213
259
|
};
|
|
214
260
|
}
|
|
215
261
|
}
|
package/src/repository.ts
CHANGED
|
@@ -8,13 +8,14 @@ import {
|
|
|
8
8
|
PURPOSE,
|
|
9
9
|
type Doc,
|
|
10
10
|
} from "./common.js";
|
|
11
|
-
import { PRESET,
|
|
11
|
+
import { PRESET, indexConfigs, supportedPreset, type Preset } from "./build.js";
|
|
12
12
|
import { identity, type Identity } from "./git.js";
|
|
13
13
|
import { LambdaRemote, one, branch } from "./remote.js";
|
|
14
14
|
import { exclusive, type Binding } from "./publish.js";
|
|
15
15
|
import { stateRoot, type Settings } from "./settings.js";
|
|
16
16
|
import type { IndexConfigsUnion } from "@functional-systems/lambdadb";
|
|
17
17
|
export type Repository = Binding & {
|
|
18
|
+
preset?: Preset;
|
|
18
19
|
collection: string;
|
|
19
20
|
name: string;
|
|
20
21
|
description: string;
|
|
@@ -42,12 +43,15 @@ export function collectionName(
|
|
|
42
43
|
return `code-${slug}-${hash(["collection-v1", key, configHash]).slice(0, 16)}`;
|
|
43
44
|
}
|
|
44
45
|
/** LambdaDB adds its built-in keyword id index to collection metadata. */
|
|
45
|
-
export function matchesIndexSchema(
|
|
46
|
+
export function matchesIndexSchema(
|
|
47
|
+
actual: unknown,
|
|
48
|
+
preset: Preset = PRESET,
|
|
49
|
+
): boolean {
|
|
46
50
|
if (!actual || typeof actual !== "object" || Array.isArray(actual))
|
|
47
51
|
return false;
|
|
48
52
|
const { id, ...fields } = actual as Record<string, unknown>;
|
|
49
53
|
if (id !== undefined && hash(id) !== hash({ type: "keyword" })) return false;
|
|
50
|
-
return hash(fields) === hash(
|
|
54
|
+
return hash(fields) === hash(indexConfigs(preset));
|
|
51
55
|
}
|
|
52
56
|
function descriptor(r: Repository): Doc {
|
|
53
57
|
return {
|
|
@@ -60,7 +64,7 @@ function descriptor(r: Repository): Doc {
|
|
|
60
64
|
repoId: r.repoId,
|
|
61
65
|
indexId: r.indexId,
|
|
62
66
|
name: r.name,
|
|
63
|
-
preset: PRESET,
|
|
67
|
+
preset: r.preset ?? PRESET,
|
|
64
68
|
initialization: "ready",
|
|
65
69
|
};
|
|
66
70
|
}
|
|
@@ -76,8 +80,8 @@ function decode(
|
|
|
76
80
|
invariant(
|
|
77
81
|
doc?.role === "repository" &&
|
|
78
82
|
doc.initialization === "ready" &&
|
|
79
|
-
doc.
|
|
80
|
-
|
|
83
|
+
supportedPreset(doc.preset) &&
|
|
84
|
+
doc.configHash === hash(doc.preset),
|
|
81
85
|
"Repository is partially initialized or uses an unsupported preset.",
|
|
82
86
|
);
|
|
83
87
|
invariant(
|
|
@@ -87,7 +91,7 @@ function decode(
|
|
|
87
91
|
"Repository descriptor and Collection labels disagree.",
|
|
88
92
|
);
|
|
89
93
|
invariant(
|
|
90
|
-
matchesIndexSchema(c.indexConfigs),
|
|
94
|
+
matchesIndexSchema(c.indexConfigs, doc.preset as Preset),
|
|
91
95
|
"Collection schema differs from the pinned preset.",
|
|
92
96
|
);
|
|
93
97
|
invariant(
|
|
@@ -98,6 +102,7 @@ function decode(
|
|
|
98
102
|
"Malformed repository descriptor.",
|
|
99
103
|
);
|
|
100
104
|
return {
|
|
105
|
+
preset: doc.preset as Preset,
|
|
101
106
|
collection: c.collectionName,
|
|
102
107
|
repoKey: doc.repoKey,
|
|
103
108
|
repoId: doc.repoId,
|
|
@@ -148,7 +153,7 @@ export async function selectRepository(
|
|
|
148
153
|
invariant(
|
|
149
154
|
selected.length === 1,
|
|
150
155
|
selected.length
|
|
151
|
-
? "Ambiguous repository; use its
|
|
156
|
+
? "Ambiguous repository; use its exact Collection name (presets have separate Collections)."
|
|
152
157
|
: `Repository not found${partial.length ? "; partially initialized collections exist (see repo list)" : ""}.`,
|
|
153
158
|
);
|
|
154
159
|
return selected[0]!;
|
|
@@ -160,10 +165,13 @@ export async function register(
|
|
|
160
165
|
remote?: string;
|
|
161
166
|
description?: string;
|
|
162
167
|
labels?: Record<string, string>;
|
|
168
|
+
preset?: Preset;
|
|
163
169
|
},
|
|
164
170
|
): Promise<Repository> {
|
|
165
171
|
const source = await identity(args.path, args.remote);
|
|
166
|
-
const
|
|
172
|
+
const preset = args.preset ?? PRESET;
|
|
173
|
+
invariant(supportedPreset(preset), "Unsupported repository preset.");
|
|
174
|
+
const configHash = hash(preset),
|
|
167
175
|
collection = collectionName(source.key, source.name, configHash),
|
|
168
176
|
state = destination(remote.settings, collection);
|
|
169
177
|
return exclusive(state, async () => {
|
|
@@ -218,6 +226,7 @@ export async function register(
|
|
|
218
226
|
);
|
|
219
227
|
const indexId = randomUUID();
|
|
220
228
|
intent = {
|
|
229
|
+
preset,
|
|
221
230
|
repoKey: source.key,
|
|
222
231
|
name: source.name,
|
|
223
232
|
repoId: randomUUID(),
|
|
@@ -244,13 +253,13 @@ export async function register(
|
|
|
244
253
|
if (existing)
|
|
245
254
|
invariant(
|
|
246
255
|
hash(existing.tags) === hash(intent.tags) &&
|
|
247
|
-
matchesIndexSchema(existing.indexConfigs),
|
|
256
|
+
matchesIndexSchema(existing.indexConfigs, preset),
|
|
248
257
|
"Existing Collection differs from provisioning journal.",
|
|
249
258
|
);
|
|
250
259
|
else
|
|
251
260
|
await remote.create(
|
|
252
261
|
collection,
|
|
253
|
-
JSON.parse(JSON.stringify(
|
|
262
|
+
JSON.parse(JSON.stringify(indexConfigs(preset))) as Record<
|
|
254
263
|
string,
|
|
255
264
|
IndexConfigsUnion
|
|
256
265
|
>,
|
package/src/rerank.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { invariant } from "./common.js";
|
|
4
|
+
|
|
5
|
+
export const QWEN_MODEL = "Qwen/Qwen3-Reranker-0.6B";
|
|
6
|
+
export const QWEN_REVISION = "e61197ed45024b0ed8a2d74b80b4d909f1255473";
|
|
7
|
+
// Reserved worker exit codes; never interpret or forward library stderr.
|
|
8
|
+
const WORKER_FAILURES: Record<number, string> = {
|
|
9
|
+
20: "Qwen Python dependencies are missing or incompatible. Install runtime/requirements.txt with the Python selected by SRCX_RERANK_PYTHON.",
|
|
10
|
+
21: "Qwen model cache is missing or incomplete. Download the pinned model revision from README and check HF_HUB_CACHE.",
|
|
11
|
+
22: "Qwen device is invalid or unavailable. Set SRCX_RERANK_DEVICE to auto or cpu, or a supported mps/cuda device.",
|
|
12
|
+
23: "Qwen query/chunk pair exceeds 8,192 tokens. Shorten the query or use search without --rerank; source is not truncated.",
|
|
13
|
+
24: "Qwen request exceeds 4 MiB. Reduce --candidates or shorten the query.",
|
|
14
|
+
};
|
|
15
|
+
export type Candidate = { id: string; text: string };
|
|
16
|
+
export type RerankOptions = {
|
|
17
|
+
rerank?: "qwen";
|
|
18
|
+
candidates?: number;
|
|
19
|
+
onTiming?: (timing: Record<string, unknown>) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function candidateLimit(limit: number, options: RerankOptions): number {
|
|
23
|
+
invariant(
|
|
24
|
+
Number.isInteger(limit) && limit >= 1 && limit <= 100,
|
|
25
|
+
"Limit must be an integer from 1 to 100.",
|
|
26
|
+
);
|
|
27
|
+
invariant(
|
|
28
|
+
options.rerank === undefined || options.rerank === "qwen",
|
|
29
|
+
"Unknown reranker.",
|
|
30
|
+
);
|
|
31
|
+
invariant(
|
|
32
|
+
options.rerank || options.candidates === undefined,
|
|
33
|
+
"--candidates requires --rerank qwen.",
|
|
34
|
+
);
|
|
35
|
+
const count = options.rerank
|
|
36
|
+
? (options.candidates ?? Math.max(50, limit))
|
|
37
|
+
: limit;
|
|
38
|
+
invariant(
|
|
39
|
+
Number.isInteger(count) && count >= limit && count <= 100,
|
|
40
|
+
"Candidates must be an integer between --limit and 100.",
|
|
41
|
+
);
|
|
42
|
+
if (options.rerank)
|
|
43
|
+
invariant(
|
|
44
|
+
process.env.SRCX_RERANK_PYTHON,
|
|
45
|
+
"Set SRCX_RERANK_PYTHON to the prepared Python executable; see README reranker setup.",
|
|
46
|
+
);
|
|
47
|
+
return count;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function qwenScores(query: string, candidates: Candidate[]) {
|
|
51
|
+
invariant(
|
|
52
|
+
candidates.length > 0 &&
|
|
53
|
+
candidates.length <= 100 &&
|
|
54
|
+
new Set(candidates.map((c) => c.id)).size === candidates.length,
|
|
55
|
+
"Invalid reranker candidates.",
|
|
56
|
+
);
|
|
57
|
+
const input = JSON.stringify({ query, candidates });
|
|
58
|
+
invariant(
|
|
59
|
+
Buffer.byteLength(input) <= 4 * 1024 * 1024,
|
|
60
|
+
"Reranker input exceeds 4 MiB. Reduce --candidates or shorten the query; no fallback was applied.",
|
|
61
|
+
);
|
|
62
|
+
const python = process.env.SRCX_RERANK_PYTHON;
|
|
63
|
+
invariant(
|
|
64
|
+
python,
|
|
65
|
+
"Set SRCX_RERANK_PYTHON to the prepared Python executable.",
|
|
66
|
+
);
|
|
67
|
+
const start = performance.now();
|
|
68
|
+
const stdout = await new Promise<string>((resolve, reject) => {
|
|
69
|
+
const child = execFile(
|
|
70
|
+
python,
|
|
71
|
+
[fileURLToPath(new URL("../runtime/qwen.py", import.meta.url))],
|
|
72
|
+
{
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
timeout: 120_000,
|
|
75
|
+
killSignal: "SIGKILL",
|
|
76
|
+
maxBuffer: 1024 * 1024,
|
|
77
|
+
env: {
|
|
78
|
+
...process.env,
|
|
79
|
+
HF_HUB_OFFLINE: "1",
|
|
80
|
+
HF_HUB_DISABLE_TELEMETRY: "1",
|
|
81
|
+
TOKENIZERS_PARALLELISM: "false",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
(error, output) => {
|
|
85
|
+
if (!error) return resolve(output);
|
|
86
|
+
let message =
|
|
87
|
+
"Qwen reranking failed. Check Python dependencies, cached model and SRCX_RERANK_DEVICE.";
|
|
88
|
+
if (error.code === "ENOENT")
|
|
89
|
+
message =
|
|
90
|
+
"Qwen Python executable was not found. Set SRCX_RERANK_PYTHON to an existing Python executable.";
|
|
91
|
+
else if (error.code === "EACCES")
|
|
92
|
+
message =
|
|
93
|
+
"Qwen Python executable cannot be started. Check execute permissions for SRCX_RERANK_PYTHON.";
|
|
94
|
+
else if (error.killed && error.signal === "SIGKILL" && !error.code)
|
|
95
|
+
message =
|
|
96
|
+
"Qwen reranking exceeded 120 seconds. Reduce --candidates or use an available faster device via SRCX_RERANK_DEVICE.";
|
|
97
|
+
else if (typeof error.code === "number")
|
|
98
|
+
message = WORKER_FAILURES[error.code] ?? message;
|
|
99
|
+
reject(new Error(`${message} No fallback was applied.`));
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
// An early worker exit can close stdin before Node finishes writing.
|
|
103
|
+
child.stdin?.on("error", () => {});
|
|
104
|
+
child.stdin?.end(input);
|
|
105
|
+
});
|
|
106
|
+
let response;
|
|
107
|
+
try {
|
|
108
|
+
response = JSON.parse(stdout);
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error("Invalid Qwen response.");
|
|
111
|
+
}
|
|
112
|
+
invariant(
|
|
113
|
+
response?.model === QWEN_MODEL &&
|
|
114
|
+
response?.revision === QWEN_REVISION &&
|
|
115
|
+
Array.isArray(response.scores) &&
|
|
116
|
+
response.scores.length === candidates.length,
|
|
117
|
+
"Qwen response model or candidate count mismatch.",
|
|
118
|
+
);
|
|
119
|
+
const scores: number[] = response.scores.map(
|
|
120
|
+
(entry: { id?: unknown; score?: unknown }, index: number) => {
|
|
121
|
+
invariant(
|
|
122
|
+
entry?.id === candidates[index]!.id &&
|
|
123
|
+
typeof entry.score === "number" &&
|
|
124
|
+
Number.isFinite(entry.score),
|
|
125
|
+
"Qwen response candidate identity or score mismatch.",
|
|
126
|
+
);
|
|
127
|
+
return entry.score;
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
return { scores, elapsedMs: performance.now() - start };
|
|
131
|
+
}
|
package/src/search.ts
CHANGED
|
@@ -5,7 +5,15 @@ import { one, tag, type CollectionStore } from "./remote.js";
|
|
|
5
5
|
import { stateRoot, type Settings } from "./settings.js";
|
|
6
6
|
import type { Repository } from "./repository.js";
|
|
7
7
|
import type { Published } from "./publish.js";
|
|
8
|
-
import type
|
|
8
|
+
import { PRESET, recordHash, type InventoryItem } from "./build.js";
|
|
9
|
+
import { implementationSpan, type Implementation } from "./implementation.js";
|
|
10
|
+
import {
|
|
11
|
+
candidateLimit,
|
|
12
|
+
qwenScores,
|
|
13
|
+
QWEN_MODEL,
|
|
14
|
+
QWEN_REVISION,
|
|
15
|
+
type RerankOptions,
|
|
16
|
+
} from "./rerank.js";
|
|
9
17
|
export type Handle = {
|
|
10
18
|
id: string;
|
|
11
19
|
endpoint: string;
|
|
@@ -84,6 +92,33 @@ export function lexicalQuery(
|
|
|
84
92
|
],
|
|
85
93
|
};
|
|
86
94
|
}
|
|
95
|
+
export type SearchMode = "lexical" | "semantic" | "hybrid";
|
|
96
|
+
export function retrievalQuery(
|
|
97
|
+
query: string,
|
|
98
|
+
size: number,
|
|
99
|
+
filters: { path?: string; language?: string },
|
|
100
|
+
mode: SearchMode,
|
|
101
|
+
): Record<string, unknown> {
|
|
102
|
+
invariant(
|
|
103
|
+
["lexical", "semantic", "hybrid"].includes(mode),
|
|
104
|
+
"Unknown search mode.",
|
|
105
|
+
);
|
|
106
|
+
const lexical = lexicalQuery(query, filters);
|
|
107
|
+
if (mode === "lexical") return lexical;
|
|
108
|
+
const semantic = {
|
|
109
|
+
knn: {
|
|
110
|
+
field: "embedding",
|
|
111
|
+
queryText: query,
|
|
112
|
+
k: size,
|
|
113
|
+
filter: {
|
|
114
|
+
bool: (lexical.bool as { occur: string }[]).filter(
|
|
115
|
+
(c) => c.occur === "filter",
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
return mode === "semantic" ? semantic : { rrf: [lexical, semantic] };
|
|
121
|
+
}
|
|
87
122
|
export async function search(
|
|
88
123
|
store: CollectionStore,
|
|
89
124
|
s: Settings,
|
|
@@ -92,21 +127,28 @@ export async function search(
|
|
|
92
127
|
query: string,
|
|
93
128
|
size = 10,
|
|
94
129
|
filters: { path?: string; language?: string } = {},
|
|
130
|
+
mode: SearchMode = "lexical",
|
|
131
|
+
options: RerankOptions = {},
|
|
95
132
|
): Promise<unknown[]> {
|
|
133
|
+
const started = performance.now();
|
|
134
|
+
const count = candidateLimit(size, options);
|
|
96
135
|
invariant(
|
|
97
|
-
|
|
98
|
-
"
|
|
136
|
+
mode === "lexical" || r.preset?.embedding?.managed,
|
|
137
|
+
"Semantic/hybrid search requires a managed embedding Collection; register with --embedding text-embedding-3-small or text-embedding-3-large.",
|
|
99
138
|
);
|
|
139
|
+
const request = retrievalQuery(query, count, filters, mode);
|
|
100
140
|
const entries = await inventoryAt(store, r, v);
|
|
101
141
|
const files = new Map(
|
|
102
142
|
entries.filter((e) => e.status === "included").map((e) => [e.fileId, e]),
|
|
103
143
|
);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
144
|
+
const candidates = [];
|
|
145
|
+
const hits = await store.query(v.tagName, request, count);
|
|
146
|
+
invariant(
|
|
147
|
+
hits.length <= count &&
|
|
148
|
+
new Set(hits.map((h) => h.doc.id)).size === hits.length,
|
|
149
|
+
"Invalid search candidate count or duplicate IDs.",
|
|
150
|
+
);
|
|
151
|
+
for (const hit of hits) {
|
|
110
152
|
const d = hit.doc,
|
|
111
153
|
e = files.get(d.fileId as string);
|
|
112
154
|
invariant(
|
|
@@ -128,28 +170,78 @@ export async function search(
|
|
|
128
170
|
path: String(d.path),
|
|
129
171
|
contentHash: String(d.contentHash),
|
|
130
172
|
chunkId: d.id,
|
|
131
|
-
chunkHash:
|
|
173
|
+
chunkHash: recordHash(d, r.preset ?? PRESET),
|
|
132
174
|
startLine: Number(d.startLine),
|
|
133
175
|
endLine: Number(d.endLine),
|
|
134
176
|
};
|
|
135
177
|
// Check the exact source/ranges before persisting an evidence handle.
|
|
136
|
-
const evidence = await readHandle(store, s, handle
|
|
137
|
-
|
|
178
|
+
const evidence = await readHandle(store, s, handle, {
|
|
179
|
+
exactChunk: !!options.rerank,
|
|
180
|
+
});
|
|
181
|
+
candidates.push({
|
|
182
|
+
handle,
|
|
183
|
+
text: evidence.sourceText,
|
|
184
|
+
result: {
|
|
185
|
+
resultId: id,
|
|
186
|
+
repository: r.repoKey,
|
|
187
|
+
commitOid: v.commitOid,
|
|
188
|
+
version: v.tagName,
|
|
189
|
+
snapshotId: v.snapshotId,
|
|
190
|
+
path: d.path,
|
|
191
|
+
startLine: d.startLine,
|
|
192
|
+
endLine: d.endLine,
|
|
193
|
+
symbol: d.symbol,
|
|
194
|
+
score: hit.score,
|
|
195
|
+
excerpt: evidence.sourceText.slice(0, 600),
|
|
196
|
+
citation: evidence.citation,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const retrievalMs = performance.now() - started;
|
|
201
|
+
let rerankMs = 0;
|
|
202
|
+
let ranked = candidates.map((candidate, index) => ({
|
|
203
|
+
...candidate,
|
|
204
|
+
index,
|
|
205
|
+
rerankScore: 0,
|
|
206
|
+
}));
|
|
207
|
+
if (options.rerank && candidates.length) {
|
|
208
|
+
const response = await qwenScores(
|
|
209
|
+
query,
|
|
210
|
+
candidates.map((c) => ({ id: c.handle.chunkId!, text: c.text })),
|
|
211
|
+
);
|
|
212
|
+
rerankMs = response.elapsedMs;
|
|
213
|
+
ranked = ranked
|
|
214
|
+
.map((c, index) => ({ ...c, rerankScore: response.scores[index]! }))
|
|
215
|
+
.sort((a, b) => b.rerankScore - a.rerankScore || a.index - b.index);
|
|
216
|
+
}
|
|
217
|
+
if (options.rerank)
|
|
218
|
+
invariant(
|
|
219
|
+
(await store.tags()).find((t) => t.name === v.tagName)?.snapshotId ===
|
|
220
|
+
v.snapshotId,
|
|
221
|
+
"Pinned Tag was deleted or recreated during reranking.",
|
|
222
|
+
);
|
|
223
|
+
const result = [];
|
|
224
|
+
for (const c of ranked.slice(0, size)) {
|
|
225
|
+
await atomic(join(stateRoot(), "results", c.handle.id + ".json"), c.handle);
|
|
138
226
|
result.push({
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
symbol: d.symbol,
|
|
148
|
-
score: hit.score,
|
|
149
|
-
excerpt: evidence.sourceText.slice(0, 600),
|
|
150
|
-
citation: evidence.citation,
|
|
227
|
+
...c.result,
|
|
228
|
+
...(options.rerank
|
|
229
|
+
? {
|
|
230
|
+
rerankScore: c.rerankScore,
|
|
231
|
+
retrievalRank: c.index + 1,
|
|
232
|
+
reranker: { model: QWEN_MODEL, revision: QWEN_REVISION },
|
|
233
|
+
}
|
|
234
|
+
: {}),
|
|
151
235
|
});
|
|
152
236
|
}
|
|
237
|
+
if (options.rerank)
|
|
238
|
+
options.onTiming?.({
|
|
239
|
+
candidates: candidates.length,
|
|
240
|
+
returned: result.length,
|
|
241
|
+
retrievalMs,
|
|
242
|
+
rerankMs,
|
|
243
|
+
searchMs: performance.now() - started,
|
|
244
|
+
});
|
|
153
245
|
return result;
|
|
154
246
|
}
|
|
155
247
|
export async function loadHandle(id: string): Promise<Handle> {
|
|
@@ -191,8 +283,19 @@ export async function readHandle(
|
|
|
191
283
|
context?: number;
|
|
192
284
|
fullFile?: boolean;
|
|
193
285
|
lines?: [number, number];
|
|
286
|
+
exactChunk?: boolean;
|
|
287
|
+
implementation?: boolean;
|
|
194
288
|
} = {},
|
|
195
289
|
) {
|
|
290
|
+
invariant(
|
|
291
|
+
!options.implementation ||
|
|
292
|
+
(h.chunkId &&
|
|
293
|
+
!options.fullFile &&
|
|
294
|
+
!options.lines &&
|
|
295
|
+
!options.context &&
|
|
296
|
+
!options.exactChunk),
|
|
297
|
+
"Implementation reads require a result handle without range overrides.",
|
|
298
|
+
);
|
|
196
299
|
invariant(
|
|
197
300
|
h.endpoint === s.endpoint && h.project === s.project,
|
|
198
301
|
"Result belongs to another endpoint/project; restore that connection before reading.",
|
|
@@ -218,11 +321,13 @@ export async function readHandle(
|
|
|
218
321
|
raw.length === 0
|
|
219
322
|
? 0
|
|
220
323
|
: lineAt(raw, raw.length - (raw.at(-1) === 10 ? 1 : 0));
|
|
324
|
+
let chunkSource: string | undefined;
|
|
325
|
+
let implementation: Implementation | undefined;
|
|
221
326
|
if (h.chunkId) {
|
|
222
327
|
const d = await one(store, tag(v.tagName), h.chunkId);
|
|
223
328
|
invariant(
|
|
224
329
|
d &&
|
|
225
|
-
|
|
330
|
+
recordHash(d, h.repository.preset ?? PRESET) === h.chunkHash &&
|
|
226
331
|
d.fileId === h.fileId &&
|
|
227
332
|
d.contentHash === h.contentHash,
|
|
228
333
|
"Pinned chunk has changed or is missing.",
|
|
@@ -239,6 +344,12 @@ export async function readHandle(
|
|
|
239
344
|
lineAt(raw, z - 1) === h.endLine,
|
|
240
345
|
"Invalid source range.",
|
|
241
346
|
);
|
|
347
|
+
chunkSource = raw.subarray(a, z).toString("utf8");
|
|
348
|
+
if (options.implementation)
|
|
349
|
+
implementation = await implementationSpan(file.sourceText, h.path, {
|
|
350
|
+
startByte: a,
|
|
351
|
+
endByte: z,
|
|
352
|
+
});
|
|
242
353
|
}
|
|
243
354
|
const context = options.context ?? 0;
|
|
244
355
|
invariant(
|
|
@@ -247,6 +358,10 @@ export async function readHandle(
|
|
|
247
358
|
);
|
|
248
359
|
let start = options.lines?.[0] ?? h.startLine,
|
|
249
360
|
end = options.lines?.[1] ?? h.endLine;
|
|
361
|
+
if (implementation?.status === "resolved") {
|
|
362
|
+
start = implementation.startLine!;
|
|
363
|
+
end = implementation.endLine!;
|
|
364
|
+
}
|
|
250
365
|
if (options.fullFile) {
|
|
251
366
|
start = 1;
|
|
252
367
|
end = total;
|
|
@@ -264,9 +379,19 @@ export async function readHandle(
|
|
|
264
379
|
end = Math.min(total, end + context);
|
|
265
380
|
}
|
|
266
381
|
const lines = file.sourceText.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
382
|
+
invariant(
|
|
383
|
+
!options.exactChunk ||
|
|
384
|
+
(chunkSource !== undefined &&
|
|
385
|
+
!options.fullFile &&
|
|
386
|
+
!options.lines &&
|
|
387
|
+
!options.context),
|
|
388
|
+
"Exact chunk reads require a chunk handle without range overrides.",
|
|
389
|
+
);
|
|
390
|
+
const sourceText = options.exactChunk
|
|
391
|
+
? chunkSource!
|
|
392
|
+
: options.fullFile
|
|
393
|
+
? file.sourceText
|
|
394
|
+
: lines.slice(start - 1, end).join("");
|
|
270
395
|
return {
|
|
271
396
|
repository: h.repository.repoKey,
|
|
272
397
|
commitOid: v.commitOid,
|
|
@@ -276,6 +401,7 @@ export async function readHandle(
|
|
|
276
401
|
startLine: start,
|
|
277
402
|
endLine: end,
|
|
278
403
|
contentHash: h.contentHash,
|
|
404
|
+
...(implementation ? { implementation } : {}),
|
|
279
405
|
sourceText,
|
|
280
406
|
citation: `${h.repository.repoKey}@${v.commitOid}:${h.path}:${start}-${end}`,
|
|
281
407
|
};
|