sift-light 1.0.0

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.
Files changed (145) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/CONTRIBUTING.md +19 -0
  3. package/LICENSE +661 -0
  4. package/README.md +177 -0
  5. package/README.zh-CN.md +177 -0
  6. package/SECURITY.md +25 -0
  7. package/package.json +142 -0
  8. package/plugins/sift-light/.claude-plugin/plugin.json +10 -0
  9. package/plugins/sift-light/.codex-plugin/plugin.json +20 -0
  10. package/plugins/sift-light/.mcp.json +24 -0
  11. package/plugins/sift-light/LICENSE +661 -0
  12. package/plugins/sift-light/concept-worker.mjs +567 -0
  13. package/plugins/sift-light/hooks/THIRD_PARTY_NOTICES.txt +70 -0
  14. package/plugins/sift-light/hooks/hooks.json +16 -0
  15. package/plugins/sift-light/hooks/search-policy.mjs +4113 -0
  16. package/plugins/sift-light/hooks/tree-sitter-bash.wasm +0 -0
  17. package/plugins/sift-light/hooks/tree-sitter-powershell.wasm +0 -0
  18. package/plugins/sift-light/hooks/tree-sitter.wasm +0 -0
  19. package/plugins/sift-light/kimi.plugin.json +36 -0
  20. package/plugins/sift-light/mcp-server.mjs +1 -0
  21. package/plugins/sift-light/omp-extension.mjs +20914 -0
  22. package/plugins/sift-light/package.json +11 -0
  23. package/plugins/sift-light/syntax-worker.mjs +280 -0
  24. package/plugins/sift-light/syntax-worker.toml +2 -0
  25. package/src/analysis-evidence.ts +43 -0
  26. package/src/analysis-limits.ts +27 -0
  27. package/src/analysis-store.ts +559 -0
  28. package/src/analysis-term-pages.ts +73 -0
  29. package/src/analysis-types.ts +162 -0
  30. package/src/audit-search.ts +391 -0
  31. package/src/capped-lines.ts +112 -0
  32. package/src/concept-embedding-cache.ts +186 -0
  33. package/src/concept-inference.ts +218 -0
  34. package/src/concept-model.ts +96 -0
  35. package/src/concept-search.ts +577 -0
  36. package/src/concept-setup.ts +75 -0
  37. package/src/concept-source-generation.ts +268 -0
  38. package/src/concept-worker.mjs +567 -0
  39. package/src/concept-worker.ts +210 -0
  40. package/src/config-reader.ts +283 -0
  41. package/src/config.ts +37 -0
  42. package/src/context-budget.ts +26 -0
  43. package/src/discovery-errors.ts +2 -0
  44. package/src/errors.ts +95 -0
  45. package/src/evidence-candidates.ts +344 -0
  46. package/src/evidence-ranking.ts +48 -0
  47. package/src/evidence-service.ts +1047 -0
  48. package/src/evidence-validation.ts +466 -0
  49. package/src/evidence-validity.ts +33 -0
  50. package/src/excerpt.ts +53 -0
  51. package/src/file-discovery.ts +150 -0
  52. package/src/file-metadata-filter.ts +46 -0
  53. package/src/format.ts +407 -0
  54. package/src/git-diff.ts +195 -0
  55. package/src/git-process.ts +189 -0
  56. package/src/git-repository.ts +367 -0
  57. package/src/git-source.ts +385 -0
  58. package/src/historical-paths.ts +205 -0
  59. package/src/hybrid-search.ts +340 -0
  60. package/src/import-model.ts +611 -0
  61. package/src/import-navigation.ts +100 -0
  62. package/src/import-resolution.ts +240 -0
  63. package/src/index.ts +97 -0
  64. package/src/inspect-batch.ts +265 -0
  65. package/src/inspect.ts +271 -0
  66. package/src/language-capabilities.ts +157 -0
  67. package/src/language-capability-definitions.ts +204 -0
  68. package/src/literal-search.ts +25 -0
  69. package/src/mcp-cli.ts +21 -0
  70. package/src/mcp-model-output.ts +120 -0
  71. package/src/mcp-output.ts +9 -0
  72. package/src/mcp-semantic-judge.ts +39 -0
  73. package/src/mcp-server.mjs +14064 -0
  74. package/src/mcp-server.ts +157 -0
  75. package/src/mcp-stdio.ts +114 -0
  76. package/src/mcp.ts +631 -0
  77. package/src/model-error.ts +120 -0
  78. package/src/multi-term-search.ts +122 -0
  79. package/src/omp-index.ts +277 -0
  80. package/src/omp-marketplace-entry.ts +7 -0
  81. package/src/operation-lifecycle.ts +413 -0
  82. package/src/operation-output.ts +100 -0
  83. package/src/owned-parallel.ts +22 -0
  84. package/src/owned-process.ts +102 -0
  85. package/src/owned-task-queue.ts +38 -0
  86. package/src/package-version.ts +3 -0
  87. package/src/path-policy.ts +203 -0
  88. package/src/pi-search-policy.ts +33 -0
  89. package/src/prompt-guidelines.ts +54 -0
  90. package/src/python-outline.ts +231 -0
  91. package/src/record-value.ts +3 -0
  92. package/src/redaction.ts +95 -0
  93. package/src/request-contract-catalog.ts +215 -0
  94. package/src/request-contract-error.ts +96 -0
  95. package/src/request-contract.ts +481 -0
  96. package/src/request.ts +132 -0
  97. package/src/result-statistics.ts +178 -0
  98. package/src/rg.ts +574 -0
  99. package/src/ripgrep-diagnostics.ts +122 -0
  100. package/src/ripgrep-executable.ts +43 -0
  101. package/src/runtime.ts +53 -0
  102. package/src/scan-revisions.ts +148 -0
  103. package/src/script-runtime.ts +11 -0
  104. package/src/search-policy-commands.ts +170 -0
  105. package/src/search-policy-hook.ts +61 -0
  106. package/src/search-policy-recovery.ts +388 -0
  107. package/src/search-policy-shell.ts +280 -0
  108. package/src/search-policy.ts +102 -0
  109. package/src/search-retention.ts +77 -0
  110. package/src/semantic-judge-batches.ts +135 -0
  111. package/src/semantic-judge.ts +519 -0
  112. package/src/service.ts +945 -0
  113. package/src/session-summary.ts +100 -0
  114. package/src/snapshot-store.ts +203 -0
  115. package/src/source-access.ts +288 -0
  116. package/src/source-continuations.ts +168 -0
  117. package/src/source-document.ts +277 -0
  118. package/src/source-inspection.ts +731 -0
  119. package/src/source-pages.ts +112 -0
  120. package/src/source.ts +286 -0
  121. package/src/structural-search.ts +125 -0
  122. package/src/structure.ts +259 -0
  123. package/src/summary-previews.ts +96 -0
  124. package/src/summary.ts +79 -0
  125. package/src/syntax-facts.ts +464 -0
  126. package/src/syntax-fields.ts +161 -0
  127. package/src/syntax-search.ts +361 -0
  128. package/src/syntax-tree.ts +30 -0
  129. package/src/syntax-types.ts +72 -0
  130. package/src/syntax-worker.mjs +280 -0
  131. package/src/syntax-worker.toml +2 -0
  132. package/src/syntax-worker.ts +143 -0
  133. package/src/syntax.ts +259 -0
  134. package/src/test-navigation-facts.ts +261 -0
  135. package/src/test-navigation.ts +543 -0
  136. package/src/tool-schema.ts +321 -0
  137. package/src/tui/dashboard.ts +107 -0
  138. package/src/tui/layout.ts +291 -0
  139. package/src/tui/palette.ts +73 -0
  140. package/src/tui/presentation.ts +240 -0
  141. package/src/tui/renderers.ts +106 -0
  142. package/src/types.ts +368 -0
  143. package/src/validation-output.ts +148 -0
  144. package/src/validation-types.ts +48 -0
  145. package/src/workspace-files.ts +142 -0
@@ -0,0 +1,577 @@
1
+ import { rangeEvidence } from "./analysis-evidence.js";
2
+ import { OwnedTaskQueue } from "./owned-task-queue.js";
3
+ const inferenceQueue = new OwnedTaskQueue();
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { StringDecoder } from "node:string_decoder";
7
+ import { mkdir } from "node:fs/promises";
8
+ import { rm } from "node:fs/promises";
9
+ import { createHash, randomUUID } from "node:crypto";
10
+ import type { AnalysisResultSet, ConceptScoreProfile } from "./analysis-types.js";
11
+ import {
12
+ CONCEPT_MODEL,
13
+ CONCEPT_PASSAGE_OVERLAP_CHARS,
14
+ CONCEPT_REVISION,
15
+ MAX_CONCEPT_CHARS,
16
+ MAX_CONCEPT_WORKER_OUTPUT_BYTES,
17
+ conceptCacheDirectory,
18
+ } from "./concept-model.js";
19
+ import { abortError, ConceptUnavailableError, SiftLightError } from "./errors.js";
20
+ import { runOwnedProcess } from "./owned-process.js";
21
+ import { scriptRuntimeEnvironment } from "./script-runtime.js";
22
+ import { isRecordValue } from "./record-value.js";
23
+ import { normalizeRequest } from "./request.js";
24
+ import type { SiftLightInput } from "./service.js";
25
+ import { SourceAccess } from "./source-access.js";
26
+ import type { SourceDocument, ByteRange } from "./source-document.js";
27
+ import {
28
+ createConceptSourceGeneration,
29
+ conceptSourceSummary,
30
+ verifyConceptSourceGeneration,
31
+ type ConceptSourceGeneration,
32
+ } from "./concept-source-generation.js";
33
+ import type { OperationProgress } from "./operation-lifecycle.js";
34
+ import { MAX_ANALYSIS_RESULTS, MAX_STRUCTURE_FILES } from "./analysis-limits.js";
35
+ import { listWorkspaceFiles } from "./workspace-files.js";
36
+
37
+ export interface Passage {
38
+ document: SourceDocument;
39
+ range: ByteRange;
40
+ text: string;
41
+ }
42
+
43
+ function conciseWorkerError(stderr: string): string {
44
+ const errorLine = stderr
45
+ .split(/\r?\n/)
46
+ .map((line) => line.trim())
47
+ .find((line) => /^(?:[A-Za-z_$][\w$]*Error|Error|error):\s*\S/i.test(line));
48
+ if (errorLine) return errorLine.replace(/^[^:]+(?:Error|error):\s*/i, "").slice(0, 512);
49
+ const diagnostic = stderr
50
+ .split(/\r?\n/)
51
+ .map((line) => line.trim())
52
+ .find(Boolean);
53
+ if (diagnostic) return diagnostic.slice(0, 512);
54
+ return "worker returned no concise diagnostic";
55
+ }
56
+
57
+ /** A worker that disappears without a protocol result can be restarted once by the owner. */
58
+ export class ConceptWorkerExitError extends SiftLightError {
59
+ readonly exitCode: number | null;
60
+
61
+ constructor(exitCode: number | null, diagnostic: string) {
62
+ super(`Local concept worker exited unexpectedly (${String(exitCode)}): ${diagnostic}`);
63
+ this.name = "ConceptWorkerExitError";
64
+ this.exitCode = exitCode;
65
+ }
66
+ }
67
+
68
+ function scoreProfile(scores: readonly number[]): ConceptScoreProfile {
69
+ const ordered = scores.toSorted((a, b) => b - a);
70
+ const count = ordered.length;
71
+ const top = ordered[0];
72
+ const min = ordered.at(-1);
73
+ if (top === undefined || min === undefined || count === 0)
74
+ throw new Error("Concept score profile requires at least one score");
75
+ const middle = Math.floor(count / 2);
76
+ const middleValue = ordered[middle] ?? top;
77
+ const median = count % 2 === 1 ? middleValue : ((ordered[middle - 1] ?? top) + middleValue) / 2;
78
+ const second = ordered[1];
79
+ return {
80
+ count,
81
+ top,
82
+ ...(second !== undefined ? { second, topMargin: top - second } : {}),
83
+ median: median ?? top,
84
+ min,
85
+ spread: top - min,
86
+ };
87
+ }
88
+
89
+ function passage(document: SourceDocument, start: number): { value: Passage; next: number } {
90
+ let end = Math.min(document.text.length, start + MAX_CONCEPT_CHARS);
91
+ if (end < document.text.length) {
92
+ const newline = document.text.lastIndexOf("\n", end);
93
+ if (newline > start + MAX_CONCEPT_CHARS / 2) end = newline + 1;
94
+ const code = document.text.charCodeAt(end);
95
+ if (code >= 0xdc00 && code <= 0xdfff) end -= 1;
96
+ }
97
+ const range = { start: document.toByteOffset(start), end: document.toByteOffset(end) };
98
+ let next = end;
99
+ if (end < document.text.length) {
100
+ next = Math.max(start + 1, end - CONCEPT_PASSAGE_OVERLAP_CHARS);
101
+ const code = document.text.charCodeAt(next);
102
+ if (code >= 0xdc00 && code <= 0xdfff) next += 1;
103
+ }
104
+ return {
105
+ value: {
106
+ document,
107
+ range,
108
+ text: document.text.slice(start, end),
109
+ },
110
+ next,
111
+ };
112
+ }
113
+
114
+ export interface ConceptInferenceResult {
115
+ scores: number[];
116
+ cacheHits: number;
117
+ cacheMisses: number;
118
+ cacheMaxBytes: number;
119
+ cacheBytes?: number;
120
+ windowsRanked: number;
121
+ warnings: string[];
122
+ peakRssBytes: number;
123
+ }
124
+
125
+ export interface ConceptSearchExecution {
126
+ analysis: AnalysisResultSet;
127
+ sourceGeneration: ConceptSourceGeneration;
128
+ }
129
+
130
+ export interface ConceptSearchOptions {
131
+ retainPaths?: ReadonlySet<string> | PromiseLike<ReadonlySet<string>>;
132
+ verifySourceGeneration?: boolean;
133
+ }
134
+
135
+ export type ConceptInferenceRunner = (
136
+ query: string,
137
+ passages: Passage[],
138
+ parent?: AbortSignal,
139
+ onProgress?: (progress: OperationProgress) => void,
140
+ ) => Promise<ConceptInferenceResult>;
141
+
142
+ async function similarities(
143
+ query: string,
144
+ passages: Passage[],
145
+ parent?: AbortSignal,
146
+ onProgress?: (progress: OperationProgress) => void,
147
+ ): Promise<ConceptInferenceResult> {
148
+ const worker = fileURLToPath(new URL("./concept-worker.mjs", import.meta.url));
149
+ const config = fileURLToPath(new URL("./syntax-worker.toml", import.meta.url));
150
+ const env = scriptRuntimeEnvironment();
151
+ const stagingRoot = join(conceptCacheDirectory(), ".staging", randomUUID());
152
+ await mkdir(stagingRoot, { recursive: true });
153
+ let bytes = 0;
154
+ let lineBuffer = "";
155
+ let finalValue: Record<string, unknown> | undefined;
156
+ let sawFinal = false;
157
+ const decoder = new StringDecoder("utf8");
158
+ try {
159
+ const processResult = await runOwnedProcess(
160
+ {
161
+ executable: process.execPath,
162
+ args: process.versions.bun
163
+ ? [
164
+ `--config=${config}`,
165
+ "--no-env-file",
166
+ "--no-macros",
167
+ "--no-install",
168
+ worker,
169
+ "--infer",
170
+ ]
171
+ : [worker, "--infer"],
172
+ cwd: dirname(worker),
173
+ env: { ...env, SIFT_LIGHT_CONCEPT_CACHE_STAGING_DIR: stagingRoot },
174
+ ...(parent ? { signal: parent } : {}),
175
+ input: Buffer.from(
176
+ JSON.stringify({
177
+ query,
178
+ encodedPassages: passages.map((item) => Buffer.from(item.text).toString("base64")),
179
+ }),
180
+ ),
181
+ },
182
+ async (stdout) => {
183
+ for await (const chunk of stdout) {
184
+ bytes += chunk.byteLength;
185
+ if (bytes > MAX_CONCEPT_WORKER_OUTPUT_BYTES)
186
+ throw new SiftLightError("Concept worker exceeded its 4 MiB response budget");
187
+ lineBuffer += decoder.write(Buffer.from(chunk));
188
+ let newline = lineBuffer.indexOf("\n");
189
+ while (newline >= 0) {
190
+ const line = lineBuffer.slice(0, newline).trim();
191
+ lineBuffer = lineBuffer.slice(newline + 1);
192
+ newline = lineBuffer.indexOf("\n");
193
+ if (!line) continue;
194
+ const parsed: unknown = JSON.parse(line);
195
+ if (!isRecordValue(parsed) || typeof parsed.type !== "string")
196
+ throw new SiftLightError("Invalid concept worker progress response");
197
+ if (parsed.type === "progress") {
198
+ if (sawFinal)
199
+ throw new SiftLightError("Concept worker emitted progress after its result");
200
+ if (
201
+ typeof parsed.phase !== "string" ||
202
+ (parsed.completed !== undefined &&
203
+ (typeof parsed.completed !== "number" ||
204
+ !Number.isSafeInteger(parsed.completed) ||
205
+ parsed.completed < 0)) ||
206
+ (parsed.total !== undefined &&
207
+ (typeof parsed.total !== "number" ||
208
+ !Number.isSafeInteger(parsed.total) ||
209
+ parsed.total < 0))
210
+ )
211
+ throw new SiftLightError("Invalid concept worker progress response");
212
+ const progress: OperationProgress = { phase: parsed.phase };
213
+ if (typeof parsed.completed === "number") progress.completed = parsed.completed;
214
+ if (typeof parsed.total === "number") progress.total = parsed.total;
215
+ if (
216
+ typeof parsed.uniqueEmbeddings === "number" &&
217
+ typeof parsed.passages === "number"
218
+ )
219
+ progress.detail = `unique embeddings ${String(parsed.uniqueEmbeddings)}, passages ${String(parsed.passages)}`;
220
+ onProgress?.(progress);
221
+ } else if (parsed.type === "result") {
222
+ if (sawFinal) throw new SiftLightError("Concept worker emitted more than one result");
223
+ sawFinal = true;
224
+ finalValue = parsed;
225
+ } else {
226
+ throw new SiftLightError("Invalid concept worker response type");
227
+ }
228
+ }
229
+ }
230
+ },
231
+ );
232
+ if (processResult.code === null)
233
+ throw new ConceptWorkerExitError(
234
+ processResult.code,
235
+ conciseWorkerError(processResult.stderr),
236
+ );
237
+ if (processResult.code !== 0)
238
+ throw new ConceptUnavailableError(
239
+ `Local concept inference failed (${String(processResult.code)}): ${conciseWorkerError(processResult.stderr)}`,
240
+ );
241
+ lineBuffer += decoder.end();
242
+ if (lineBuffer.trim()) {
243
+ const parsed: unknown = JSON.parse(lineBuffer.trim());
244
+ if (!isRecordValue(parsed) || parsed.type !== "result")
245
+ throw new SiftLightError("Concept worker did not return a result record");
246
+ if (sawFinal) throw new SiftLightError("Concept worker emitted more than one result");
247
+ finalValue = parsed;
248
+ sawFinal = true;
249
+ }
250
+ const value: unknown = finalValue;
251
+ if (
252
+ !isRecordValue(value) ||
253
+ !Array.isArray(value.scores) ||
254
+ value.scores.length !== passages.length ||
255
+ value.scores.some((score) => typeof score !== "number" || !Number.isFinite(score)) ||
256
+ typeof value.cacheHits !== "number" ||
257
+ !Number.isSafeInteger(value.cacheHits) ||
258
+ value.cacheHits < 0 ||
259
+ typeof value.cacheMisses !== "number" ||
260
+ !Number.isSafeInteger(value.cacheMisses) ||
261
+ value.cacheMisses < 0 ||
262
+ typeof value.cacheMaxBytes !== "number" ||
263
+ !Number.isSafeInteger(value.cacheMaxBytes) ||
264
+ value.cacheMaxBytes <= 0 ||
265
+ (value.cacheBytes !== undefined &&
266
+ (typeof value.cacheBytes !== "number" ||
267
+ !Number.isSafeInteger(value.cacheBytes) ||
268
+ value.cacheBytes < 0)) ||
269
+ typeof value.windowsRanked !== "number" ||
270
+ !Number.isSafeInteger(value.windowsRanked) ||
271
+ value.windowsRanked < passages.length ||
272
+ !Array.isArray(value.warnings) ||
273
+ value.warnings.some((warning) => typeof warning !== "string") ||
274
+ typeof value.peakRssBytes !== "number" ||
275
+ !Number.isFinite(value.peakRssBytes) ||
276
+ value.peakRssBytes < 0
277
+ )
278
+ throw new SiftLightError("Invalid concept inference response");
279
+ return {
280
+ scores: value.scores.filter((score): score is number => typeof score === "number"),
281
+ cacheHits: value.cacheHits,
282
+ cacheMisses: value.cacheMisses,
283
+ cacheMaxBytes: value.cacheMaxBytes,
284
+ ...(typeof value.cacheBytes === "number" ? { cacheBytes: value.cacheBytes } : {}),
285
+ windowsRanked: value.windowsRanked,
286
+ warnings: value.warnings.filter((warning): warning is string => typeof warning === "string"),
287
+ peakRssBytes: value.peakRssBytes,
288
+ };
289
+ } catch (error) {
290
+ if (parent?.aborted) throw abortError();
291
+ if (error instanceof ConceptWorkerExitError) throw error;
292
+ if (error instanceof ConceptUnavailableError) throw error;
293
+ const message = error instanceof Error ? error.message : "unknown provider failure";
294
+ throw new ConceptUnavailableError(`Local concept inference failed: ${message}`, {
295
+ cause: error,
296
+ });
297
+ } finally {
298
+ try {
299
+ await rm(stagingRoot, { recursive: true, force: true });
300
+ } catch (error) {
301
+ // oxlint-disable-next-line no-unsafe-finally -- owner staging cleanup must report failure.
302
+ throw new ConceptUnavailableError("Unable to clean up concept worker staging files", {
303
+ cause: error,
304
+ });
305
+ }
306
+ }
307
+ }
308
+
309
+ export function validateConceptQuery(query: string | undefined): string {
310
+ if (!query?.trim() || query.length > 256 || !query.isWellFormed() || /[\r\n\0]/.test(query))
311
+ throw new SiftLightError(
312
+ "Concept query requires nonempty, single-line well-formed text of at most 256 characters",
313
+ );
314
+ return query;
315
+ }
316
+
317
+ async function runConceptSearch(
318
+ input: SiftLightInput,
319
+ access: SourceAccess,
320
+ infer: ConceptInferenceRunner,
321
+ onProgress?: (progress: OperationProgress) => void,
322
+ options: ConceptSearchOptions = {},
323
+ ): Promise<ConceptSearchExecution> {
324
+ const query = validateConceptQuery(input.query);
325
+ const retainPaths =
326
+ options.retainPaths === undefined ? undefined : Promise.resolve(options.retainPaths);
327
+ const started = performance.now();
328
+ const request = normalizeRequest({ ...input, pattern: "" });
329
+ const filters = {
330
+ ...(request.path ? { path: request.path } : {}),
331
+ glob: request.glob,
332
+ exclude: request.exclude,
333
+ hidden: request.hidden,
334
+ };
335
+ const files = await listWorkspaceFiles(access.cwd, access.signal, {
336
+ ...filters,
337
+ maxFiles: access.maxFiles,
338
+ });
339
+ const fileBatches = Array.from(
340
+ { length: Math.ceil(files.paths.length / MAX_STRUCTURE_FILES) },
341
+ (_, index) => files.paths.slice(index * MAX_STRUCTURE_FILES, (index + 1) * MAX_STRUCTURE_FILES),
342
+ );
343
+ const result: AnalysisResultSet = {
344
+ kind: "concept",
345
+ unit: "evidence-items",
346
+ items: [],
347
+ partial: files.partial,
348
+ reasons: [...files.reasons],
349
+ redact: input.redact ?? false,
350
+ };
351
+ const inventory: ConceptSourceGeneration["inventory"][number][] = [];
352
+ const retainedDocuments: SourceDocument[] = [];
353
+ const sourceReasons = [...files.reasons];
354
+ let sourcePartial = files.partial;
355
+ const allScores: number[] = [];
356
+ let filesAdmitted = 0;
357
+ let filesSkippedEmpty = 0;
358
+ let filesSkippedBinary = 0;
359
+ let filesUnavailable = 0;
360
+ let filesRead = 0;
361
+ let bytesRead = 0;
362
+ let passagesQueued = 0;
363
+ let conceptWindowsRanked = 0;
364
+ let conceptCacheHits = 0;
365
+ let conceptCacheMisses = 0;
366
+ let conceptCacheMaxBytes = 0;
367
+ let conceptCacheBytes: number | undefined;
368
+ let inferencePeakRssBytes = 0;
369
+ let retentionTruncated = false;
370
+ const generationStartedAt = Date.now();
371
+ for (const [batchIndex, paths] of fileBatches.entries()) {
372
+ const batchAccess = access.batch(paths.length);
373
+ // oxlint-disable-next-line no-await-in-loop -- sequential batches bound live source documents and model memory.
374
+ const generation = await createConceptSourceGeneration(batchAccess, filters, {
375
+ paths,
376
+ partial: false,
377
+ reasons: [],
378
+ });
379
+ inventory.push(...generation.inventory);
380
+ filesAdmitted += generation.filesAdmitted;
381
+ filesSkippedEmpty += generation.filesSkippedEmpty;
382
+ filesSkippedBinary += generation.filesSkippedBinary;
383
+ filesUnavailable += generation.filesUnavailable;
384
+ filesRead += batchAccess.filesRead;
385
+ bytesRead += batchAccess.bytesRead;
386
+ sourcePartial ||= generation.partial;
387
+ sourceReasons.push(...generation.reasons);
388
+ result.partial ||= generation.partial;
389
+ result.reasons.push(...generation.reasons);
390
+ onProgress?.({
391
+ phase: "source-generation",
392
+ completed: Math.min((batchIndex + 1) * MAX_STRUCTURE_FILES, files.paths.length),
393
+ total: files.paths.length,
394
+ detail: `batch ${String(batchIndex + 1)} of ${String(fileBatches.length)}; admitted ${String(filesAdmitted)}, unavailable ${String(filesUnavailable)}`,
395
+ });
396
+
397
+ const documents = generation.documents.map((document) => ({ document, next: 0 }));
398
+ const passages: Passage[] = [];
399
+ while (documents.some((item) => item.next < item.document.text.length)) {
400
+ for (const item of documents) {
401
+ if (item.next >= item.document.text.length) continue;
402
+ const chunk = passage(item.document, item.next);
403
+ passages.push(chunk.value);
404
+ item.next = chunk.next;
405
+ }
406
+ }
407
+ passagesQueued += passages.length;
408
+ onProgress?.({
409
+ phase: "passage-queue",
410
+ completed: passagesQueued,
411
+ detail: `batch ${String(batchIndex + 1)} of ${String(fileBatches.length)}`,
412
+ });
413
+ if (!passages.length) continue;
414
+ // oxlint-disable-next-line no-await-in-loop -- one owned inference batch runs at a time and feeds one global ranking.
415
+ const inferred = await infer(query, passages, access.signal, onProgress);
416
+ // Literal discovery and this first Concept batch start together. Resolve the literal file set
417
+ // only when this batch is ready to release its documents, then retain matching documents only.
418
+ // oxlint-disable-next-line no-await-in-loop -- every sequential batch reuses the same settled literal path promise.
419
+ const retainedPathSet = retainPaths === undefined ? undefined : await retainPaths;
420
+ for (const document of generation.documents) {
421
+ if (retainedPathSet?.has(resolve(access.cwd, document.path))) {
422
+ retainedDocuments.push(document);
423
+ }
424
+ }
425
+ result.reasons.push(...inferred.warnings);
426
+ allScores.push(...inferred.scores);
427
+ conceptWindowsRanked += inferred.windowsRanked;
428
+ conceptCacheHits += inferred.cacheHits;
429
+ conceptCacheMisses += inferred.cacheMisses;
430
+ conceptCacheMaxBytes = Math.max(conceptCacheMaxBytes, inferred.cacheMaxBytes);
431
+ conceptCacheBytes = inferred.cacheBytes ?? conceptCacheBytes;
432
+ inferencePeakRssBytes = Math.max(inferencePeakRssBytes, inferred.peakRssBytes);
433
+ const batchItems = passages.map((item, index) => {
434
+ const similarity = inferred.scores[index];
435
+ if (similarity === undefined) throw new Error("Missing concept similarity");
436
+ const evidence = rangeEvidence(item.document, item.range);
437
+ return {
438
+ path: item.document.path,
439
+ line: item.document.lineAt(item.range.start),
440
+ source: item.document.reference,
441
+ range: item.range,
442
+ label: `Concept candidate (cosine ${similarity.toFixed(4)})`,
443
+ excerpt: evidence.excerpt,
444
+ details: {
445
+ kind: "concept-candidate",
446
+ certainty: "candidate",
447
+ score: similarity,
448
+ rankingReason:
449
+ "local multilingual E5 cosine similarity; relevance candidate, no binding or execution claim",
450
+ model: CONCEPT_MODEL,
451
+ revision: CONCEPT_REVISION,
452
+ tokenTruncated: false,
453
+ excerptRange: evidence.excerptRange,
454
+ excerptTruncated: evidence.excerptTruncated,
455
+ },
456
+ } satisfies AnalysisResultSet["items"][number];
457
+ });
458
+ const ranked = [...result.items, ...batchItems].toSorted(
459
+ (a, b) =>
460
+ Number(b.details?.score) - Number(a.details?.score) ||
461
+ a.path.localeCompare(b.path) ||
462
+ a.line - b.line,
463
+ );
464
+ if (ranked.length > MAX_ANALYSIS_RESULTS) retentionTruncated = true;
465
+ result.items = ranked.slice(0, MAX_ANALYSIS_RESULTS);
466
+ }
467
+ if (retentionTruncated) {
468
+ result.partial = true;
469
+ result.reasons.push(
470
+ `Concept ranking retained the top ${String(MAX_ANALYSIS_RESULTS)} candidates from ${String(passagesQueued)} passages`,
471
+ );
472
+ }
473
+ const sourceGeneration: ConceptSourceGeneration = {
474
+ cwd: access.cwd,
475
+ filters,
476
+ files,
477
+ inventory,
478
+ documents: retainedDocuments,
479
+ partial: sourcePartial,
480
+ reasons: [...new Set(sourceReasons)],
481
+ filesSkippedEmpty,
482
+ filesSkippedBinary,
483
+ filesUnavailable,
484
+ filesAdmitted,
485
+ batches: fileBatches.length,
486
+ batchFileLimit: MAX_STRUCTURE_FILES,
487
+ fileLimit: access.maxFiles,
488
+ startedAt: generationStartedAt,
489
+ inventoryHash: createHash("sha256")
490
+ .update(JSON.stringify(inventory))
491
+ .digest("hex")
492
+ .slice(0, 32),
493
+ };
494
+ result.counts = {
495
+ filesEnumerated: files.paths.length,
496
+ filesAdmitted,
497
+ filesSkippedEmpty,
498
+ filesSkippedBinary,
499
+ filesUnavailable,
500
+ passagesQueued,
501
+ batchesPlanned: fileBatches.length,
502
+ batchesCompleted: fileBatches.length,
503
+ batchFileLimit: MAX_STRUCTURE_FILES,
504
+ };
505
+ if (allScores.length) {
506
+ result.stats = {
507
+ inferencePeakRssBytes,
508
+ passagesRanked: passagesQueued,
509
+ conceptWindowsRanked,
510
+ conceptCacheHits,
511
+ conceptCacheMisses,
512
+ conceptCacheMaxBytes,
513
+ ...(conceptCacheBytes === undefined ? {} : { conceptCacheBytes }),
514
+ scoreProfile: scoreProfile(allScores),
515
+ };
516
+ }
517
+ result.filesRead = filesRead;
518
+ result.bytesRead = bytesRead;
519
+ result.stats = {
520
+ ...result.stats,
521
+ elapsedMs: Math.round(performance.now() - started),
522
+ filesEnumerated: files.paths.length,
523
+ filesAdmitted,
524
+ };
525
+ result.coverage = {
526
+ conceptCandidates: result.partial ? "partial" : "complete",
527
+ admissionPlan: sourcePartial ? "partial" : "complete",
528
+ retention: retentionTruncated ? "partial" : "complete",
529
+ compilerBindings: "not-applicable",
530
+ };
531
+ result.scope = {
532
+ path: request.path ?? ".",
533
+ requestedPath: request.path ?? ".",
534
+ glob: request.glob,
535
+ exclude: request.exclude,
536
+ hidden: request.hidden,
537
+ ignorePolicy: request.ignorePolicy ?? "respect",
538
+ expandedToProjectRoot: false,
539
+ assertion: request.path && request.path !== "." ? "requested-scope" : "project-wide",
540
+ };
541
+ if (options.verifySourceGeneration !== false) {
542
+ await verifyConceptSourceGeneration(sourceGeneration, access);
543
+ }
544
+ result.sourceGeneration = conceptSourceSummary(sourceGeneration);
545
+ return { analysis: result, sourceGeneration };
546
+ }
547
+
548
+ export function conceptSearch(
549
+ input: SiftLightInput,
550
+ access: SourceAccess,
551
+ onProgress?: (progress: OperationProgress) => void,
552
+ options?: ConceptSearchOptions,
553
+ ): Promise<ConceptSearchExecution> {
554
+ return runConceptSearchQueued(input, access, similarities, onProgress, options);
555
+ }
556
+
557
+ export type ConceptSearchRunner = typeof conceptSearch;
558
+
559
+ async function runConceptSearchQueued(
560
+ input: SiftLightInput,
561
+ access: SourceAccess,
562
+ infer: ConceptInferenceRunner,
563
+ onProgress?: (progress: OperationProgress) => void,
564
+ options?: ConceptSearchOptions,
565
+ ): Promise<ConceptSearchExecution> {
566
+ return inferenceQueue
567
+ .run(() => runConceptSearch(input, access, infer, onProgress, options), access.signal)
568
+ .catch((error: unknown) => {
569
+ if (access.signal?.aborted) throw abortError();
570
+ throw error;
571
+ });
572
+ }
573
+
574
+ export function createConceptSearchRunner(infer: ConceptInferenceRunner): ConceptSearchRunner {
575
+ return (input, access, onProgress, options) =>
576
+ runConceptSearchQueued(input, access, infer, onProgress, options);
577
+ }
@@ -0,0 +1,75 @@
1
+ import { mkdir, mkdtemp, open, rename, rm, stat } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import {
4
+ CONCEPT_ASSETS,
5
+ CONCEPT_MODEL,
6
+ CONCEPT_REVISION,
7
+ conceptModelDirectory,
8
+ verifyConceptModel,
9
+ } from "./concept-model.js";
10
+
11
+ /** Explicit asset installation only. Never called by a search request. */
12
+ export async function installConceptModel(): Promise<void> {
13
+ const destination = conceptModelDirectory();
14
+ try {
15
+ const existing = await stat(destination);
16
+ if (!existing.isDirectory()) throw new Error("Concept model destination is not a directory");
17
+ await verifyConceptModel(destination);
18
+ process.stderr.write(`Concept model is already verified: ${destination}\n`);
19
+ return;
20
+ } catch (error) {
21
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
22
+ }
23
+ await mkdir(dirname(destination), { recursive: true });
24
+ const stage = await mkdtemp(join(dirname(destination), ".install-"));
25
+ const signal = AbortSignal.timeout(10 * 60_000);
26
+ try {
27
+ for (const asset of CONCEPT_ASSETS) {
28
+ process.stderr.write(
29
+ `Downloading pinned concept asset: ${asset.path} (${String(asset.bytes)} bytes)\n`,
30
+ );
31
+ // oxlint-disable-next-line no-await-in-loop -- sequential streaming downloads bound memory and share one deadline.
32
+ const response = await fetch(
33
+ `https://huggingface.co/${CONCEPT_MODEL}/resolve/${CONCEPT_REVISION}/${asset.path}`,
34
+ { signal },
35
+ );
36
+ if (!response.ok || !response.body)
37
+ throw new Error(`Model download failed: ${String(response.status)} ${asset.path}`);
38
+ const path = join(stage, asset.path);
39
+ // oxlint-disable-next-line no-await-in-loop -- prepare only this staging asset.
40
+ await mkdir(dirname(path), { recursive: true });
41
+ // oxlint-disable-next-line no-await-in-loop -- exclusive staging file, never overwrite a installed model.
42
+ const file = await open(path, "wx");
43
+ let size = 0;
44
+ try {
45
+ // oxlint-disable-next-line no-await-in-loop -- stream one bounded asset at a time.
46
+ for await (const chunk of response.body) {
47
+ size += chunk.byteLength;
48
+ if (size > asset.bytes) throw new Error(`Model asset exceeds pinned size: ${asset.path}`);
49
+ await file.writeFile(chunk);
50
+ }
51
+ // oxlint-disable-next-line no-await-in-loop -- flush completed staged asset before atomic installation.
52
+ await file.sync();
53
+ } finally {
54
+ // oxlint-disable-next-line no-await-in-loop -- always release this owned staging descriptor.
55
+ await file.close();
56
+ }
57
+ }
58
+ await verifyConceptModel(stage);
59
+ try {
60
+ await rename(stage, destination);
61
+ } catch (error) {
62
+ if (
63
+ !(error instanceof Error) ||
64
+ !("code" in error) ||
65
+ !["EEXIST", "ENOTEMPTY"].includes(String(error.code))
66
+ )
67
+ throw error;
68
+ // A concurrent successful installer may have won the atomic directory rename.
69
+ await verifyConceptModel(destination);
70
+ }
71
+ process.stderr.write(`Concept model verified and installed: ${destination}\n`);
72
+ } finally {
73
+ await rm(stage, { recursive: true, force: true });
74
+ }
75
+ }