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,731 @@
1
+ import { resolve } from "node:path";
2
+ import { abortError, SiftLightError } from "./errors.js";
3
+ import type { InspectionTarget } from "./inspect.js";
4
+ import type { SourceAccess } from "./source-access.js";
5
+ import { SourceContinuations } from "./source-continuations.js";
6
+ import {
7
+ SourceDocumentError,
8
+ type ByteRange,
9
+ type SourceDocument,
10
+ type SourceReference,
11
+ } from "./source-document.js";
12
+ import { mergeByteRanges, sourcePage, type SourceFragment } from "./source-pages.js";
13
+ import { getSourceRevision, sameSourceRevision, sourceRangeFromBytes } from "./source.js";
14
+ import type { CodeStructureProvider } from "./structure.js";
15
+ import { syntaxLanguage } from "./syntax.js";
16
+ import type { SiftLightInput } from "./service.js";
17
+ import {
18
+ MAX_RESULT_BYTES,
19
+ type InspectBatchItemDetails,
20
+ type SiftLightResult,
21
+ type SourceBoundary,
22
+ type SourceExcerptDetails,
23
+ type SourceRevision,
24
+ type StructureDetails,
25
+ } from "./types.js";
26
+
27
+ export interface SourceInspectionTarget {
28
+ path: string;
29
+ line: number;
30
+ matchIndex?: number;
31
+ reference?: SourceReference;
32
+ range?: ByteRange;
33
+ /** Raw source byte offset; retained-match focus is line-relative. */
34
+ absoluteFocus?: number;
35
+ focus?: number;
36
+ expectedRevision?: SourceRevision;
37
+ unverified?: boolean;
38
+ retry?: SiftLightInput;
39
+ }
40
+ interface PreparedTarget {
41
+ target: SourceInspectionTarget;
42
+ document: SourceDocument;
43
+ range: ByteRange;
44
+ structure: StructureDetails;
45
+ boundary: Exclude<SourceBoundary, "mixed">;
46
+ focus: number;
47
+ }
48
+ interface SourceBlock {
49
+ document: SourceDocument;
50
+ ranges: ByteRange[];
51
+ targets: number[];
52
+ prepared: PreparedTarget[];
53
+ fragments: SourceFragment[];
54
+ remaining: ByteRange[];
55
+ text: string[];
56
+ boundary: SourceBoundary | undefined;
57
+ continuation?: string;
58
+ }
59
+
60
+ function usesDocumentLineWindow(path: string): boolean {
61
+ return /\.(?:md|markdown)$/iu.test(path);
62
+ }
63
+
64
+ function hasBareCarriageReturn(bytes: Uint8Array): boolean {
65
+ for (let index = 0; index < bytes.length; index += 1)
66
+ if (bytes[index] === 13 && bytes[index + 1] !== 10) return true;
67
+ return false;
68
+ }
69
+
70
+ export function matchInspectionTarget(target: InspectionTarget): SourceInspectionTarget {
71
+ return {
72
+ path: target.path,
73
+ line: target.line,
74
+ unverified: target.unverified,
75
+ ...(target.expectedRevision ? { expectedRevision: target.expectedRevision } : {}),
76
+ ...(target.retainedMatch?.occurrences[0]
77
+ ? { focus: target.retainedMatch.occurrences[0].byteStart }
78
+ : {}),
79
+ };
80
+ }
81
+
82
+ function errorStatus(error: unknown): StructureDetails["status"] | undefined {
83
+ if (error instanceof SourceDocumentError)
84
+ return error.reason === "encoding" ? "source-unavailable" : error.reason;
85
+ if (
86
+ error instanceof Error &&
87
+ "code" in error &&
88
+ ["ENOENT", "EACCES", "EPERM", "EISDIR", "ENOTDIR"].includes(String(error.code))
89
+ )
90
+ return "source-unavailable";
91
+ return undefined;
92
+ }
93
+
94
+ async function prepare(
95
+ target: SourceInspectionTarget,
96
+ access: SourceAccess,
97
+ structure?: CodeStructureProvider,
98
+ ): Promise<PreparedTarget> {
99
+ if (target.unverified)
100
+ throw new SourceDocumentError(
101
+ "source-unavailable",
102
+ "Snapshot source revision is unverified; refresh the search",
103
+ );
104
+ const document = await access.load(target.path, target.reference);
105
+ if (
106
+ target.expectedRevision &&
107
+ (document.reference.origin.kind !== "worktree" ||
108
+ !sameSourceRevision(target.expectedRevision, document.reference.origin.revision))
109
+ )
110
+ throw new SourceDocumentError("source-changed", "Source changed; refresh the search");
111
+ if (target.line > document.lineStarts.length)
112
+ throw new SourceDocumentError(
113
+ "source-unavailable",
114
+ `Source line ${target.line} is beyond the end of the file`,
115
+ );
116
+ const lineRange = document.lineRange(target.line);
117
+ const focus =
118
+ target.range?.start ??
119
+ target.absoluteFocus ??
120
+ Math.min(lineRange.end, lineRange.start + (target.focus ?? 0));
121
+ let range = target.range;
122
+ let details: StructureDetails = { status: "no-symbol" };
123
+ const language = syntaxLanguage(document.path);
124
+ if (document.utf8 && language && language !== "go") {
125
+ const syntax = await access.syntax(document);
126
+ details = {
127
+ status:
128
+ syntax.status === "ok"
129
+ ? "no-symbol"
130
+ : syntax.status === "unsupported"
131
+ ? "provider-unavailable"
132
+ : "parse-error",
133
+ provider: "tree-sitter",
134
+ language,
135
+ };
136
+ if (syntax.status === "ok") {
137
+ const character = document.toCharacterOffset(focus);
138
+ const symbols = syntax.symbols
139
+ .filter((symbol) => symbol.hasBody && symbol.start <= character && character < symbol.end)
140
+ .toSorted((a, b) => a.end - a.start - (b.end - b.start));
141
+ // A path+line can point at indentation before the declaration itself.
142
+ const symbol =
143
+ symbols[0] ??
144
+ syntax.symbols.find(
145
+ (item) =>
146
+ item.hasBody && document.lineAt(document.toByteOffset(item.start)) === target.line,
147
+ );
148
+ if (symbol && !target.range) {
149
+ range = {
150
+ start: document.toByteOffset(symbol.start),
151
+ end: document.toByteOffset(symbol.end),
152
+ };
153
+ const lines = {
154
+ startLine: document.lineAt(range.start),
155
+ endLine: document.lineAt(Math.max(range.start, range.end - 1)),
156
+ };
157
+ details = {
158
+ status: "available",
159
+ provider: "tree-sitter",
160
+ language,
161
+ range: lines,
162
+ symbol: {
163
+ name: symbol.name,
164
+ kind: symbol.kind,
165
+ scope: symbol.scope ? [symbol.scope] : [],
166
+ range: lines,
167
+ },
168
+ };
169
+ }
170
+ }
171
+ } else if (
172
+ document.utf8 &&
173
+ structure &&
174
+ document.reference.origin.kind === "worktree" &&
175
+ !target.range &&
176
+ !usesDocumentLineWindow(document.path)
177
+ ) {
178
+ const result = await structure.inspect(
179
+ {
180
+ absolutePath: resolve(access.cwd, target.path),
181
+ cwd: access.cwd,
182
+ line: target.line,
183
+ expectedRevision: document.reference.origin.revision,
184
+ },
185
+ access.signal,
186
+ );
187
+ details = result.details;
188
+ if (["source-changed", "source-unavailable", "file-too-large"].includes(details.status))
189
+ throw new SourceDocumentError(
190
+ details.status === "source-changed" ? "source-changed" : "source-unavailable",
191
+ `Source inspection: ${details.status}`,
192
+ );
193
+ if (details.range)
194
+ range = document.lineRange(
195
+ details.range.startLine,
196
+ Math.min(details.range.endLine, document.lineStarts.length),
197
+ );
198
+ } else if (usesDocumentLineWindow(document.path)) {
199
+ details = { status: "no-symbol" };
200
+ } else {
201
+ details = { status: "provider-unavailable", ...(language ? { language } : {}) };
202
+ }
203
+ range ??= document.lineRange(
204
+ Math.max(1, target.line - 10),
205
+ Math.min(document.lineStarts.length, target.line + 10),
206
+ );
207
+ const boundary: Exclude<SourceBoundary, "mixed"> = target.range
208
+ ? "requested-range"
209
+ : details.status === "available" && details.range
210
+ ? "syntax"
211
+ : "line-window";
212
+ document.checkRange(range);
213
+ return { target, document, range, structure: details, boundary, focus };
214
+ }
215
+
216
+ function boundaryNote(block: SourceBlock): string {
217
+ if (block.boundary === "line-window" || block.boundary === "mixed") {
218
+ const fallback = block.prepared.find((prepared) => prepared.boundary === "line-window");
219
+ const status = fallback?.structure.status;
220
+ const provider = fallback?.structure.provider;
221
+ const diagnostic =
222
+ status === "no-symbol" && provider === undefined
223
+ ? ""
224
+ : status
225
+ ? " (" + status + (provider ? " via " + provider : "") + ")"
226
+ : "";
227
+ return "; syntax boundary unavailable" + diagnostic + "; bounded line window";
228
+ }
229
+ return block.boundary === "requested-range"
230
+ ? "; requested range; syntax boundary not inferred"
231
+ : "";
232
+ }
233
+
234
+ function blockDetails(block: SourceBlock): SourceExcerptDetails {
235
+ const starts = block.fragments.map((fragment) => fragment.start);
236
+ const ends = block.fragments.map((fragment) => fragment.end);
237
+ const start = starts.length > 0 ? Math.min(...starts) : (block.ranges[0]?.start ?? 0);
238
+ const end = ends.length > 0 ? Math.max(...ends) : start;
239
+ const nextRequest: SiftLightInput | undefined = block.continuation
240
+ ? { mode: "inspect", sourceCursor: block.continuation }
241
+ : undefined;
242
+ return {
243
+ range: {
244
+ startLine: block.document.lineAt(start),
245
+ endLine: block.document.lineAt(Math.max(start, end - 1)),
246
+ },
247
+ omittedBefore: block.remaining
248
+ .filter((range) => range.end <= start)
249
+ .reduce(
250
+ (n, range) => n + block.document.lineAt(range.end) - block.document.lineAt(range.start),
251
+ 0,
252
+ ),
253
+ omittedAfter: block.remaining
254
+ .filter((range) => range.start >= end)
255
+ .reduce(
256
+ (n, range) => n + block.document.lineAt(range.end) - block.document.lineAt(range.start),
257
+ 0,
258
+ ),
259
+ truncatedLines: [],
260
+ reference: block.document.reference,
261
+ targetRanges: block.ranges,
262
+ fragments: block.fragments,
263
+ remainingRanges: block.remaining,
264
+ complete: block.document.utf8 && block.remaining.length === 0,
265
+ ...(block.boundary ? { boundary: block.boundary } : {}),
266
+ ...(nextRequest ? { nextRequest } : {}),
267
+ };
268
+ }
269
+
270
+ function render(items: InspectBatchItemDetails[], blocks: SourceBlock[], single: boolean): string {
271
+ const rows = items.map(
272
+ (item) =>
273
+ `Target #${item.inputIndex} ${item.path ?? ""}:${item.line ?? ""}: ${item.status}${item.block ? `; Block #${item.block}` : ""}${item.structure && !(item.structure.status === "no-symbol" && item.structure.provider === undefined) ? ` [structure: ${item.structure.status}${item.structure.provider ? ` via ${item.structure.provider}` : ""}${item.structure.reason ? `; ${item.structure.reason}` : ""}]` : ""}${item.structure?.symbol ? ` ${item.structure.symbol.name} (${item.structure.symbol.kind}) lines ${item.structure.symbol.range.startLine}-${item.structure.symbol.range.endLine}` : ""}${item.error ? `; ${item.error}` : ""}${item.retry ? `\nRetry: ${JSON.stringify(item.retry)}` : ""}`,
274
+ );
275
+ const sourceRows = blocks.map((block, index) => {
276
+ const origin =
277
+ block.document.reference.origin.kind === "git"
278
+ ? "commit " +
279
+ block.document.reference.origin.commit +
280
+ "; blob " +
281
+ block.document.reference.origin.blob
282
+ : "source sha256 " + block.document.reference.origin.contentHash;
283
+ const completeness = block.remaining.length
284
+ ? "PARTIAL; missing byte ranges " + JSON.stringify(block.remaining)
285
+ : "complete for selected range";
286
+ const next = block.continuation
287
+ ? "\nNext request: " + JSON.stringify({ mode: "inspect", sourceCursor: block.continuation })
288
+ : "";
289
+ return (
290
+ "[Block #" +
291
+ String(index + 1) +
292
+ "] " +
293
+ block.document.path +
294
+ "; " +
295
+ origin +
296
+ "\n" +
297
+ block.text.join("\n") +
298
+ "\n[source " +
299
+ completeness +
300
+ boundaryNote(block) +
301
+ "; shared 16384-byte output limit]" +
302
+ next
303
+ );
304
+ });
305
+ return [
306
+ single
307
+ ? "Source inspection (Inspection metadata)"
308
+ : `Batch inspection (Inspection metadata): ${items.filter((item) => item.status === "returned").length} of ${items.length} targets returned; overlapping ranges merged before the shared 16384-byte budget.`,
309
+ ...rows,
310
+ ...sourceRows,
311
+ ].join("\n\n");
312
+ }
313
+ function blockBoundary(block: SourceBlock): SourceBoundary | undefined {
314
+ const boundaries = [...new Set(block.prepared.map((prepared) => prepared.boundary))];
315
+ if (boundaries.length === 1) return boundaries[0];
316
+ return boundaries.length > 1 ? "mixed" : undefined;
317
+ }
318
+
319
+ function fallbackContinuationRange(
320
+ document: SourceDocument,
321
+ ranges: readonly ByteRange[],
322
+ ): ByteRange | undefined {
323
+ const last = ranges.at(-1);
324
+ if (!last || last.end >= document.bytes.length) return undefined;
325
+ const nextStartLine = document.lineAt(last.end);
326
+ const nextFocusLine = Math.min(document.lineStarts.length, nextStartLine + 10);
327
+ if (nextFocusLine <= nextStartLine) return undefined;
328
+ return document.lineRange(
329
+ nextStartLine,
330
+ Math.min(document.lineStarts.length, nextFocusLine + 10),
331
+ );
332
+ }
333
+
334
+ function metadataStructure(details: StructureDetails): StructureDetails {
335
+ return {
336
+ status: details.status,
337
+ ...(details.provider ? { provider: details.provider } : {}),
338
+ ...(details.language ? { language: details.language } : {}),
339
+ ...(details.range ? { range: details.range } : {}),
340
+ };
341
+ }
342
+
343
+ async function sourceDocumentIsCurrent(
344
+ document: SourceDocument,
345
+ access: SourceAccess,
346
+ ): Promise<boolean> {
347
+ if (document.reference.origin.kind !== "worktree") return true;
348
+ const current = await getSourceRevision(resolve(access.cwd, document.path));
349
+ return current !== undefined && sameSourceRevision(current, document.reference.origin.revision);
350
+ }
351
+
352
+ /** Inspects and verifies targets while exposing only location and parser metadata. */
353
+ export async function inspectDocumentsMetadata(
354
+ targets: SourceInspectionTarget[],
355
+ access: SourceAccess,
356
+ structure?: CodeStructureProvider,
357
+ ): Promise<SiftLightResult> {
358
+ const items: InspectBatchItemDetails[] = [];
359
+ const paths = new Set<string>();
360
+ const preparedTargets: Array<{ itemIndex: number; document: SourceDocument }> = [];
361
+ for (const [index, target] of targets.entries()) {
362
+ try {
363
+ // oxlint-disable-next-line no-await-in-loop -- targets share one bounded source/parse context.
364
+ const prepared = await prepare(target, access, structure);
365
+ paths.add(prepared.document.path);
366
+ preparedTargets.push({ itemIndex: items.length, document: prepared.document });
367
+ items.push({
368
+ inputIndex: index + 1,
369
+ path: target.path,
370
+ line: target.line,
371
+ status: "returned",
372
+ ...(target.matchIndex !== undefined ? { matchIndex: target.matchIndex } : {}),
373
+ structure: metadataStructure(prepared.structure),
374
+ });
375
+ } catch (error) {
376
+ if (access.signal?.aborted || (error instanceof Error && error.name === "AbortError"))
377
+ throw abortError();
378
+ const status = errorStatus(error);
379
+ if (!status) throw error;
380
+ items.push({
381
+ inputIndex: index + 1,
382
+ path: target.path,
383
+ line: target.line,
384
+ status: "error",
385
+ structure: { status },
386
+ error: error instanceof Error ? error.message : `inspection unavailable (${status})`,
387
+ });
388
+ }
389
+ }
390
+ const preparedByPath = new Map<string, Array<{ itemIndex: number; document: SourceDocument }>>();
391
+ for (const prepared of preparedTargets) {
392
+ const entries = preparedByPath.get(prepared.document.path) ?? [];
393
+ entries.push(prepared);
394
+ preparedByPath.set(prepared.document.path, entries);
395
+ }
396
+ for (const entries of preparedByPath.values()) {
397
+ const first = entries[0];
398
+ if (!first) continue;
399
+ const firstOrigin = first.document.reference.origin;
400
+ const revisionsDiffer =
401
+ firstOrigin.kind === "worktree" &&
402
+ entries.some(({ document }) => {
403
+ const origin = document.reference.origin;
404
+ return (
405
+ origin.kind !== "worktree" || !sameSourceRevision(origin.revision, firstOrigin.revision)
406
+ );
407
+ });
408
+ // oxlint-disable-next-line no-await-in-loop -- each distinct source path is revalidated after its targets are prepared.
409
+ const changed = revisionsDiffer || !(await sourceDocumentIsCurrent(first.document, access));
410
+ if (!changed) continue;
411
+ for (const { itemIndex } of entries) {
412
+ const item = items[itemIndex];
413
+ if (!item) continue;
414
+ item.status = "error";
415
+ item.structure = { status: "source-changed" };
416
+ item.error = "Source changed during inspection; refresh the source";
417
+ }
418
+ }
419
+ const complete = items.every((item) => item.status === "returned");
420
+ const lines = [
421
+ `Inspection metadata (${complete ? "complete" : "PARTIAL"}; ${String(targets.length)} target(s); ${String(paths.size)} file(s)).`,
422
+ ...items.map(
423
+ (item) =>
424
+ `#${String(item.inputIndex)} ${item.path ?? "[unknown path]"}:${String(item.line ?? "?")} — ${item.status}${item.structure?.status ? `; structure=${item.structure.status}` : ""}${item.structure?.language ? `; language=${item.structure.language}` : ""}${item.error ? `; ${item.error}` : ""}`,
425
+ ),
426
+ ];
427
+ const first = items[0];
428
+ return {
429
+ text: lines.join("\n\n"),
430
+ details: {
431
+ version: 1,
432
+ mode: "inspect",
433
+ status: complete ? "complete" : "partial",
434
+ snapshotComplete: complete,
435
+ totalMatches: 0,
436
+ storedMatches: 0,
437
+ returnedMatches: 0,
438
+ totalFiles: paths.size,
439
+ inspections: items,
440
+ ...(targets.length === 1 && first?.structure
441
+ ? { structure: metadataStructure(first.structure) }
442
+ : {}),
443
+ },
444
+ };
445
+ }
446
+
447
+ export async function inspectDocuments(
448
+ targets: SourceInspectionTarget[],
449
+ access: SourceAccess,
450
+ continuations: SourceContinuations,
451
+ structure?: CodeStructureProvider,
452
+ ): Promise<SiftLightResult> {
453
+ const items: InspectBatchItemDetails[] = [];
454
+ const blocks: SourceBlock[] = [];
455
+ let metadataOnly = false;
456
+ for (const [index, target] of targets.entries()) {
457
+ try {
458
+ // oxlint-disable-next-line no-await-in-loop -- targets share one bounded source/parse context and input-order response budget.
459
+ const prepared = await prepare(target, access, structure);
460
+ metadataOnly ||= !prepared.document.utf8 || hasBareCarriageReturn(prepared.document.bytes);
461
+ let blockIndex = blocks.findIndex((block) => block.document === prepared.document);
462
+ if (blockIndex < 0) {
463
+ blockIndex = blocks.length;
464
+ blocks.push({
465
+ document: prepared.document,
466
+ ranges: [],
467
+ targets: [],
468
+ prepared: [],
469
+ fragments: [],
470
+ remaining: [],
471
+ text: [],
472
+ boundary: undefined,
473
+ });
474
+ }
475
+ const block = blocks[blockIndex];
476
+ if (!block) throw new Error("Inspection block is unavailable");
477
+ block.ranges.push(prepared.range);
478
+ block.targets.push(index);
479
+ block.prepared.push(prepared);
480
+ items.push({
481
+ inputIndex: index + 1,
482
+ path: target.path,
483
+ line: target.line,
484
+ status: "returned",
485
+ ...(target.matchIndex !== undefined ? { matchIndex: target.matchIndex } : {}),
486
+ block: blockIndex + 1,
487
+ structure: prepared.structure,
488
+ });
489
+ } catch (error) {
490
+ if (access.signal?.aborted || (error instanceof Error && error.name === "AbortError"))
491
+ throw abortError();
492
+ const status = errorStatus(error);
493
+ if (!status) throw error;
494
+ items.push({
495
+ inputIndex: index + 1,
496
+ path: target.path,
497
+ line: target.line,
498
+ status: "error",
499
+ structure: { status },
500
+ error: error instanceof Error ? error.message : status,
501
+ });
502
+ }
503
+ }
504
+ if (metadataOnly) return inspectDocumentsMetadata(targets, access, structure);
505
+ for (const block of blocks) {
506
+ block.ranges = mergeByteRanges(block.ranges);
507
+ block.remaining = block.ranges;
508
+ block.boundary = blockBoundary(block);
509
+ }
510
+ const baseBytes = Buffer.byteLength(render(items, blocks, targets.length === 1));
511
+ let remainingResponseBytes = MAX_RESULT_BYTES - baseBytes - blocks.length * 400;
512
+ if (blocks.length && remainingResponseBytes < blocks.length * 256)
513
+ throw new SiftLightError(
514
+ "Inspection selectors exceed the shared response limit; use fewer targets",
515
+ );
516
+ for (const [index, block] of blocks.entries()) {
517
+ const followingBlocks = blocks.length - index - 1;
518
+ let allowance = remainingResponseBytes - followingBlocks * 256;
519
+ if (!block.document.utf8) {
520
+ const target = block.prepared[0];
521
+ if (!target) throw new Error("Missing lossy-source target");
522
+ const lineStart = block.document.lineStarts[target.target.line - 1] ?? 0;
523
+ const relativeFocus = target.focus - lineStart;
524
+ const preview = sourceRangeFromBytes(
525
+ block.document.bytes,
526
+ Math.max(1, target.target.line - 10),
527
+ Math.min(block.document.lineStarts.length, target.target.line + 10),
528
+ target.target.line,
529
+ {
530
+ maxBytes: Math.min(MAX_RESULT_BYTES - 1024, Math.max(256, allowance - 300)),
531
+ focus: {
532
+ byteStart: relativeFocus,
533
+ byteEnd: relativeFocus,
534
+ range: {
535
+ start: { line: target.target.line - 1, character: relativeFocus },
536
+ end: { line: target.target.line - 1, character: relativeFocus },
537
+ encoding: "utf-8",
538
+ },
539
+ },
540
+ },
541
+ );
542
+ block.text.push(
543
+ `[lossy UTF-8 preview only; original bytes are not fully representable; source continuation unavailable; lines may be clipped at 500 characters]\n${preview.text}`,
544
+ );
545
+ remainingResponseBytes -= Buffer.byteLength(block.text.at(-1) ?? "") + 1;
546
+ for (const targetIndex of block.targets) {
547
+ const item = items[targetIndex];
548
+ if (item)
549
+ item.source = {
550
+ range: { startLine: preview.startLine, endLine: preview.endLine },
551
+ omittedBefore: preview.omittedBefore,
552
+ omittedAfter: preview.omittedAfter,
553
+ truncatedLines: preview.truncatedLines,
554
+ complete: false,
555
+ ...(block.boundary ? { boundary: block.boundary } : {}),
556
+ reference: block.document.reference,
557
+ };
558
+ }
559
+ continue;
560
+ }
561
+ // Allocate around distinct requested locations after computing the union.
562
+ // This keeps distant batch targets visible without charging overlap twice.
563
+ const focuses = [...new Set(block.prepared.map((prepared) => prepared.focus))];
564
+ for (const [focusIndex, focus] of focuses.entries()) {
565
+ if (
566
+ !block.remaining.some((range) => range.start <= focus && focus < range.end) ||
567
+ allowance < 256
568
+ )
569
+ continue;
570
+ const missingBytes = block.remaining.reduce(
571
+ (total, range) => total + range.end - range.start,
572
+ 0,
573
+ );
574
+ const budget =
575
+ missingBytes + block.remaining.length * 200 < allowance
576
+ ? allowance
577
+ : Math.max(256, Math.floor(allowance / (focuses.length - focusIndex)));
578
+ const page = sourcePage(block.document, block.remaining, budget, focus);
579
+ block.fragments.push(page.fragment);
580
+ block.remaining = page.remaining;
581
+ block.text.push(page.text);
582
+ const pageBytes = Buffer.byteLength(page.text) + 1;
583
+ allowance -= pageBytes;
584
+ remainingResponseBytes -= pageBytes;
585
+ }
586
+ while (block.remaining.length && allowance >= 256) {
587
+ const page = sourcePage(block.document, block.remaining, allowance);
588
+ block.fragments.push(page.fragment);
589
+ block.remaining = page.remaining;
590
+ block.text.push(page.text);
591
+ const pageBytes = Buffer.byteLength(page.text) + 1;
592
+ allowance -= pageBytes;
593
+ remainingResponseBytes -= pageBytes;
594
+ }
595
+ const fallback =
596
+ block.boundary === "line-window" || block.boundary === "mixed"
597
+ ? fallbackContinuationRange(block.document, block.ranges)
598
+ : undefined;
599
+ const continuationTarget = fallback ? [...block.ranges, fallback] : block.ranges;
600
+ const continuationGaps = fallback ? [...block.remaining, fallback] : block.remaining;
601
+ if (continuationGaps.length)
602
+ block.continuation = continuations.create(
603
+ block.document.reference,
604
+ continuationTarget,
605
+ continuationGaps,
606
+ block.boundary,
607
+ );
608
+ if (block.document.reference.origin.kind === "worktree") {
609
+ // oxlint-disable-next-line no-await-in-loop -- each block's revision check follows its own completed read and shared output allocation.
610
+ const current = await getSourceRevision(resolve(access.cwd, block.document.path));
611
+ if (!current || !sameSourceRevision(current, block.document.reference.origin.revision)) {
612
+ block.text = [];
613
+ block.fragments = [];
614
+ block.remaining = block.ranges;
615
+ delete block.continuation;
616
+ for (const targetIndex of block.targets) {
617
+ const item = items[targetIndex];
618
+ if (item) {
619
+ item.status = "error";
620
+ item.structure = { status: "source-changed" };
621
+ item.error = "Source changed during inspection; refresh the source";
622
+ }
623
+ }
624
+ }
625
+ }
626
+ for (const targetIndex of block.targets) {
627
+ const item = items[targetIndex];
628
+ if (item?.status === "returned") item.source = blockDetails(block);
629
+ }
630
+ if (access.signal?.aborted) throw abortError();
631
+ if (index >= 5) throw new Error("Inspection target limit was not validated");
632
+ }
633
+ const text = render(items, blocks, targets.length === 1);
634
+ if (Buffer.byteLength(text) > MAX_RESULT_BYTES)
635
+ throw new SiftLightError("Inspection metadata exceeds the response byte limit");
636
+ const complete =
637
+ items.every((item) => item.status === "returned") &&
638
+ blocks.every((block) => block.remaining.length === 0);
639
+ const first = items[0];
640
+ return {
641
+ text,
642
+ details: {
643
+ version: 1,
644
+ mode: "inspect",
645
+ status: complete ? "complete" : "partial",
646
+ snapshotComplete: complete,
647
+ totalMatches: 0,
648
+ storedMatches: 0,
649
+ returnedMatches: 0,
650
+ totalFiles: blocks.length,
651
+ inspections: items,
652
+ sourceBlocks: blocks.map((block) => ({
653
+ path: block.document.path,
654
+ source: blockDetails(block),
655
+ })),
656
+ ...(targets.length === 1 && first?.structure ? { structure: first.structure } : {}),
657
+ ...(targets.length === 1 && first?.source
658
+ ? {
659
+ source: first.source,
660
+ ...(first.source.nextRequest ? { nextRequest: first.source.nextRequest } : {}),
661
+ }
662
+ : {}),
663
+ },
664
+ };
665
+ }
666
+
667
+ export function continueSourceMetadata(
668
+ cursor: string,
669
+ continuations: SourceContinuations,
670
+ ): SiftLightResult {
671
+ const state = continuations.resolve(cursor);
672
+ const remainingBytes = state.remaining.reduce(
673
+ (total, range) => total + range.end - range.start,
674
+ 0,
675
+ );
676
+ return {
677
+ text: `Source continuation metadata (PARTIAL). Source content is not displayed; ${state.source.path} has ${String(state.remaining.length)} remaining range(s) and ${String(remainingBytes)} remaining byte(s).`,
678
+ details: {
679
+ version: 1,
680
+ mode: "inspect",
681
+ status: "partial",
682
+ snapshotComplete: false,
683
+ totalMatches: 0,
684
+ storedMatches: 0,
685
+ returnedMatches: 0,
686
+ totalFiles: 1,
687
+ },
688
+ };
689
+ }
690
+
691
+ export async function continueSource(
692
+ cursor: string,
693
+ access: SourceAccess,
694
+ continuations: SourceContinuations,
695
+ ): Promise<SiftLightResult> {
696
+ const state = continuations.resolve(cursor);
697
+ const document = await access.load(state.source.path, state.source);
698
+ const page = sourcePage(document, state.remaining, MAX_RESULT_BYTES - 1400);
699
+ const next = continuations.advance(cursor, page.fragment);
700
+ const block: SourceBlock = {
701
+ document,
702
+ ranges: state.target,
703
+ targets: [],
704
+ prepared: [],
705
+ fragments: [page.fragment],
706
+ remaining: page.remaining,
707
+ text: [page.text],
708
+ boundary: state.boundary,
709
+ ...(next ? { continuation: next } : {}),
710
+ };
711
+ const source = blockDetails(block);
712
+ const text = render([], [block], true);
713
+ if (Buffer.byteLength(text) > MAX_RESULT_BYTES)
714
+ throw new SiftLightError("Source continuation metadata exceeds the output limit");
715
+ return {
716
+ text,
717
+ details: {
718
+ version: 1,
719
+ mode: "inspect",
720
+ status: next ? "partial" : "complete",
721
+ snapshotComplete: !next,
722
+ totalMatches: 0,
723
+ storedMatches: 0,
724
+ returnedMatches: 0,
725
+ totalFiles: 1,
726
+ source,
727
+ sourceBlocks: [{ path: document.path, source }],
728
+ ...(source.nextRequest ? { nextRequest: source.nextRequest } : {}),
729
+ },
730
+ };
731
+ }