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,344 @@
1
+ import { resolve } from "node:path";
2
+ import {
3
+ MAX_ANALYSIS_RESULTS,
4
+ MAX_STRUCTURE_BYTES,
5
+ MAX_STRUCTURE_FILES,
6
+ } from "./analysis-limits.js";
7
+ import { consumeCappedLines } from "./capped-lines.js";
8
+ import { abortError, SiftLightError } from "./errors.js";
9
+ import { readGitChanges, type GitChangeRequest } from "./git-source.js";
10
+ import { filterHistoricalPaths } from "./historical-paths.js";
11
+ import { runOwnedProcess } from "./owned-process.js";
12
+ import { isPathInsideCwd } from "./path-policy.js";
13
+ import { patternArguments, type RipgrepRunner } from "./rg.js";
14
+ import { resolveRipgrepExecutable } from "./ripgrep-executable.js";
15
+ import { sameSourceRevision } from "./source.js";
16
+ import { SourceBudgetError } from "./source-access.js";
17
+ import {
18
+ SourceDocument,
19
+ SourceDocumentError,
20
+ type ByteRange,
21
+ type SourceReference,
22
+ } from "./source-document.js";
23
+ import { MAX_PROTOCOL_LINE_BYTES, type SearchRequest } from "./types.js";
24
+
25
+ export interface EvidenceCandidateFile {
26
+ document: SourceDocument;
27
+ occurrences: ByteRange[];
28
+ changedRanges?: ByteRange[];
29
+ change?: string;
30
+ }
31
+
32
+ export interface EvidenceCandidateOptions {
33
+ request: SearchRequest;
34
+ changes?: GitChangeRequest;
35
+ cwd: string;
36
+ signal?: AbortSignal;
37
+ access: { load(path: string, expected?: SourceReference): Promise<SourceDocument> };
38
+ runRipgrep: RipgrepRunner;
39
+ maxFiles?: number;
40
+ }
41
+
42
+ export interface EvidenceCandidates {
43
+ files: EvidenceCandidateFile[];
44
+ partial: boolean;
45
+ reasons: string[];
46
+ filesRead: number;
47
+ bytesRead: number;
48
+ changes?: { base: string; target: string; scope: string; side: string };
49
+ }
50
+
51
+ interface MatchBudget {
52
+ retained: number;
53
+ protocolBytes: number;
54
+ }
55
+ class CandidateLimit extends SiftLightError {}
56
+
57
+ function record(value: unknown): value is Record<string, unknown> {
58
+ return typeof value === "object" && value !== null;
59
+ }
60
+
61
+ function integer(value: unknown): value is number {
62
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
63
+ }
64
+
65
+ function eventBytes(value: unknown): Buffer {
66
+ if (record(value) && typeof value.text === "string") return Buffer.from(value.text);
67
+ if (record(value) && typeof value.bytes === "string") return Buffer.from(value.bytes, "base64");
68
+ throw new SiftLightError("Raw ripgrep event omitted source bytes");
69
+ }
70
+
71
+ /** Half-open source ranges never attribute the start of an unchanged next line to a changed line. */
72
+ export function occurrenceInsideRanges(
73
+ range: ByteRange,
74
+ allowed: ByteRange[],
75
+ document: SourceDocument,
76
+ ): boolean {
77
+ return allowed.some(
78
+ (outer) =>
79
+ range.start >= outer.start &&
80
+ range.end <= outer.end &&
81
+ (range.end > range.start ||
82
+ range.start < outer.end ||
83
+ (outer.end === document.bytes.length && document.bytes.at(-1) !== 10)),
84
+ );
85
+ }
86
+
87
+ async function searchRawSource(
88
+ cwd: string,
89
+ document: SourceDocument,
90
+ request: SearchRequest,
91
+ budget: MatchBudget,
92
+ allowed: ByteRange[] | undefined,
93
+ signal?: AbortSignal,
94
+ ): Promise<{ occurrences: ByteRange[]; reason?: string }> {
95
+ const occurrences: ByteRange[] = [];
96
+ try {
97
+ const result = await runOwnedProcess(
98
+ {
99
+ executable: await resolveRipgrepExecutable(),
100
+ args: [
101
+ "--no-config",
102
+ "--encoding",
103
+ "none",
104
+ "--json",
105
+ "--line-number",
106
+ "--color=never",
107
+ ...patternArguments(request),
108
+ "--",
109
+ request.pattern,
110
+ "-",
111
+ ],
112
+ cwd,
113
+ input: document.bytes,
114
+ ...(signal ? { signal } : {}),
115
+ },
116
+ (stdout) =>
117
+ consumeCappedLines(
118
+ stdout,
119
+ (line) => {
120
+ budget.protocolBytes += Buffer.byteLength(line);
121
+ if (budget.protocolBytes > MAX_STRUCTURE_BYTES)
122
+ throw new CandidateLimit("Raw candidate matching reached the 32 MiB protocol budget");
123
+ let event: unknown;
124
+ try {
125
+ event = JSON.parse(line);
126
+ } catch (error) {
127
+ throw new SiftLightError("Invalid raw ripgrep JSON", { cause: error });
128
+ }
129
+ if (!record(event) || event.type !== "match") return;
130
+ const data = event.data;
131
+ if (
132
+ !record(data) ||
133
+ !integer(data.absolute_offset) ||
134
+ !integer(data.line_number) ||
135
+ data.line_number < 1 ||
136
+ !Array.isArray(data.submatches)
137
+ )
138
+ throw new SiftLightError("Invalid raw ripgrep match event");
139
+ const start = data.absolute_offset;
140
+ const bytes = eventBytes(data.lines);
141
+ if (
142
+ document.lineStarts[data.line_number - 1] !== start ||
143
+ start + bytes.length > document.bytes.length ||
144
+ !document.bytes.subarray(start, start + bytes.length).equals(bytes)
145
+ )
146
+ throw new SiftLightError(
147
+ "Raw ripgrep evidence does not match its source version and line offset",
148
+ );
149
+ for (const submatch of data.submatches) {
150
+ if (
151
+ !record(submatch) ||
152
+ !integer(submatch.start) ||
153
+ !integer(submatch.end) ||
154
+ submatch.end < submatch.start ||
155
+ submatch.end > bytes.length ||
156
+ !bytes.subarray(submatch.start, submatch.end).equals(eventBytes(submatch.match))
157
+ )
158
+ throw new SiftLightError("Invalid raw ripgrep occurrence bounds or bytes");
159
+ const range = { start: start + submatch.start, end: start + submatch.end };
160
+ if (allowed && !occurrenceInsideRanges(range, allowed, document)) continue;
161
+ if (budget.retained >= MAX_ANALYSIS_RESULTS)
162
+ throw new CandidateLimit(
163
+ `Candidate matching reached the ${String(MAX_ANALYSIS_RESULTS)} occurrence limit`,
164
+ );
165
+ budget.retained += 1;
166
+ occurrences.push(range);
167
+ }
168
+ },
169
+ { maxLineBytes: MAX_PROTOCOL_LINE_BYTES },
170
+ ),
171
+ );
172
+ if (result.code !== 0 && result.code !== 1)
173
+ throw new SiftLightError(result.stderr.trim() || `Raw ripgrep exited ${String(result.code)}`);
174
+ } catch (error) {
175
+ if (!(error instanceof CandidateLimit)) throw error;
176
+ return { occurrences, reason: error.message };
177
+ }
178
+ return { occurrences };
179
+ }
180
+
181
+ /* oxlint-disable no-await-in-loop -- retained files share one source and occurrence budget. */
182
+ async function ordinaryCandidates(options: EvidenceCandidateOptions): Promise<EvidenceCandidates> {
183
+ const scan = await options.runRipgrep(options.request, options.cwd, options.signal);
184
+ if (options.signal?.aborted) throw abortError();
185
+ const reasons = new Set<string>();
186
+ if (!scan.snapshotComplete) {
187
+ reasons.add("Search retention is partial; only retained matching files can be analyzed");
188
+ for (const reason of scan.retention?.reasons ?? []) reasons.add(reason);
189
+ }
190
+ const grouped = new Map<string, typeof scan.matches>();
191
+ for (const match of scan.matches) {
192
+ const existing = grouped.get(match.absolutePath);
193
+ if (existing) existing.push(match);
194
+ else grouped.set(match.absolutePath, [match]);
195
+ }
196
+ const files: EvidenceCandidateFile[] = [];
197
+ let filesRead = 0;
198
+ let bytesRead = 0;
199
+ let retained = 0;
200
+ const maxFiles = options.maxFiles ?? MAX_STRUCTURE_FILES;
201
+ for (const [absolute, matches] of grouped) {
202
+ if (options.signal?.aborted) throw abortError();
203
+ const revision = scan.sourceRevisions.get(absolute);
204
+ if (!revision) {
205
+ reasons.add("Some matching files lack a verified search revision");
206
+ continue;
207
+ }
208
+ if (filesRead >= maxFiles || bytesRead + revision.size > MAX_STRUCTURE_BYTES) {
209
+ reasons.add(`Candidate analysis reached the ${String(maxFiles)}-file / 32 MiB source limit`);
210
+ continue;
211
+ }
212
+ filesRead += 1;
213
+ let document: SourceDocument;
214
+ try {
215
+ document = await options.access.load(absolute);
216
+ } catch (error) {
217
+ if (error instanceof SourceDocumentError || error instanceof SourceBudgetError) {
218
+ reasons.add(error.message);
219
+ continue;
220
+ }
221
+ throw error;
222
+ }
223
+ bytesRead += document.bytes.length;
224
+ if (
225
+ document.reference.origin.kind !== "worktree" ||
226
+ !sameSourceRevision(revision, document.reference.origin.revision)
227
+ ) {
228
+ reasons.add(`Source changed since search: ${document.path}`);
229
+ continue;
230
+ }
231
+ if (
232
+ (document.bytes[0] === 255 && document.bytes[1] === 254) ||
233
+ (document.bytes[0] === 254 && document.bytes[1] === 255)
234
+ ) {
235
+ reasons.add(
236
+ `Transcoded search offsets cannot be bound to raw UTF-16 source: ${document.path}`,
237
+ );
238
+ continue;
239
+ }
240
+ const utf8Bom = document.bytes.subarray(0, 3).equals(Buffer.from([239, 187, 191]));
241
+ const occurrences: ByteRange[] = [];
242
+ for (const match of matches) {
243
+ const lineStart = document.lineStarts[match.lineNumber - 1];
244
+ if (lineStart === undefined)
245
+ throw new SiftLightError("Retained match line is outside its verified source");
246
+ const base = lineStart + (utf8Bom && match.lineNumber === 1 ? 3 : 0);
247
+ const lineEnd = document.lineStarts[match.lineNumber] ?? document.bytes.length;
248
+ if (match.occurrences.length === 0)
249
+ reasons.add("Some retained matches have no exact occurrence ranges");
250
+ for (const occurrence of match.occurrences) {
251
+ const range = { start: base + occurrence.byteStart, end: base + occurrence.byteEnd };
252
+ document.checkRange(range);
253
+ if (range.end > lineEnd)
254
+ throw new SiftLightError("Retained occurrence extends beyond its verified source line");
255
+ if (retained >= MAX_ANALYSIS_RESULTS) {
256
+ reasons.add(
257
+ `Candidate matching reached the ${String(MAX_ANALYSIS_RESULTS)} occurrence limit`,
258
+ );
259
+ break;
260
+ }
261
+ retained += 1;
262
+ occurrences.push(range);
263
+ }
264
+ }
265
+ if (occurrences.length > 0) files.push({ document, occurrences });
266
+ }
267
+ return { files, partial: reasons.size > 0, reasons: [...reasons], filesRead, bytesRead };
268
+ }
269
+ /* oxlint-enable no-await-in-loop */
270
+
271
+ /** One candidate boundary for ordinary retained searches and fixed Git source versions. */
272
+ /* oxlint-disable no-await-in-loop -- Git sources consume one cumulative rg protocol and occurrence budget. */
273
+ export async function collectEvidenceCandidates(
274
+ options: EvidenceCandidateOptions,
275
+ ): Promise<EvidenceCandidates> {
276
+ if (!options.changes) return ordinaryCandidates(options);
277
+ if (
278
+ options.request.path &&
279
+ !isPathInsideCwd(resolve(options.cwd, options.request.path), options.cwd)
280
+ ) {
281
+ throw new SiftLightError(
282
+ "Git changes for paths outside cwd are not supported; relaunch Pi from that repository or a common parent",
283
+ );
284
+ }
285
+ const reasons = new Set<string>();
286
+ const result = await readGitChanges(options.cwd, options.changes, options.signal, {
287
+ filterPaths: async (paths) => {
288
+ const selected = await filterHistoricalPaths(
289
+ options.cwd,
290
+ paths,
291
+ options.request,
292
+ options.signal,
293
+ );
294
+ for (const reason of selected.reasons) reasons.add(reason);
295
+ return { paths: selected.paths, bytesRead: selected.ignoreBytesRead };
296
+ },
297
+ });
298
+ if (options.signal?.aborted) throw abortError();
299
+ for (const reason of result.reasons) reasons.add(reason);
300
+ const files: EvidenceCandidateFile[] = [];
301
+ const budget: MatchBudget = { retained: 0, protocolBytes: 0 };
302
+ for (const file of result.files) {
303
+ if (options.signal?.aborted) throw abortError();
304
+ if (!file.content || !file.origin) {
305
+ if (file.sourceStatus !== "absent")
306
+ reasons.add(`${file.path}: ${file.reason ?? file.sourceStatus}`);
307
+ continue;
308
+ }
309
+ const document = new SourceDocument({ path: file.path, origin: file.origin }, file.content);
310
+ const changedRanges = file.changedRanges.map((range) =>
311
+ document.lineRange(range.startLine, range.endLine),
312
+ );
313
+ if (options.changes.scope === "lines" && changedRanges.length === 0) continue;
314
+ const matched = await searchRawSource(
315
+ options.cwd,
316
+ document,
317
+ options.request,
318
+ budget,
319
+ options.changes.scope === "lines" ? changedRanges : undefined,
320
+ options.signal,
321
+ );
322
+ if (matched.reason) reasons.add(matched.reason);
323
+ if (matched.occurrences.length > 0)
324
+ files.push({
325
+ document,
326
+ occurrences: matched.occurrences,
327
+ changedRanges,
328
+ change: file.change,
329
+ });
330
+ if (matched.reason) {
331
+ reasons.add("Remaining Git candidate files were not searched after the matching limit");
332
+ break;
333
+ }
334
+ }
335
+ return {
336
+ files,
337
+ partial: reasons.size > 0,
338
+ reasons: [...reasons],
339
+ filesRead: result.filesRead,
340
+ bytesRead: result.bytesRead,
341
+ changes: { base: result.base, target: result.target, scope: result.scope, side: result.side },
342
+ };
343
+ }
344
+ /* oxlint-enable no-await-in-loop */
@@ -0,0 +1,48 @@
1
+ import type { AnalysisItem } from "./analysis-types.js";
2
+
3
+ /** Preserve evidence tiers; within a tier surface one location per file before repeating a file. */
4
+ export function rankEvidence(
5
+ items: readonly AnalysisItem[],
6
+ priority: (item: AnalysisItem) => number,
7
+ ): AnalysisItem[] {
8
+ const tiers = new Map<number, Map<string, AnalysisItem[]>>();
9
+ const ordered = items.toSorted(
10
+ (a, b) =>
11
+ priority(a) - priority(b) ||
12
+ a.path.localeCompare(b.path) ||
13
+ a.line - b.line ||
14
+ (a.range?.start ?? 0) - (b.range?.start ?? 0),
15
+ );
16
+ for (const item of ordered) {
17
+ const score = priority(item);
18
+ let files = tiers.get(score);
19
+ if (!files) {
20
+ files = new Map();
21
+ tiers.set(score, files);
22
+ }
23
+ let entries = files.get(item.path);
24
+ if (!entries) {
25
+ entries = [];
26
+ files.set(item.path, entries);
27
+ }
28
+ entries.push(item);
29
+ }
30
+ const result: AnalysisItem[] = [];
31
+ for (const files of tiers.values()) {
32
+ const groups = [...files.values()];
33
+ const depth = Math.max(0, ...groups.map((group) => group.length));
34
+ for (let index = 0; index < depth; index += 1)
35
+ for (const group of groups) {
36
+ const item = group[index];
37
+ if (item)
38
+ result.push({
39
+ ...item,
40
+ details: {
41
+ ...item.details,
42
+ rankingOrder: "evidence tier, then round-robin files, then source position",
43
+ },
44
+ });
45
+ }
46
+ }
47
+ return result;
48
+ }