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,340 @@
1
+ import { DEFAULT_HYBRID_CONCEPT_LIMIT, MAX_HYBRID_CONCEPT_LIMIT } from "./analysis-limits.js";
2
+ import { resolve } from "node:path";
3
+ import type { AnalysisItem, AnalysisResultSet, CoverageStatus } from "./analysis-types.js";
4
+ import type { ConceptSearchExecution } from "./concept-search.js";
5
+ import {
6
+ conceptSourceSummary,
7
+ ConceptSourceChangedError,
8
+ verifyConceptSourceGeneration,
9
+ } from "./concept-source-generation.js";
10
+ import { SiftLightError } from "./errors.js";
11
+ import { SourceAccess, SourceBudgetError } from "./source-access.js";
12
+ import { SourceDocumentError, type ByteRange, type SourceDocument } from "./source-document.js";
13
+ import { sameSourceRevision } from "./source.js";
14
+ import type { SearchScan } from "./types.js";
15
+ import { applySemanticJudge, type SemanticJudgeIntegration } from "./semantic-judge.js";
16
+
17
+ export class HybridSourceChangedError extends ConceptSourceChangedError {
18
+ constructor(
19
+ message = "Hybrid source changed while exact and concept evidence were being merged",
20
+ ) {
21
+ super(message);
22
+ this.name = "HybridSourceChangedError";
23
+ }
24
+ }
25
+
26
+ export function sameHybridLiteralScan(left: SearchScan, right: SearchScan): boolean {
27
+ if (
28
+ left.totalMatches !== right.totalMatches ||
29
+ left.snapshotComplete !== right.snapshotComplete ||
30
+ left.fileCounts.size !== right.fileCounts.size
31
+ )
32
+ return false;
33
+
34
+ // A scan can retain file counts without retaining source revisions. Build the
35
+ // lookup once so a large exact-result page does not repeatedly scan matches
36
+ // while comparing its admitted inventory.
37
+ const leftMatchesByDisplayPath = new Map<string, SearchScan["matches"][number]>();
38
+ for (const match of left.matches) {
39
+ if (!leftMatchesByDisplayPath.has(match.displayPath)) {
40
+ leftMatchesByDisplayPath.set(match.displayPath, match);
41
+ }
42
+ }
43
+ const rightMatchesByDisplayPath = new Map<string, SearchScan["matches"][number]>();
44
+ for (const match of right.matches) {
45
+ if (!rightMatchesByDisplayPath.has(match.displayPath)) {
46
+ rightMatchesByDisplayPath.set(match.displayPath, match);
47
+ }
48
+ }
49
+
50
+ for (const [path, count] of left.fileCounts) {
51
+ if (right.fileCounts.get(path) !== count) return false;
52
+ const leftMatch = leftMatchesByDisplayPath.get(path);
53
+ const rightMatch = rightMatchesByDisplayPath.get(path);
54
+ if (leftMatch?.absolutePath !== rightMatch?.absolutePath) return false;
55
+ if (!leftMatch || !rightMatch) continue;
56
+ const leftRevision = left.sourceRevisions.get(leftMatch.absolutePath);
57
+ const rightRevision = right.sourceRevisions.get(rightMatch.absolutePath);
58
+ // Missing revision metadata is unknown coverage, not evidence that the
59
+ // source changed. literalEvidence carries that uncertainty as partial.
60
+ if (leftRevision === undefined || rightRevision === undefined) continue;
61
+ if (!sameSourceRevision(leftRevision, rightRevision)) return false;
62
+ }
63
+ return true;
64
+ }
65
+
66
+ function rangesOverlap(left: ByteRange, right: ByteRange): boolean {
67
+ return left.start < right.end && right.start < left.end;
68
+ }
69
+
70
+ export function hybridConceptLimit(value: number | undefined): number {
71
+ const candidate = value ?? DEFAULT_HYBRID_CONCEPT_LIMIT;
72
+ if (!Number.isSafeInteger(candidate) || candidate < 1 || candidate > MAX_HYBRID_CONCEPT_LIMIT) {
73
+ throw new SiftLightError(
74
+ `conceptLimit must be an integer from 1 through ${String(MAX_HYBRID_CONCEPT_LIMIT)}`,
75
+ );
76
+ }
77
+ return candidate;
78
+ }
79
+
80
+ function absoluteOccurrenceRanges(
81
+ document: SourceDocument,
82
+ line: number,
83
+ match: SearchScan["matches"][number],
84
+ ): ByteRange[] {
85
+ const lineRange = document.lineRange(line);
86
+ return match.occurrences.map((occurrence) => ({
87
+ start: lineRange.start + occurrence.byteStart,
88
+ end: lineRange.start + occurrence.byteEnd,
89
+ }));
90
+ }
91
+
92
+ async function literalEvidence(
93
+ scan: SearchScan,
94
+ access: SourceAccess,
95
+ generation: ConceptSearchExecution["sourceGeneration"],
96
+ ): Promise<{
97
+ items: AnalysisItem[];
98
+ rangesByPath: Map<string, ByteRange[]>;
99
+ sourceCoverage: CoverageStatus;
100
+ reasons: string[];
101
+ reusedDocuments: number;
102
+ loadedDocuments: number;
103
+ }> {
104
+ const documents = new Map<string, SourceDocument>();
105
+ const unavailable = new Map<string, string>();
106
+ const generatedDocuments = new Map(
107
+ generation.documents.map((document) => [resolve(access.cwd, document.path), document]),
108
+ );
109
+ let reusedDocuments = 0;
110
+ let loadedDocuments = 0;
111
+ for (const match of scan.matches) {
112
+ if (documents.has(match.absolutePath) || unavailable.has(match.absolutePath)) continue;
113
+ try {
114
+ const generated = generatedDocuments.get(resolve(access.cwd, match.absolutePath));
115
+ // Reuse the document admitted by the unified source generation; only unmatched literal
116
+ // files need a bounded inspection read.
117
+ // oxlint-disable-next-line no-await-in-loop -- source reads share one bounded access budget.
118
+ const document = generated ?? (await access.load(match.absolutePath));
119
+ if (generated) reusedDocuments += 1;
120
+ else loadedDocuments += 1;
121
+ const expected = scan.sourceRevisions.get(match.absolutePath);
122
+ if (!expected) {
123
+ unavailable.set(match.absolutePath, "source revision metadata was unavailable");
124
+ continue;
125
+ }
126
+ if (document.reference.origin.kind !== "worktree") {
127
+ unavailable.set(match.absolutePath, "source revision origin was unavailable");
128
+ continue;
129
+ }
130
+ if (!sameSourceRevision(expected, document.reference.origin.revision)) {
131
+ throw new HybridSourceChangedError(
132
+ `${match.displayPath}: source revision changed while preparing hybrid evidence`,
133
+ );
134
+ }
135
+ documents.set(match.absolutePath, document);
136
+ } catch (error) {
137
+ if (error instanceof SourceDocumentError && error.reason === "source-changed")
138
+ throw new HybridSourceChangedError(`${match.displayPath}: ${error.message}`);
139
+ if (error instanceof SourceBudgetError || error instanceof SourceDocumentError) {
140
+ unavailable.set(match.absolutePath, error.message);
141
+ continue;
142
+ }
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ const rangesByPath = new Map<string, ByteRange[]>();
148
+ const items = scan.matches.map((match): AnalysisItem => {
149
+ const document = documents.get(match.absolutePath);
150
+ const path = document?.path ?? match.displayPath;
151
+ const ranges = document ? absoluteOccurrenceRanges(document, match.lineNumber, match) : [];
152
+ if (ranges.length) {
153
+ const existing = rangesByPath.get(path) ?? [];
154
+ existing.push(...ranges);
155
+ rangesByPath.set(path, existing);
156
+ }
157
+ const primary = ranges[0];
158
+ return {
159
+ path,
160
+ line: match.lineNumber,
161
+ label: `Literal exact match (${String(match.occurrences.length)} occurrence${match.occurrences.length === 1 ? "" : "s"})${document && primary ? "" : "; source inspection unavailable"}`,
162
+ excerpt: match.lineContent,
163
+ ...(document && primary ? { source: document.reference, range: primary } : {}),
164
+ details: {
165
+ kind: "literal-match",
166
+ source: "literal",
167
+ certainty: "exact",
168
+ sourceVerified: Boolean(document && primary),
169
+ occurrenceCount: match.occurrences.length,
170
+ ranges,
171
+ lineContentTruncated: match.lineTruncated,
172
+ },
173
+ };
174
+ });
175
+ const reasons = [...new Set(unavailable.values())].map(
176
+ (reason) => `Literal source inspection is unavailable for retained evidence: ${reason}`,
177
+ );
178
+ return {
179
+ items,
180
+ rangesByPath,
181
+ sourceCoverage: unavailable.size ? "partial" : "complete",
182
+ reasons,
183
+ reusedDocuments,
184
+ loadedDocuments,
185
+ };
186
+ }
187
+
188
+ function isLiteralOverlap(item: AnalysisItem, rangesByPath: Map<string, ByteRange[]>): boolean {
189
+ const itemRange = item.range;
190
+ if (!itemRange) return false;
191
+ return (rangesByPath.get(item.path) ?? []).some((range) => rangesOverlap(range, itemRange));
192
+ }
193
+
194
+ export async function combineHybridSearch(
195
+ scan: SearchScan,
196
+ execution: ConceptSearchExecution,
197
+ access: SourceAccess,
198
+ conceptLimit: number,
199
+ query: string,
200
+ semanticJudge?: SemanticJudgeIntegration,
201
+ signal?: AbortSignal,
202
+ ): Promise<AnalysisResultSet> {
203
+ const concept = execution.analysis;
204
+ if (concept.kind !== "concept") throw new Error("Hybrid search requires concept evidence");
205
+ await verifyConceptSourceGeneration(execution.sourceGeneration, access);
206
+ const literal = await literalEvidence(scan, access, execution.sourceGeneration);
207
+ // Concept passages deliberately overlap at chunk boundaries. Deduplicate in
208
+ // rank order before applying the supplement limit, keeping the best evidence.
209
+ const retainedRanges = new Map<string, ByteRange[]>();
210
+ const eligibleConcept = concept.items.filter((item) => {
211
+ if (isLiteralOverlap(item, literal.rangesByPath)) return false;
212
+ const range = item.range;
213
+ if (!range) return true;
214
+ const ranges = retainedRanges.get(item.path) ?? [];
215
+ let low = 0;
216
+ let high = ranges.length;
217
+ while (low < high) {
218
+ const middle = Math.floor((low + high) / 2);
219
+ if (ranges[middle]!.start < range.start) low = middle + 1;
220
+ else high = middle;
221
+ }
222
+ const previous = ranges[low - 1];
223
+ const next = ranges[low];
224
+ if ((previous && rangesOverlap(previous, range)) || (next && rangesOverlap(next, range)))
225
+ return false;
226
+ ranges.splice(low, 0, range);
227
+ retainedRanges.set(item.path, ranges);
228
+ return true;
229
+ });
230
+ const duplicateConceptCandidates = concept.items.length - eligibleConcept.length;
231
+ const judgedConcept = await applySemanticJudge(
232
+ {
233
+ kind: "hybrid",
234
+ unit: "evidence-items",
235
+ items: eligibleConcept,
236
+ partial: false,
237
+ reasons: [],
238
+ },
239
+ query,
240
+ semanticJudge,
241
+ signal,
242
+ );
243
+ const selectedConcept = judgedConcept.items.slice(0, conceptLimit).map((item) =>
244
+ Object.assign({}, item, {
245
+ details: Object.assign({}, item.details, { source: "concept" }),
246
+ }),
247
+ );
248
+ const conceptCandidatesOmitted = Math.max(0, eligibleConcept.length - selectedConcept.length);
249
+ const literalOccurrencesRetained = scan.matches.reduce(
250
+ (total, match) => total + match.occurrences.length,
251
+ 0,
252
+ );
253
+ const literalCoverage: CoverageStatus = scan.snapshotComplete ? "complete" : "partial";
254
+ const conceptCoverage =
255
+ concept.coverage?.conceptCandidates ?? (concept.partial ? "partial" : "complete");
256
+ const conceptSourceCoverage: CoverageStatus = execution.sourceGeneration.partial
257
+ ? "partial"
258
+ : "complete";
259
+ const deduplicationCoverage: CoverageStatus =
260
+ scan.snapshotComplete &&
261
+ literal.sourceCoverage === "complete" &&
262
+ conceptSourceCoverage === "complete"
263
+ ? "complete"
264
+ : "partial";
265
+ const partial =
266
+ !scan.snapshotComplete ||
267
+ concept.partial ||
268
+ conceptCoverage === "skipped" ||
269
+ conceptSourceCoverage === "partial" ||
270
+ literal.sourceCoverage === "partial" ||
271
+ deduplicationCoverage === "partial" ||
272
+ judgedConcept.semanticJudge?.status === "failed" ||
273
+ judgedConcept.semanticJudge?.status === "partial";
274
+ const selectionReason = conceptCandidatesOmitted
275
+ ? `Hybrid concept limit retained the top ${String(selectedConcept.length)} of ${String(eligibleConcept.length)} non-overlapping semantic candidates`
276
+ : undefined;
277
+ return {
278
+ kind: "hybrid",
279
+ unit: "evidence-items",
280
+ items: [...literal.items, ...selectedConcept],
281
+ partial,
282
+ reasons: [
283
+ ...(scan.retention?.reasons ?? []),
284
+ ...concept.reasons,
285
+ ...literal.reasons,
286
+ ...execution.sourceGeneration.reasons,
287
+ ...(selectionReason ? [selectionReason] : []),
288
+ ...(judgedConcept.semanticJudge?.reason ? [judgedConcept.semanticJudge.reason] : []),
289
+ ],
290
+ filesRead: (concept.filesRead ?? 0) + access.filesRead,
291
+ bytesRead: (concept.bytesRead ?? 0) + access.bytesRead,
292
+ counts: {
293
+ ...concept.counts,
294
+ literalMatchingLinesFound: scan.totalMatches,
295
+ literalMatchingLinesPrepared: literal.items.length,
296
+ literalOccurrencesRetained,
297
+ conceptCandidatesRanked: concept.items.length,
298
+ conceptCandidatesDeduplicated: duplicateConceptCandidates,
299
+ conceptCandidatesEligible: eligibleConcept.length,
300
+ conceptCandidatesSelected: selectedConcept.length,
301
+ conceptCandidatesOmitted,
302
+ literalItemsRetained: literal.items.length,
303
+ conceptItemsRetained: selectedConcept.length,
304
+ literalDocumentsReused: literal.reusedDocuments,
305
+ literalDocumentsLoaded: literal.loadedDocuments,
306
+ ...(judgedConcept.semanticJudge
307
+ ? {
308
+ semanticJudgeCandidatesConsidered: judgedConcept.semanticJudge.candidatesConsidered,
309
+ semanticJudgeCandidatesJudged: judgedConcept.semanticJudge.judgedCandidates,
310
+ }
311
+ : {}),
312
+ },
313
+ ...(concept.scope ? { scope: concept.scope } : {}),
314
+ coverage: {
315
+ literalMatches: literalCoverage,
316
+ conceptCandidates: conceptCoverage,
317
+ crossSourceDeduplication: deduplicationCoverage,
318
+ sourceInspection: literal.sourceCoverage,
319
+ conceptSourceInspection: conceptSourceCoverage,
320
+ retention: concept.coverage?.retention ?? "complete",
321
+ },
322
+ ...(concept.stats ? { stats: concept.stats } : {}),
323
+ ...(concept.redact !== undefined ? { redact: concept.redact } : {}),
324
+ sourceGeneration: conceptSourceSummary(execution.sourceGeneration),
325
+ ...(judgedConcept.semanticJudge ? { semanticJudge: judgedConcept.semanticJudge } : {}),
326
+ };
327
+ }
328
+
329
+ export function retainedHybridCounts(
330
+ original: Readonly<Record<string, number>>,
331
+ items: readonly AnalysisItem[],
332
+ ): Record<string, number> {
333
+ let literalItemsRetained = 0;
334
+ let conceptItemsRetained = 0;
335
+ for (const item of items) {
336
+ if (item.details?.source === "literal") literalItemsRetained += 1;
337
+ if (item.details?.source === "concept") conceptItemsRetained += 1;
338
+ }
339
+ return { ...original, literalItemsRetained, conceptItemsRetained };
340
+ }