@functional-systems/srcx 0.1.0-dev.4 → 0.1.0-dev.40
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 +179 -0
- package/CONTRIBUTING.md +4 -0
- package/DESIGN.md +378 -58
- package/README.md +628 -35
- package/RELEASING.md +15 -3
- package/VALIDATION.md +1120 -16
- package/dist/artifact.d.ts +31 -0
- package/dist/artifact.js +237 -0
- package/dist/artifact.js.map +1 -0
- package/dist/build.d.ts +86 -3
- package/dist/build.js +233 -71
- 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 +130 -21
- package/dist/cli.js.map +1 -1
- package/dist/file-policy.d.ts +20 -0
- package/dist/file-policy.js +103 -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 +224 -46
- package/dist/publish.js.map +1 -1
- package/dist/releases.js +21 -3
- package/dist/releases.js.map +1 -1
- package/dist/remote.d.ts +1 -0
- package/dist/remote.js +50 -9
- package/dist/remote.js.map +1 -1
- package/dist/repository.d.ts +4 -1
- package/dist/repository.js +26 -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 +18 -3
- package/dist/search.js +143 -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 +17 -6
- 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 +103 -0
- package/src/artifact.ts +303 -0
- package/src/build.ts +313 -84
- package/src/chunk.ts +360 -12
- package/src/cli.ts +263 -23
- package/src/file-policy.ts +166 -0
- package/src/implementation.ts +253 -0
- package/src/publish.ts +321 -47
- package/src/releases.ts +36 -3
- package/src/remote.ts +70 -13
- package/src/repository.ts +40 -11
- package/src/rerank.ts +131 -0
- package/src/search.ts +229 -28
- package/src/skills.ts +205 -0
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,
|
|
@@ -137,6 +142,26 @@ export async function selectRepository(
|
|
|
137
142
|
remote: LambdaRemote,
|
|
138
143
|
selector: string,
|
|
139
144
|
): Promise<Repository> {
|
|
145
|
+
// Names emitted by collectionName are explicit destinations. Avoid reading
|
|
146
|
+
// every repository descriptor just to select one Collection. A missing name
|
|
147
|
+
// can still be a Git repository alias with this spelling.
|
|
148
|
+
if (/^code-[a-z0-9-]{1,24}-[a-f0-9]{16}$/.test(selector)) {
|
|
149
|
+
const c = await remote.collection(selector);
|
|
150
|
+
if (c) {
|
|
151
|
+
invariant(
|
|
152
|
+
c.collectionName === selector,
|
|
153
|
+
"Collection lookup returned a different name.",
|
|
154
|
+
);
|
|
155
|
+
invariant(
|
|
156
|
+
c.tags.purpose === PURPOSE,
|
|
157
|
+
"Collection is not a srcx repository.",
|
|
158
|
+
);
|
|
159
|
+
return decode(
|
|
160
|
+
await one(remote.store(selector), branch("main"), "__repo__", true),
|
|
161
|
+
c,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
140
165
|
const { repositories, partial } = await discover(remote);
|
|
141
166
|
const selected = repositories.filter(
|
|
142
167
|
(r) =>
|
|
@@ -148,7 +173,7 @@ export async function selectRepository(
|
|
|
148
173
|
invariant(
|
|
149
174
|
selected.length === 1,
|
|
150
175
|
selected.length
|
|
151
|
-
? "Ambiguous repository; use its
|
|
176
|
+
? "Ambiguous repository; use its exact Collection name (presets have separate Collections)."
|
|
152
177
|
: `Repository not found${partial.length ? "; partially initialized collections exist (see repo list)" : ""}.`,
|
|
153
178
|
);
|
|
154
179
|
return selected[0]!;
|
|
@@ -160,10 +185,13 @@ export async function register(
|
|
|
160
185
|
remote?: string;
|
|
161
186
|
description?: string;
|
|
162
187
|
labels?: Record<string, string>;
|
|
188
|
+
preset?: Preset;
|
|
163
189
|
},
|
|
164
190
|
): Promise<Repository> {
|
|
165
191
|
const source = await identity(args.path, args.remote);
|
|
166
|
-
const
|
|
192
|
+
const preset = args.preset ?? PRESET;
|
|
193
|
+
invariant(supportedPreset(preset), "Unsupported repository preset.");
|
|
194
|
+
const configHash = hash(preset),
|
|
167
195
|
collection = collectionName(source.key, source.name, configHash),
|
|
168
196
|
state = destination(remote.settings, collection);
|
|
169
197
|
return exclusive(state, async () => {
|
|
@@ -218,6 +246,7 @@ export async function register(
|
|
|
218
246
|
);
|
|
219
247
|
const indexId = randomUUID();
|
|
220
248
|
intent = {
|
|
249
|
+
preset,
|
|
221
250
|
repoKey: source.key,
|
|
222
251
|
name: source.name,
|
|
223
252
|
repoId: randomUUID(),
|
|
@@ -244,13 +273,13 @@ export async function register(
|
|
|
244
273
|
if (existing)
|
|
245
274
|
invariant(
|
|
246
275
|
hash(existing.tags) === hash(intent.tags) &&
|
|
247
|
-
matchesIndexSchema(existing.indexConfigs),
|
|
276
|
+
matchesIndexSchema(existing.indexConfigs, preset),
|
|
248
277
|
"Existing Collection differs from provisioning journal.",
|
|
249
278
|
);
|
|
250
279
|
else
|
|
251
280
|
await remote.create(
|
|
252
281
|
collection,
|
|
253
|
-
JSON.parse(JSON.stringify(
|
|
282
|
+
JSON.parse(JSON.stringify(indexConfigs(preset))) as Record<
|
|
254
283
|
string,
|
|
255
284
|
IndexConfigsUnion
|
|
256
285
|
>,
|
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,25 +127,102 @@ 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);
|
|
140
|
+
return queryResults(
|
|
141
|
+
store,
|
|
142
|
+
s,
|
|
143
|
+
r,
|
|
144
|
+
v,
|
|
145
|
+
request,
|
|
146
|
+
count,
|
|
147
|
+
size,
|
|
148
|
+
query,
|
|
149
|
+
options,
|
|
150
|
+
started,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Exact indexed symbol lookup; declarations and same-name definitions remain visible. */
|
|
155
|
+
export async function lookup(
|
|
156
|
+
store: CollectionStore,
|
|
157
|
+
s: Settings,
|
|
158
|
+
r: Repository,
|
|
159
|
+
v: Published,
|
|
160
|
+
symbol: string,
|
|
161
|
+
size = 5,
|
|
162
|
+
filters: { path?: string; language?: string } = {},
|
|
163
|
+
): Promise<unknown[]> {
|
|
164
|
+
const started = performance.now();
|
|
165
|
+
invariant(
|
|
166
|
+
symbol.trim().length > 0 && symbol.length <= 200,
|
|
167
|
+
"Symbol must contain 1–200 characters.",
|
|
168
|
+
);
|
|
169
|
+
invariant(
|
|
170
|
+
Number.isInteger(size) && size >= 1 && size <= 20,
|
|
171
|
+
"Lookup limit must be between 1 and 20.",
|
|
172
|
+
);
|
|
173
|
+
const request = {
|
|
174
|
+
bool: Object.entries({ kind: "chunk", symbol, ...filters })
|
|
175
|
+
.filter(([, value]) => value !== undefined)
|
|
176
|
+
.map(([field, value]) => ({
|
|
177
|
+
queryString: { query: value, defaultField: field, skipSyntax: true },
|
|
178
|
+
occur: "filter",
|
|
179
|
+
})),
|
|
180
|
+
};
|
|
181
|
+
return queryResults(
|
|
182
|
+
store,
|
|
183
|
+
s,
|
|
184
|
+
r,
|
|
185
|
+
v,
|
|
186
|
+
request,
|
|
187
|
+
size,
|
|
188
|
+
size,
|
|
189
|
+
symbol,
|
|
190
|
+
{},
|
|
191
|
+
started,
|
|
192
|
+
true,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function queryResults(
|
|
197
|
+
store: CollectionStore,
|
|
198
|
+
s: Settings,
|
|
199
|
+
r: Repository,
|
|
200
|
+
v: Published,
|
|
201
|
+
request: Record<string, unknown>,
|
|
202
|
+
count: number,
|
|
203
|
+
size: number,
|
|
204
|
+
query: string,
|
|
205
|
+
options: RerankOptions,
|
|
206
|
+
started: number,
|
|
207
|
+
includeSource = false,
|
|
208
|
+
): Promise<unknown[]> {
|
|
100
209
|
const entries = await inventoryAt(store, r, v);
|
|
101
210
|
const files = new Map(
|
|
102
211
|
entries.filter((e) => e.status === "included").map((e) => [e.fileId, e]),
|
|
103
212
|
);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
213
|
+
const candidates = [];
|
|
214
|
+
const hits = await store.query(v.tagName, request, count);
|
|
215
|
+
invariant(
|
|
216
|
+
hits.length <= count &&
|
|
217
|
+
new Set(hits.map((h) => h.doc.id)).size === hits.length,
|
|
218
|
+
"Invalid search candidate count or duplicate IDs.",
|
|
219
|
+
);
|
|
220
|
+
for (const hit of hits) {
|
|
110
221
|
const d = hit.doc,
|
|
111
222
|
e = files.get(d.fileId as string);
|
|
112
223
|
invariant(
|
|
113
224
|
d.kind === "chunk" &&
|
|
225
|
+
(!includeSource || d.symbol === query) &&
|
|
114
226
|
d.configHash === r.configHash &&
|
|
115
227
|
e?.chunkIds?.includes(d.id) &&
|
|
116
228
|
d.contentHash === e.contentHash &&
|
|
@@ -128,28 +240,83 @@ export async function search(
|
|
|
128
240
|
path: String(d.path),
|
|
129
241
|
contentHash: String(d.contentHash),
|
|
130
242
|
chunkId: d.id,
|
|
131
|
-
chunkHash:
|
|
243
|
+
chunkHash: recordHash(d, r.preset ?? PRESET),
|
|
132
244
|
startLine: Number(d.startLine),
|
|
133
245
|
endLine: Number(d.endLine),
|
|
134
246
|
};
|
|
135
247
|
// Check the exact source/ranges before persisting an evidence handle.
|
|
136
|
-
const evidence = await readHandle(store, s, handle
|
|
137
|
-
|
|
248
|
+
const evidence = await readHandle(store, s, handle, {
|
|
249
|
+
exactChunk: includeSource || !!options.rerank,
|
|
250
|
+
});
|
|
251
|
+
candidates.push({
|
|
252
|
+
handle,
|
|
253
|
+
text: evidence.sourceText,
|
|
254
|
+
result: {
|
|
255
|
+
resultId: id,
|
|
256
|
+
repository: r.repoKey,
|
|
257
|
+
commitOid: v.commitOid,
|
|
258
|
+
version: v.tagName,
|
|
259
|
+
snapshotId: v.snapshotId,
|
|
260
|
+
path: d.path,
|
|
261
|
+
startLine: d.startLine,
|
|
262
|
+
endLine: d.endLine,
|
|
263
|
+
symbol: d.symbol,
|
|
264
|
+
...(includeSource
|
|
265
|
+
? {
|
|
266
|
+
scope: d.scope,
|
|
267
|
+
chunkKind: d.chunkKind,
|
|
268
|
+
sourceText: evidence.sourceText,
|
|
269
|
+
}
|
|
270
|
+
: { score: hit.score, excerpt: evidence.sourceText.slice(0, 600) }),
|
|
271
|
+
citation: evidence.citation,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const retrievalMs = performance.now() - started;
|
|
276
|
+
let rerankMs = 0;
|
|
277
|
+
let ranked = candidates.map((candidate, index) => ({
|
|
278
|
+
...candidate,
|
|
279
|
+
index,
|
|
280
|
+
rerankScore: 0,
|
|
281
|
+
}));
|
|
282
|
+
if (options.rerank && candidates.length) {
|
|
283
|
+
const response = await qwenScores(
|
|
284
|
+
query,
|
|
285
|
+
candidates.map((c) => ({ id: c.handle.chunkId!, text: c.text })),
|
|
286
|
+
);
|
|
287
|
+
rerankMs = response.elapsedMs;
|
|
288
|
+
ranked = ranked
|
|
289
|
+
.map((c, index) => ({ ...c, rerankScore: response.scores[index]! }))
|
|
290
|
+
.sort((a, b) => b.rerankScore - a.rerankScore || a.index - b.index);
|
|
291
|
+
}
|
|
292
|
+
if (options.rerank || includeSource)
|
|
293
|
+
invariant(
|
|
294
|
+
(await store.tags()).find((t) => t.name === v.tagName)?.snapshotId ===
|
|
295
|
+
v.snapshotId,
|
|
296
|
+
"Pinned Tag was deleted or recreated during result preparation.",
|
|
297
|
+
);
|
|
298
|
+
const result = [];
|
|
299
|
+
for (const c of ranked.slice(0, size)) {
|
|
300
|
+
await atomic(join(stateRoot(), "results", c.handle.id + ".json"), c.handle);
|
|
138
301
|
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,
|
|
302
|
+
...c.result,
|
|
303
|
+
...(options.rerank
|
|
304
|
+
? {
|
|
305
|
+
rerankScore: c.rerankScore,
|
|
306
|
+
retrievalRank: c.index + 1,
|
|
307
|
+
reranker: { model: QWEN_MODEL, revision: QWEN_REVISION },
|
|
308
|
+
}
|
|
309
|
+
: {}),
|
|
151
310
|
});
|
|
152
311
|
}
|
|
312
|
+
if (options.rerank)
|
|
313
|
+
options.onTiming?.({
|
|
314
|
+
candidates: candidates.length,
|
|
315
|
+
returned: result.length,
|
|
316
|
+
retrievalMs,
|
|
317
|
+
rerankMs,
|
|
318
|
+
searchMs: performance.now() - started,
|
|
319
|
+
});
|
|
153
320
|
return result;
|
|
154
321
|
}
|
|
155
322
|
export async function loadHandle(id: string): Promise<Handle> {
|
|
@@ -191,8 +358,19 @@ export async function readHandle(
|
|
|
191
358
|
context?: number;
|
|
192
359
|
fullFile?: boolean;
|
|
193
360
|
lines?: [number, number];
|
|
361
|
+
exactChunk?: boolean;
|
|
362
|
+
implementation?: boolean;
|
|
194
363
|
} = {},
|
|
195
364
|
) {
|
|
365
|
+
invariant(
|
|
366
|
+
!options.implementation ||
|
|
367
|
+
(h.chunkId &&
|
|
368
|
+
!options.fullFile &&
|
|
369
|
+
!options.lines &&
|
|
370
|
+
!options.context &&
|
|
371
|
+
!options.exactChunk),
|
|
372
|
+
"Implementation reads require a result handle without range overrides.",
|
|
373
|
+
);
|
|
196
374
|
invariant(
|
|
197
375
|
h.endpoint === s.endpoint && h.project === s.project,
|
|
198
376
|
"Result belongs to another endpoint/project; restore that connection before reading.",
|
|
@@ -218,11 +396,13 @@ export async function readHandle(
|
|
|
218
396
|
raw.length === 0
|
|
219
397
|
? 0
|
|
220
398
|
: lineAt(raw, raw.length - (raw.at(-1) === 10 ? 1 : 0));
|
|
399
|
+
let chunkSource: string | undefined;
|
|
400
|
+
let implementation: Implementation | undefined;
|
|
221
401
|
if (h.chunkId) {
|
|
222
402
|
const d = await one(store, tag(v.tagName), h.chunkId);
|
|
223
403
|
invariant(
|
|
224
404
|
d &&
|
|
225
|
-
|
|
405
|
+
recordHash(d, h.repository.preset ?? PRESET) === h.chunkHash &&
|
|
226
406
|
d.fileId === h.fileId &&
|
|
227
407
|
d.contentHash === h.contentHash,
|
|
228
408
|
"Pinned chunk has changed or is missing.",
|
|
@@ -239,6 +419,12 @@ export async function readHandle(
|
|
|
239
419
|
lineAt(raw, z - 1) === h.endLine,
|
|
240
420
|
"Invalid source range.",
|
|
241
421
|
);
|
|
422
|
+
chunkSource = raw.subarray(a, z).toString("utf8");
|
|
423
|
+
if (options.implementation)
|
|
424
|
+
implementation = await implementationSpan(file.sourceText, h.path, {
|
|
425
|
+
startByte: a,
|
|
426
|
+
endByte: z,
|
|
427
|
+
});
|
|
242
428
|
}
|
|
243
429
|
const context = options.context ?? 0;
|
|
244
430
|
invariant(
|
|
@@ -247,6 +433,10 @@ export async function readHandle(
|
|
|
247
433
|
);
|
|
248
434
|
let start = options.lines?.[0] ?? h.startLine,
|
|
249
435
|
end = options.lines?.[1] ?? h.endLine;
|
|
436
|
+
if (implementation?.status === "resolved") {
|
|
437
|
+
start = implementation.startLine!;
|
|
438
|
+
end = implementation.endLine!;
|
|
439
|
+
}
|
|
250
440
|
if (options.fullFile) {
|
|
251
441
|
start = 1;
|
|
252
442
|
end = total;
|
|
@@ -264,9 +454,19 @@ export async function readHandle(
|
|
|
264
454
|
end = Math.min(total, end + context);
|
|
265
455
|
}
|
|
266
456
|
const lines = file.sourceText.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
457
|
+
invariant(
|
|
458
|
+
!options.exactChunk ||
|
|
459
|
+
(chunkSource !== undefined &&
|
|
460
|
+
!options.fullFile &&
|
|
461
|
+
!options.lines &&
|
|
462
|
+
!options.context),
|
|
463
|
+
"Exact chunk reads require a chunk handle without range overrides.",
|
|
464
|
+
);
|
|
465
|
+
const sourceText = options.exactChunk
|
|
466
|
+
? chunkSource!
|
|
467
|
+
: options.fullFile
|
|
468
|
+
? file.sourceText
|
|
469
|
+
: lines.slice(start - 1, end).join("");
|
|
270
470
|
return {
|
|
271
471
|
repository: h.repository.repoKey,
|
|
272
472
|
commitOid: v.commitOid,
|
|
@@ -276,6 +476,7 @@ export async function readHandle(
|
|
|
276
476
|
startLine: start,
|
|
277
477
|
endLine: end,
|
|
278
478
|
contentHash: h.contentHash,
|
|
479
|
+
...(implementation ? { implementation } : {}),
|
|
279
480
|
sourceText,
|
|
280
481
|
citation: `${h.repository.repoKey}@${v.commitOid}:${h.path}:${start}-${end}`,
|
|
281
482
|
};
|