sift-light 1.0.0 → 1.0.2
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 +39 -0
- package/README.md +11 -4
- package/README.zh-CN.md +11 -4
- package/package.json +1 -1
- package/plugins/sift-light/.claude-plugin/plugin.json +1 -1
- package/plugins/sift-light/.codex-plugin/plugin.json +1 -1
- package/plugins/sift-light/.mcp.json +1 -1
- package/plugins/sift-light/concept-worker.mjs +39 -12
- package/plugins/sift-light/kimi.plugin.json +2 -2
- package/plugins/sift-light/omp-extension.mjs +558 -83
- package/plugins/sift-light/package.json +1 -1
- package/src/analysis-limits.ts +1 -1
- package/src/analysis-store.ts +6 -2
- package/src/analysis-types.ts +10 -1
- package/src/concept-inference.ts +44 -11
- package/src/concept-model.ts +2 -2
- package/src/concept-search.ts +12 -2
- package/src/concept-worker.mjs +39 -12
- package/src/concept-worker.ts +1 -1
- package/src/config-reader.ts +11 -3
- package/src/evidence-service.ts +117 -13
- package/src/file-discovery.ts +111 -17
- package/src/hybrid-search.ts +13 -0
- package/src/index.ts +4 -10
- package/src/inspect.ts +3 -0
- package/src/language-capability-definitions.ts +9 -0
- package/src/mcp-model-output.ts +47 -1
- package/src/mcp-semantic-judge.ts +19 -8
- package/src/mcp-server.mjs +754 -247
- package/src/mcp-server.ts +11 -13
- package/src/mcp.ts +2 -0
- package/src/model-error.ts +6 -16
- package/src/omp-index.ts +4 -6
- package/src/prompt-guidelines.ts +5 -5
- package/src/python-roles.ts +247 -0
- package/src/redaction.ts +9 -2
- package/src/request-aliases.ts +48 -0
- package/src/request-contract-catalog.ts +24 -7
- package/src/request-contract.ts +60 -6
- package/src/request.ts +3 -1
- package/src/runtime.ts +2 -2
- package/src/semantic-judge.ts +17 -3
- package/src/service.ts +31 -1
- package/src/session-summary.ts +4 -4
- package/src/source-inspection.ts +27 -2
- package/src/tool-schema.ts +18 -18
- package/src/tui/layout.ts +2 -2
- package/src/tui/renderers.ts +2 -2
- package/src/types.ts +4 -0
package/src/analysis-limits.ts
CHANGED
|
@@ -13,7 +13,7 @@ export const MAX_ANALYSIS_STORAGE_BYTES = 32 * 1024 * 1024;
|
|
|
13
13
|
export const ANALYSIS_METADATA_RESERVE_BYTES = 64 * 1024;
|
|
14
14
|
export const MAX_ANALYSIS_REASONS = 64;
|
|
15
15
|
export const MAX_ANALYSIS_REASON_BYTES = 4 * 1024;
|
|
16
|
-
export const MIN_ANY_OF_TERMS =
|
|
16
|
+
export const MIN_ANY_OF_TERMS = 1;
|
|
17
17
|
export const MAX_ANY_OF_TERMS = 8;
|
|
18
18
|
/** Public union size; execution keeps the proven eight-term scan boundary per chunk. */
|
|
19
19
|
export const MAX_ANY_OF_TOTAL_TERMS = 64;
|
package/src/analysis-store.ts
CHANGED
|
@@ -206,7 +206,10 @@ function publicAnalysisItem(
|
|
|
206
206
|
return {
|
|
207
207
|
path: item.path,
|
|
208
208
|
line: item.line,
|
|
209
|
-
label:
|
|
209
|
+
label:
|
|
210
|
+
(result.kind === "outline" && modelOutput) || result.kind === "files"
|
|
211
|
+
? item.label
|
|
212
|
+
: publicAnalysisLabel(result),
|
|
210
213
|
index: index + 1,
|
|
211
214
|
...(inspect ? { inspect } : {}),
|
|
212
215
|
...(publicDetails ? { details: publicDetails } : {}),
|
|
@@ -455,7 +458,8 @@ export class AnalysisStore {
|
|
|
455
458
|
if (items.length >= 30 || !appendItem(index)) break;
|
|
456
459
|
}
|
|
457
460
|
} else {
|
|
458
|
-
|
|
461
|
+
const pageSize = result.pageSize ?? 30;
|
|
462
|
+
for (let index = offset; index < result.items.length && items.length < pageSize; index += 1) {
|
|
459
463
|
if (!appendItem(index)) break;
|
|
460
464
|
}
|
|
461
465
|
}
|
package/src/analysis-types.ts
CHANGED
|
@@ -4,7 +4,12 @@ import type { SearchScopeDetails, ResultStatistics } from "./types.js";
|
|
|
4
4
|
import type { ValidationDetails } from "./validation-types.js";
|
|
5
5
|
import type { ConceptSourceSummary } from "./concept-source-generation.js";
|
|
6
6
|
|
|
7
|
-
export type CoverageStatus =
|
|
7
|
+
export type CoverageStatus =
|
|
8
|
+
| "complete"
|
|
9
|
+
| "partial"
|
|
10
|
+
| "policy-filtered"
|
|
11
|
+
| "skipped"
|
|
12
|
+
| "not-applicable";
|
|
8
13
|
|
|
9
14
|
export const SEMANTIC_JUDGE_CLASSIFICATIONS = [
|
|
10
15
|
"implementation-candidate",
|
|
@@ -125,6 +130,8 @@ export interface AnalysisDetails {
|
|
|
125
130
|
passagesRanked?: number;
|
|
126
131
|
elapsedMs?: number;
|
|
127
132
|
filesEnumerated?: number;
|
|
133
|
+
ignoredFiles?: number;
|
|
134
|
+
ignoredMatches?: number;
|
|
128
135
|
filesAdmitted?: number;
|
|
129
136
|
filesParsed?: number;
|
|
130
137
|
filesSkipped?: number;
|
|
@@ -159,4 +166,6 @@ export interface AnalysisResultSet {
|
|
|
159
166
|
stats?: AnalysisDetails["stats"];
|
|
160
167
|
sourceGeneration?: ConceptSourceSummary;
|
|
161
168
|
redact?: boolean;
|
|
169
|
+
/** Explicit output page size (`limit`); pages default to 30 items. */
|
|
170
|
+
pageSize?: number;
|
|
162
171
|
}
|
package/src/concept-inference.ts
CHANGED
|
@@ -21,7 +21,9 @@ export interface ConceptEmbeddingCallbacks {
|
|
|
21
21
|
) => void | Promise<void>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Bound each ONNX call to four model windows. Larger native batches raised peak
|
|
25
|
+
// RSS without a useful cold-start gain on the measured workload.
|
|
26
|
+
const INFERENCE_BATCH_SIZE = 4;
|
|
25
27
|
|
|
26
28
|
/** Snap a candidate UTF-16 index so it never splits a surrogate pair. */
|
|
27
29
|
export function safeUtf16End(text: string, end: number): number {
|
|
@@ -62,6 +64,10 @@ export function maximumTokenSafeEnd(
|
|
|
62
64
|
text: string,
|
|
63
65
|
start: number,
|
|
64
66
|
): number {
|
|
67
|
+
// Most source passages already fit the model window. Avoid repeatedly
|
|
68
|
+
// tokenizing prefixes of the same passage in the binary-search path.
|
|
69
|
+
if (tokenCount(extractor, `${prefix}${text.slice(start)}`) <= CONCEPT_MODEL_TOKENS)
|
|
70
|
+
return text.length;
|
|
65
71
|
let low = start + 1;
|
|
66
72
|
let high = text.length;
|
|
67
73
|
let accepted = start;
|
|
@@ -142,30 +148,56 @@ export async function embedConceptInputs(
|
|
|
142
148
|
): Promise<CachedConceptEmbedding[]> {
|
|
143
149
|
const layouts = pending.map((item) => tokenSafeWindows(extractor, item));
|
|
144
150
|
const inputs = layouts.flatMap((windows) => windows.map((window) => window.input));
|
|
145
|
-
const vectors: number[][] =
|
|
151
|
+
const vectors: (number[] | undefined)[] = Array(inputs.length);
|
|
146
152
|
const layoutEnds: number[] = [];
|
|
153
|
+
const remainingWindows = layouts.map((windows) => windows.length);
|
|
147
154
|
let layoutEnd = 0;
|
|
148
155
|
for (const windows of layouts) {
|
|
149
156
|
layoutEnd += windows.length;
|
|
150
157
|
layoutEnds.push(layoutEnd);
|
|
151
158
|
}
|
|
159
|
+
const orderedInputs = inputs
|
|
160
|
+
.map((input, index) => ({ input, index, tokens: tokenCount(extractor, input) }))
|
|
161
|
+
// Start with the largest shapes so the native arena can reuse allocations
|
|
162
|
+
// for smaller batches instead of retaining a growing sequence of arenas.
|
|
163
|
+
.toSorted((left, right) => right.tokens - left.tokens || left.index - right.index);
|
|
164
|
+
const vectorFor = (index: number): number[] => {
|
|
165
|
+
const vector = vectors[index];
|
|
166
|
+
if (!vector) throw new Error("Missing concept embedding vector");
|
|
167
|
+
return vector;
|
|
168
|
+
};
|
|
169
|
+
const ownerOf = (index: number): number => {
|
|
170
|
+
let low = 0;
|
|
171
|
+
let high = layoutEnds.length;
|
|
172
|
+
while (low < high) {
|
|
173
|
+
const middle = Math.floor((low + high) / 2);
|
|
174
|
+
if (index < layoutEnds[middle]!) high = middle;
|
|
175
|
+
else low = middle + 1;
|
|
176
|
+
}
|
|
177
|
+
return low;
|
|
178
|
+
};
|
|
152
179
|
let completedEmbedding = 0;
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
let completedWindows = 0;
|
|
181
|
+
for (let offset = 0; offset < orderedInputs.length;) {
|
|
182
|
+
const orderedBatch = orderedInputs.slice(offset, offset + INFERENCE_BATCH_SIZE);
|
|
183
|
+
const batch = orderedBatch.map((item) => item.input);
|
|
155
184
|
// oxlint-disable-next-line no-await-in-loop -- bounded batches cap native tensor memory.
|
|
156
185
|
const tensor = await extractor(batch, { pooling: "mean", normalize: true });
|
|
157
186
|
if (tensor.data.length !== batch.length * CONCEPT_EMBEDDING_DIMENSIONS)
|
|
158
187
|
throw new Error("Unexpected concept embedding dimensions");
|
|
159
188
|
for (let index = 0; index < batch.length; index += 1) {
|
|
160
189
|
const start = index * CONCEPT_EMBEDDING_DIMENSIONS;
|
|
161
|
-
|
|
162
|
-
|
|
190
|
+
const inputIndex = orderedBatch[index]!.index;
|
|
191
|
+
vectors[inputIndex] = Array.from(
|
|
192
|
+
tensor.data.slice(start, start + CONCEPT_EMBEDDING_DIMENSIONS),
|
|
193
|
+
Number,
|
|
163
194
|
);
|
|
195
|
+
remainingWindows[ownerOf(inputIndex)]!--;
|
|
164
196
|
}
|
|
165
|
-
|
|
197
|
+
completedWindows += batch.length;
|
|
166
198
|
while (
|
|
167
|
-
|
|
168
|
-
|
|
199
|
+
remainingWindows[completedEmbedding] !== undefined &&
|
|
200
|
+
remainingWindows[completedEmbedding] === 0
|
|
169
201
|
) {
|
|
170
202
|
const start = completedEmbedding === 0 ? 0 : layoutEnds[completedEmbedding - 1]!;
|
|
171
203
|
const item = pending[completedEmbedding];
|
|
@@ -177,7 +209,7 @@ export async function embedConceptInputs(
|
|
|
177
209
|
windows: (layouts[completedEmbedding] ?? []).map((window, index) => ({
|
|
178
210
|
start: window.start,
|
|
179
211
|
end: window.end,
|
|
180
|
-
vector:
|
|
212
|
+
vector: vectorFor(start + index),
|
|
181
213
|
})),
|
|
182
214
|
},
|
|
183
215
|
completedEmbedding + 1,
|
|
@@ -186,6 +218,7 @@ export async function embedConceptInputs(
|
|
|
186
218
|
completedEmbedding += 1;
|
|
187
219
|
}
|
|
188
220
|
callbacks.onBatch?.(completedWindows, inputs.length);
|
|
221
|
+
offset += batch.length;
|
|
189
222
|
}
|
|
190
223
|
let vectorIndex = 0;
|
|
191
224
|
return pending.map((item, index) => ({
|
|
@@ -193,7 +226,7 @@ export async function embedConceptInputs(
|
|
|
193
226
|
windows: (layouts[index] ?? []).map((window) => ({
|
|
194
227
|
start: window.start,
|
|
195
228
|
end: window.end,
|
|
196
|
-
vector:
|
|
229
|
+
vector: vectorFor(vectorIndex++),
|
|
197
230
|
})),
|
|
198
231
|
}));
|
|
199
232
|
}
|
package/src/concept-model.ts
CHANGED
|
@@ -8,10 +8,10 @@ export const CONCEPT_MODEL = "Xenova/multilingual-e5-small";
|
|
|
8
8
|
export const CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
|
|
9
9
|
export const MAX_CONCEPT_CHARS = 1_000;
|
|
10
10
|
export const CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
|
|
11
|
-
export const CONCEPT_MODEL_TOKENS =
|
|
11
|
+
export const CONCEPT_MODEL_TOKENS = 256;
|
|
12
12
|
export const CONCEPT_WINDOW_OVERLAP_TOKENS = 64;
|
|
13
13
|
export const CONCEPT_EMBEDDING_DIMENSIONS = 384;
|
|
14
|
-
export const CONCEPT_CACHE_VERSION =
|
|
14
|
+
export const CONCEPT_CACHE_VERSION = 2;
|
|
15
15
|
export const CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
16
16
|
export const CONCEPT_TIMEOUT_MS = 10 * 60_000;
|
|
17
17
|
export const MIN_CONCEPT_TIMEOUT_MS = 1_000;
|
package/src/concept-search.ts
CHANGED
|
@@ -86,6 +86,14 @@ function scoreProfile(scores: readonly number[]): ConceptScoreProfile {
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** E5 cosine scores favor very short, generic passages in close races. Keep the
|
|
90
|
+
* raw cosine as evidence and apply a bounded length correction only to rank. */
|
|
91
|
+
export function conceptRankingScore(cosine: number, passageLength: number): number {
|
|
92
|
+
const referenceLength = 500;
|
|
93
|
+
const maximumCorrection = 0.02;
|
|
94
|
+
return cosine - maximumCorrection * (1 - Math.sqrt(Math.min(passageLength / referenceLength, 1)));
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
function passage(document: SourceDocument, start: number): { value: Passage; next: number } {
|
|
90
98
|
let end = Math.min(document.text.length, start + MAX_CONCEPT_CHARS);
|
|
91
99
|
if (end < document.text.length) {
|
|
@@ -433,6 +441,7 @@ async function runConceptSearch(
|
|
|
433
441
|
const batchItems = passages.map((item, index) => {
|
|
434
442
|
const similarity = inferred.scores[index];
|
|
435
443
|
if (similarity === undefined) throw new Error("Missing concept similarity");
|
|
444
|
+
const rankingScore = conceptRankingScore(similarity, item.text.length);
|
|
436
445
|
const evidence = rangeEvidence(item.document, item.range);
|
|
437
446
|
return {
|
|
438
447
|
path: item.document.path,
|
|
@@ -445,8 +454,9 @@ async function runConceptSearch(
|
|
|
445
454
|
kind: "concept-candidate",
|
|
446
455
|
certainty: "candidate",
|
|
447
456
|
score: similarity,
|
|
457
|
+
rankingScore,
|
|
448
458
|
rankingReason:
|
|
449
|
-
"local multilingual E5 cosine similarity; relevance candidate, no binding or execution claim",
|
|
459
|
+
"local multilingual E5 cosine similarity with bounded short-passage rank correction; relevance candidate, no binding or execution claim",
|
|
450
460
|
model: CONCEPT_MODEL,
|
|
451
461
|
revision: CONCEPT_REVISION,
|
|
452
462
|
tokenTruncated: false,
|
|
@@ -457,7 +467,7 @@ async function runConceptSearch(
|
|
|
457
467
|
});
|
|
458
468
|
const ranked = [...result.items, ...batchItems].toSorted(
|
|
459
469
|
(a, b) =>
|
|
460
|
-
Number(b.details?.
|
|
470
|
+
Number(b.details?.rankingScore) - Number(a.details?.rankingScore) ||
|
|
461
471
|
a.path.localeCompare(b.path) ||
|
|
462
472
|
a.line - b.line,
|
|
463
473
|
);
|
package/src/concept-worker.mjs
CHANGED
|
@@ -27,10 +27,10 @@ class SiftLightError extends Error {
|
|
|
27
27
|
var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
|
|
28
28
|
var CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
|
|
29
29
|
var MAX_CONCEPT_CHARS = 1000;
|
|
30
|
-
var CONCEPT_MODEL_TOKENS =
|
|
30
|
+
var CONCEPT_MODEL_TOKENS = 256;
|
|
31
31
|
var CONCEPT_WINDOW_OVERLAP_TOKENS = 64;
|
|
32
32
|
var CONCEPT_EMBEDDING_DIMENSIONS = 384;
|
|
33
|
-
var CONCEPT_CACHE_VERSION =
|
|
33
|
+
var CONCEPT_CACHE_VERSION = 2;
|
|
34
34
|
var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
35
35
|
var CONCEPT_TIMEOUT_MS = 10 * 60000;
|
|
36
36
|
var MAX_CONCEPT_TIMEOUT_MS = 60 * 60000;
|
|
@@ -222,7 +222,7 @@ async function enforceConceptCacheLimit(root, maximumBytes = CONCEPT_CACHE_MAX_B
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// src/concept-inference.ts
|
|
225
|
-
var INFERENCE_BATCH_SIZE =
|
|
225
|
+
var INFERENCE_BATCH_SIZE = 4;
|
|
226
226
|
function safeUtf16End(text, end) {
|
|
227
227
|
if (end <= 0 || end >= text.length)
|
|
228
228
|
return end;
|
|
@@ -247,6 +247,8 @@ function binarySearchBudget(span) {
|
|
|
247
247
|
return 2 * Math.max(span, 1) + 32;
|
|
248
248
|
}
|
|
249
249
|
function maximumTokenSafeEnd(extractor, prefix, text, start) {
|
|
250
|
+
if (tokenCount(extractor, `${prefix}${text.slice(start)}`) <= CONCEPT_MODEL_TOKENS)
|
|
251
|
+
return text.length;
|
|
250
252
|
let low = start + 1;
|
|
251
253
|
let high = text.length;
|
|
252
254
|
let accepted = start;
|
|
@@ -312,25 +314,49 @@ function tokenSafeWindows(extractor, pending) {
|
|
|
312
314
|
async function embedConceptInputs(extractor, pending, callbacks = {}) {
|
|
313
315
|
const layouts = pending.map((item) => tokenSafeWindows(extractor, item));
|
|
314
316
|
const inputs = layouts.flatMap((windows) => windows.map((window) => window.input));
|
|
315
|
-
const vectors =
|
|
317
|
+
const vectors = Array(inputs.length);
|
|
316
318
|
const layoutEnds = [];
|
|
319
|
+
const remainingWindows = layouts.map((windows) => windows.length);
|
|
317
320
|
let layoutEnd = 0;
|
|
318
321
|
for (const windows of layouts) {
|
|
319
322
|
layoutEnd += windows.length;
|
|
320
323
|
layoutEnds.push(layoutEnd);
|
|
321
324
|
}
|
|
325
|
+
const orderedInputs = inputs.map((input, index) => ({ input, index, tokens: tokenCount(extractor, input) })).toSorted((left, right) => right.tokens - left.tokens || left.index - right.index);
|
|
326
|
+
const vectorFor = (index) => {
|
|
327
|
+
const vector = vectors[index];
|
|
328
|
+
if (!vector)
|
|
329
|
+
throw new Error("Missing concept embedding vector");
|
|
330
|
+
return vector;
|
|
331
|
+
};
|
|
332
|
+
const ownerOf = (index) => {
|
|
333
|
+
let low = 0;
|
|
334
|
+
let high = layoutEnds.length;
|
|
335
|
+
while (low < high) {
|
|
336
|
+
const middle = Math.floor((low + high) / 2);
|
|
337
|
+
if (index < layoutEnds[middle])
|
|
338
|
+
high = middle;
|
|
339
|
+
else
|
|
340
|
+
low = middle + 1;
|
|
341
|
+
}
|
|
342
|
+
return low;
|
|
343
|
+
};
|
|
322
344
|
let completedEmbedding = 0;
|
|
323
|
-
|
|
324
|
-
|
|
345
|
+
let completedWindows = 0;
|
|
346
|
+
for (let offset = 0;offset < orderedInputs.length; ) {
|
|
347
|
+
const orderedBatch = orderedInputs.slice(offset, offset + INFERENCE_BATCH_SIZE);
|
|
348
|
+
const batch = orderedBatch.map((item) => item.input);
|
|
325
349
|
const tensor = await extractor(batch, { pooling: "mean", normalize: true });
|
|
326
350
|
if (tensor.data.length !== batch.length * CONCEPT_EMBEDDING_DIMENSIONS)
|
|
327
351
|
throw new Error("Unexpected concept embedding dimensions");
|
|
328
352
|
for (let index = 0;index < batch.length; index += 1) {
|
|
329
353
|
const start = index * CONCEPT_EMBEDDING_DIMENSIONS;
|
|
330
|
-
|
|
354
|
+
const inputIndex = orderedBatch[index].index;
|
|
355
|
+
vectors[inputIndex] = Array.from(tensor.data.slice(start, start + CONCEPT_EMBEDDING_DIMENSIONS), Number);
|
|
356
|
+
remainingWindows[ownerOf(inputIndex)]--;
|
|
331
357
|
}
|
|
332
|
-
|
|
333
|
-
while (
|
|
358
|
+
completedWindows += batch.length;
|
|
359
|
+
while (remainingWindows[completedEmbedding] !== undefined && remainingWindows[completedEmbedding] === 0) {
|
|
334
360
|
const start = completedEmbedding === 0 ? 0 : layoutEnds[completedEmbedding - 1];
|
|
335
361
|
const item = pending[completedEmbedding];
|
|
336
362
|
if (!item)
|
|
@@ -340,12 +366,13 @@ async function embedConceptInputs(extractor, pending, callbacks = {}) {
|
|
|
340
366
|
windows: (layouts[completedEmbedding] ?? []).map((window, index) => ({
|
|
341
367
|
start: window.start,
|
|
342
368
|
end: window.end,
|
|
343
|
-
vector:
|
|
369
|
+
vector: vectorFor(start + index)
|
|
344
370
|
}))
|
|
345
371
|
}, completedEmbedding + 1, pending.length);
|
|
346
372
|
completedEmbedding += 1;
|
|
347
373
|
}
|
|
348
374
|
callbacks.onBatch?.(completedWindows, inputs.length);
|
|
375
|
+
offset += batch.length;
|
|
349
376
|
}
|
|
350
377
|
let vectorIndex = 0;
|
|
351
378
|
return pending.map((item, index) => ({
|
|
@@ -353,7 +380,7 @@ async function embedConceptInputs(extractor, pending, callbacks = {}) {
|
|
|
353
380
|
windows: (layouts[index] ?? []).map((window) => ({
|
|
354
381
|
start: window.start,
|
|
355
382
|
end: window.end,
|
|
356
|
-
vector:
|
|
383
|
+
vector: vectorFor(vectorIndex++)
|
|
357
384
|
}))
|
|
358
385
|
}));
|
|
359
386
|
}
|
|
@@ -504,7 +531,7 @@ async function search() {
|
|
|
504
531
|
local_files_only: true,
|
|
505
532
|
dtype: "q8",
|
|
506
533
|
device: "cpu",
|
|
507
|
-
session_options: { intraOpNumThreads:
|
|
534
|
+
session_options: { intraOpNumThreads: 4, interOpNumThreads: 1 }
|
|
508
535
|
});
|
|
509
536
|
try {
|
|
510
537
|
created = await embedConceptInputs(extractor, missing, {
|
package/src/concept-worker.ts
CHANGED
|
@@ -140,7 +140,7 @@ async function search(): Promise<void> {
|
|
|
140
140
|
local_files_only: true,
|
|
141
141
|
dtype: "q8",
|
|
142
142
|
device: "cpu",
|
|
143
|
-
session_options: { intraOpNumThreads:
|
|
143
|
+
session_options: { intraOpNumThreads: 4, interOpNumThreads: 1 },
|
|
144
144
|
});
|
|
145
145
|
try {
|
|
146
146
|
created = await embedConceptInputs(extractor, missing, {
|
package/src/config-reader.ts
CHANGED
|
@@ -12,6 +12,7 @@ export const SIFT_LIGHT_ENFORCEMENT_ENV = "SIFT_LIGHT_ENFORCE_SEARCH";
|
|
|
12
12
|
export interface SiftLightConfig {
|
|
13
13
|
locale: SiftLightLocale;
|
|
14
14
|
enforceSearch?: SearchEnforcementMode;
|
|
15
|
+
vectorSearchEnabled?: boolean;
|
|
15
16
|
semanticJudge?: SemanticJudgeConfig;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -44,12 +45,14 @@ export const DEFAULT_SEMANTIC_JUDGE_CONFIG: Readonly<SemanticJudgeConfig> = {
|
|
|
44
45
|
export const DEFAULT_SIFT_LIGHT_CONFIG: Readonly<SiftLightConfig> = {
|
|
45
46
|
locale: "en",
|
|
46
47
|
enforceSearch: "hard",
|
|
48
|
+
vectorSearchEnabled: false,
|
|
47
49
|
semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG,
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
interface RawSiftLightConfig {
|
|
51
53
|
locale?: unknown;
|
|
52
54
|
enforceSearch?: unknown;
|
|
55
|
+
vectorSearchEnabled?: unknown;
|
|
53
56
|
semanticJudge?: unknown;
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -221,21 +224,26 @@ function parseConfig(value: unknown, path: string): SiftLightConfig {
|
|
|
221
224
|
throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
|
|
222
225
|
}
|
|
223
226
|
const unknown = Object.keys(value).filter(
|
|
224
|
-
(key) => !["locale", "enforceSearch", "semanticJudge"].includes(key),
|
|
227
|
+
(key) => !["locale", "enforceSearch", "vectorSearchEnabled", "semanticJudge"].includes(key),
|
|
225
228
|
);
|
|
226
229
|
if (unknown.length > 0) {
|
|
227
230
|
throw new Error(
|
|
228
|
-
`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch and semanticJudge are accepted`,
|
|
231
|
+
`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch, vectorSearchEnabled and semanticJudge are accepted`,
|
|
229
232
|
);
|
|
230
233
|
}
|
|
231
|
-
const { locale, enforceSearch } = value;
|
|
234
|
+
const { locale, enforceSearch, vectorSearchEnabled } = value;
|
|
232
235
|
if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
|
|
233
236
|
throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
|
|
234
237
|
}
|
|
238
|
+
if (vectorSearchEnabled !== undefined && typeof vectorSearchEnabled !== "boolean") {
|
|
239
|
+
throw new Error(`Invalid sift-light config at ${path}: vectorSearchEnabled must be a boolean`);
|
|
240
|
+
}
|
|
235
241
|
const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
|
|
236
242
|
return {
|
|
237
243
|
locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
|
|
238
244
|
enforceSearch: enforcement,
|
|
245
|
+
vectorSearchEnabled:
|
|
246
|
+
vectorSearchEnabled ?? DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled ?? false,
|
|
239
247
|
semanticJudge: parseSemanticJudge(value.semanticJudge, path),
|
|
240
248
|
};
|
|
241
249
|
}
|
package/src/evidence-service.ts
CHANGED
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
continueSource,
|
|
40
40
|
inspectDocuments,
|
|
41
41
|
matchInspectionTarget,
|
|
42
|
-
inspectDocumentsMetadata,
|
|
43
42
|
type SourceInspectionTarget,
|
|
44
43
|
} from "./source-inspection.js";
|
|
45
44
|
import { validateSavedEvidence } from "./evidence-validation.js";
|
|
@@ -48,6 +47,7 @@ import type { CodeStructureProvider } from "./structure.js";
|
|
|
48
47
|
import { filterRoleOccurrences, findFunctionConjunctions } from "./syntax-search.js";
|
|
49
48
|
import { syntaxLanguage } from "./syntax.js";
|
|
50
49
|
import { parsePythonOutline } from "./python-outline.js";
|
|
50
|
+
import { pythonRoleAnalysis } from "./python-roles.js";
|
|
51
51
|
import {
|
|
52
52
|
combineHybridSearch,
|
|
53
53
|
hybridConceptLimit,
|
|
@@ -66,10 +66,12 @@ import {
|
|
|
66
66
|
MAX_STRUCTURE_FILES,
|
|
67
67
|
} from "./analysis-limits.js";
|
|
68
68
|
import {
|
|
69
|
+
MAX_INSPECT_REQUEST_TARGETS,
|
|
69
70
|
MAX_INSPECT_TARGETS,
|
|
70
71
|
type SearchRequest,
|
|
71
72
|
type SearchScopeDetails,
|
|
72
73
|
type SiftLightResult,
|
|
74
|
+
type StructureDetails,
|
|
73
75
|
} from "./types.js";
|
|
74
76
|
|
|
75
77
|
export function isEvidenceRequest(input: SiftLightInput): boolean {
|
|
@@ -95,6 +97,100 @@ export function isEvidenceRequest(input: SiftLightInput): boolean {
|
|
|
95
97
|
);
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Splits one inspect request into the page served now and an exact request for
|
|
102
|
+
* the rest. `paths` without a cursor opens each file from line 1.
|
|
103
|
+
*/
|
|
104
|
+
function pageInspectRequest(input: SiftLightInput): {
|
|
105
|
+
current: SiftLightInput;
|
|
106
|
+
remaining?: SiftLightInput;
|
|
107
|
+
} {
|
|
108
|
+
if (input.scope === "expand")
|
|
109
|
+
throw new SiftLightError(
|
|
110
|
+
"mode=inspect reads exact locations; scope=expand does not apply (omit scope)",
|
|
111
|
+
);
|
|
112
|
+
const { scope: _scope, paths, ...rest } = input;
|
|
113
|
+
let request: SiftLightInput = rest;
|
|
114
|
+
if (paths !== undefined) {
|
|
115
|
+
if (
|
|
116
|
+
input.cursor !== undefined ||
|
|
117
|
+
input.targets !== undefined ||
|
|
118
|
+
input.matchIndices !== undefined ||
|
|
119
|
+
input.path !== undefined ||
|
|
120
|
+
input.line !== undefined ||
|
|
121
|
+
input.matchIndex !== undefined
|
|
122
|
+
)
|
|
123
|
+
throw new SiftLightError(
|
|
124
|
+
"mode=inspect paths opens files from line 1 and cannot be combined with cursor, targets, matchIndices, path, line or matchIndex",
|
|
125
|
+
);
|
|
126
|
+
request = { ...rest, targets: paths.map((path) => ({ path, line: 1 })) };
|
|
127
|
+
}
|
|
128
|
+
const redact = input.redact ? { redact: true } : {};
|
|
129
|
+
const requested = request.targets?.length ?? request.matchIndices?.length ?? 0;
|
|
130
|
+
if (requested > MAX_INSPECT_REQUEST_TARGETS)
|
|
131
|
+
throw new SiftLightError(
|
|
132
|
+
`mode=inspect accepts at most ${String(MAX_INSPECT_REQUEST_TARGETS)} targets per request; ${String(MAX_INSPECT_TARGETS)} are inspected per response`,
|
|
133
|
+
);
|
|
134
|
+
if (request.targets && request.targets.length > MAX_INSPECT_TARGETS)
|
|
135
|
+
return {
|
|
136
|
+
current: { ...request, targets: request.targets.slice(0, MAX_INSPECT_TARGETS) },
|
|
137
|
+
remaining: {
|
|
138
|
+
mode: "inspect",
|
|
139
|
+
targets: request.targets.slice(MAX_INSPECT_TARGETS),
|
|
140
|
+
...redact,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
if (request.matchIndices && request.matchIndices.length > MAX_INSPECT_TARGETS)
|
|
144
|
+
return {
|
|
145
|
+
current: { ...request, matchIndices: request.matchIndices.slice(0, MAX_INSPECT_TARGETS) },
|
|
146
|
+
remaining: {
|
|
147
|
+
mode: "inspect",
|
|
148
|
+
...(request.cursor !== undefined ? { cursor: request.cursor } : {}),
|
|
149
|
+
matchIndices: request.matchIndices.slice(MAX_INSPECT_TARGETS),
|
|
150
|
+
...redact,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
return { current: request };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The unserved targets stay explicit: the page is partial and names its exact continuation. */
|
|
157
|
+
function withRemainingInspection(
|
|
158
|
+
result: SiftLightResult,
|
|
159
|
+
remaining: SiftLightInput,
|
|
160
|
+
): SiftLightResult {
|
|
161
|
+
const count = remaining.targets?.length ?? remaining.matchIndices?.length ?? 0;
|
|
162
|
+
return {
|
|
163
|
+
text: `${result.text}\n\n[${String(count)} more target(s) were not inspected; one response inspects ${String(MAX_INSPECT_TARGETS)}.]\nNext request: ${JSON.stringify(remaining)}`,
|
|
164
|
+
details: {
|
|
165
|
+
...result.details,
|
|
166
|
+
status: "partial",
|
|
167
|
+
snapshotComplete: false,
|
|
168
|
+
nextRequest: remaining,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Structure facts already proven by the analysis that produced a bounded item. */
|
|
174
|
+
function analysisItemStructure(item: AnalysisItem): StructureDetails {
|
|
175
|
+
const details = item.details ?? {};
|
|
176
|
+
const language = typeof details.language === "string" ? details.language : undefined;
|
|
177
|
+
const name = typeof details.name === "string" ? details.name : undefined;
|
|
178
|
+
const scope = Array.isArray(details.scope)
|
|
179
|
+
? details.scope.filter((entry): entry is string => typeof entry === "string")
|
|
180
|
+
: typeof details.scope === "string"
|
|
181
|
+
? [details.scope]
|
|
182
|
+
: [];
|
|
183
|
+
const kind = details.kind === "function" ? "function" : (item.label.split(" ", 1)[0] ?? "symbol");
|
|
184
|
+
return {
|
|
185
|
+
status: "available",
|
|
186
|
+
provider: language === "python" ? "python-outline" : "tree-sitter",
|
|
187
|
+
...(language ? { language } : {}),
|
|
188
|
+
...(name
|
|
189
|
+
? { symbol: { name, kind, scope, range: { startLine: item.line, endLine: item.line } } }
|
|
190
|
+
: {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
98
194
|
export interface EvidenceSearchOptions {
|
|
99
195
|
modelOutput?: boolean;
|
|
100
196
|
onProgress?: (progress: OperationProgress) => void;
|
|
@@ -126,12 +222,12 @@ function validateTerms(input: SiftLightInput): string[] | undefined {
|
|
|
126
222
|
}
|
|
127
223
|
if (
|
|
128
224
|
!Array.isArray(terms) ||
|
|
129
|
-
terms.length <
|
|
225
|
+
terms.length < 1 ||
|
|
130
226
|
terms.length > 3 ||
|
|
131
227
|
terms.some((term) => typeof term !== "string" || !term.trim() || /[\r\n\0]/.test(term)) ||
|
|
132
228
|
new Set(terms).size !== terms.length
|
|
133
229
|
)
|
|
134
|
-
throw new SiftLightError("allOf requires
|
|
230
|
+
throw new SiftLightError("allOf requires 1–3 distinct, nonempty, single-line literal terms");
|
|
135
231
|
if (
|
|
136
232
|
input.pattern !== undefined ||
|
|
137
233
|
input.roles !== undefined ||
|
|
@@ -396,13 +492,13 @@ export class EvidenceService {
|
|
|
396
492
|
if (typeof input.sourceCursor !== "string" || !input.sourceCursor.trim())
|
|
397
493
|
throw new CursorError("A nonempty sourceCursor is required");
|
|
398
494
|
if (input.mode !== "inspect") throw new SiftLightError("sourceCursor requires mode=inspect");
|
|
399
|
-
return continueSource(input.sourceCursor, access, this.#continuations);
|
|
495
|
+
return continueSource(input.sourceCursor, access, this.#continuations, input.path);
|
|
400
496
|
}
|
|
401
497
|
if (input.mode === "inspect") {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
498
|
+
const { current, remaining } = pageInspectRequest(input);
|
|
499
|
+
const targets = this.#inspectionTargets(current, cwd);
|
|
500
|
+
const result = await inspectDocuments(targets, access, this.#continuations, this.#structure);
|
|
501
|
+
return remaining ? withRemainingInspection(result, remaining) : result;
|
|
406
502
|
}
|
|
407
503
|
if (input.mode === "concept") {
|
|
408
504
|
const execution = await this.#conceptSearch(input, access, options.onProgress);
|
|
@@ -518,7 +614,7 @@ export class EvidenceService {
|
|
|
518
614
|
"anyOf is an explicit case-sensitive literal union; omit pattern, allOf, within, roles, literal and ignoreCase",
|
|
519
615
|
);
|
|
520
616
|
if (input.mode !== undefined && input.mode !== "auto" && input.mode !== "matches")
|
|
521
|
-
throw new SiftLightError("anyOf mode must be omitted, auto, or
|
|
617
|
+
throw new SiftLightError("anyOf mode must be omitted, auto, matches or summary");
|
|
522
618
|
const chunks = Array.from(
|
|
523
619
|
{ length: Math.ceil(anyOf.length / MAX_ANY_OF_TERMS) },
|
|
524
620
|
(_, index) => anyOf.slice(index * MAX_ANY_OF_TERMS, (index + 1) * MAX_ANY_OF_TERMS),
|
|
@@ -696,8 +792,12 @@ export class EvidenceService {
|
|
|
696
792
|
);
|
|
697
793
|
if (item) result.items.push(item);
|
|
698
794
|
} else if (terms || input.roles) {
|
|
699
|
-
|
|
700
|
-
const
|
|
795
|
+
// Python roles come from a bounded lexical scanner; other languages parse.
|
|
796
|
+
const pythonRoles = !terms && /\.py$/iu.test(file.document.path);
|
|
797
|
+
if (pythonRoles || syntaxLanguage(file.document.path)) syntaxCapableFiles += 1;
|
|
798
|
+
const syntax = pythonRoles
|
|
799
|
+
? pythonRoleAnalysis(file.document)
|
|
800
|
+
: await access.syntax(file.document);
|
|
701
801
|
const classified = terms
|
|
702
802
|
? findFunctionConjunctions(
|
|
703
803
|
file.document,
|
|
@@ -790,12 +890,16 @@ export class EvidenceService {
|
|
|
790
890
|
const item = this.#analyses.item(input.cursor, input.matchIndex);
|
|
791
891
|
if (!item.source || !item.range)
|
|
792
892
|
throw new CursorError("This analysis item has no verified source range");
|
|
793
|
-
|
|
893
|
+
// Outline symbols and function conjunctions carry their exact syntax range,
|
|
894
|
+
// so inspection returns that version-checked range as source (issue #96).
|
|
895
|
+
const bounded = item.details?.kind === "symbol" || item.details?.kind === "function";
|
|
794
896
|
return {
|
|
795
897
|
path: item.path,
|
|
796
898
|
line: item.line,
|
|
797
899
|
reference: item.source,
|
|
798
|
-
...(
|
|
900
|
+
...(bounded
|
|
901
|
+
? { range: item.range, structure: analysisItemStructure(item) }
|
|
902
|
+
: { absoluteFocus: item.range.start }),
|
|
799
903
|
};
|
|
800
904
|
}
|
|
801
905
|
return {
|