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,259 @@
1
+ import { isAbsolute, resolve } from "node:path";
2
+ import { consumeCappedLines } from "./capped-lines.js";
3
+ import { runOwnedProcess } from "./owned-process.js";
4
+ import { abortError, SiftLightError } from "./errors.js";
5
+ import { getSourceRevision, sameSourceRevision } from "./source.js";
6
+ import {
7
+ MAX_PROTOCOL_LINE_BYTES,
8
+ MAX_SOURCE_FILE_BYTES,
9
+ type SourceRevision,
10
+ type StructureDetails,
11
+ type StructureSymbol,
12
+ } from "./types.js";
13
+
14
+ export const CTAGS_CAPABILITY_ARGUMENTS = [
15
+ "--output-format=json",
16
+ "--fields=+ne",
17
+ "--extras=-p",
18
+ ] as const;
19
+
20
+ export interface StructureInspectionRequest {
21
+ absolutePath: string;
22
+ cwd: string;
23
+ line: number;
24
+ expectedRevision?: SourceRevision;
25
+ }
26
+
27
+ export interface StructureInspection {
28
+ details: StructureDetails;
29
+ currentRevision?: SourceRevision;
30
+ }
31
+
32
+ export interface CodeStructureProvider {
33
+ inspect(request: StructureInspectionRequest, signal?: AbortSignal): Promise<StructureInspection>;
34
+ }
35
+
36
+ export interface CtagsTag {
37
+ path: string;
38
+ name: string;
39
+ language?: string;
40
+ kind?: string;
41
+ scope?: string;
42
+ line?: number;
43
+ end?: number;
44
+ }
45
+
46
+ export interface CtagsStructureProviderOptions {
47
+ executable?: string;
48
+ maxFileBytes?: number;
49
+ runCtags?: (absolutePath: string, cwd: string, signal?: AbortSignal) => Promise<CtagsTag[]>;
50
+ }
51
+
52
+ class CtagsCommandError extends Error {
53
+ constructor(message: string) {
54
+ super(message);
55
+ this.name = "CtagsCommandError";
56
+ }
57
+ }
58
+
59
+ class CtagsProtocolError extends SiftLightError {}
60
+
61
+ function hasCode(error: unknown, code: string): boolean {
62
+ return error instanceof Error && "code" in error && error.code === code;
63
+ }
64
+
65
+ function isRecord(value: unknown): value is Record<string, unknown> {
66
+ return typeof value === "object" && value !== null;
67
+ }
68
+
69
+ function asOptionalString(value: unknown): string | undefined {
70
+ return typeof value === "string" && value.length > 0 ? value : undefined;
71
+ }
72
+
73
+ function asOptionalPositiveInteger(value: unknown): number | undefined {
74
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
75
+ }
76
+
77
+ function parseCtagsTag(value: unknown): CtagsTag | undefined {
78
+ if (!isRecord(value)) return undefined;
79
+ const hasTagType = Object.entries(value).some(
80
+ ([key, entry]) => key === "_type" && entry === "tag",
81
+ );
82
+ if (!hasTagType) return undefined;
83
+ const path = asOptionalString(value.path);
84
+ const name = asOptionalString(value.name);
85
+ const language = asOptionalString(value.language);
86
+ const kind = asOptionalString(value.kind);
87
+ const scope = asOptionalString(value.scope);
88
+ const line = asOptionalPositiveInteger(value.line);
89
+ const end = asOptionalPositiveInteger(value.end);
90
+ if (!path || !name) return undefined;
91
+ return {
92
+ path,
93
+ name,
94
+ ...(language ? { language } : {}),
95
+ ...(kind ? { kind } : {}),
96
+ ...(scope ? { scope } : {}),
97
+ ...(line ? { line } : {}),
98
+ ...(end ? { end } : {}),
99
+ };
100
+ }
101
+
102
+ async function runCtagsCommand(
103
+ executable: string,
104
+ absolutePath: string,
105
+ cwd: string,
106
+ signal?: AbortSignal,
107
+ ): Promise<CtagsTag[]> {
108
+ const tags: CtagsTag[] = [];
109
+ const { code, stderr } = await runOwnedProcess(
110
+ {
111
+ executable,
112
+ args: [...CTAGS_CAPABILITY_ARGUMENTS, absolutePath],
113
+ cwd,
114
+ ...(signal ? { signal } : {}),
115
+ },
116
+ async (stdout) => {
117
+ try {
118
+ await consumeCappedLines(
119
+ stdout,
120
+ (line) => {
121
+ if (line.length === 0) return;
122
+ let value: unknown;
123
+ try {
124
+ value = JSON.parse(line);
125
+ } catch (error) {
126
+ throw new CtagsProtocolError("Failed to parse Universal Ctags JSON output", {
127
+ cause: error,
128
+ });
129
+ }
130
+ const tag = parseCtagsTag(value);
131
+ if (tag) tags.push(tag);
132
+ },
133
+ { maxLineBytes: MAX_PROTOCOL_LINE_BYTES },
134
+ );
135
+ } catch (error) {
136
+ if (error instanceof Error && error.message.startsWith("Input line exceeds the ")) {
137
+ throw new CtagsProtocolError(error.message, { cause: error });
138
+ }
139
+ throw error;
140
+ }
141
+ },
142
+ );
143
+ if (code !== 0) {
144
+ throw new CtagsCommandError(stderr.trim() || `ctags exited with status ${String(code)}`);
145
+ }
146
+ return tags;
147
+ }
148
+
149
+ function pathMatches(tagPath: string, absolutePath: string, cwd: string): boolean {
150
+ return resolve(isAbsolute(tagPath) ? tagPath : resolve(cwd, tagPath)) === resolve(absolutePath);
151
+ }
152
+
153
+ function symbolFromTag(tag: CtagsTag): StructureSymbol | undefined {
154
+ if (tag.line === undefined || tag.end === undefined || tag.end < tag.line) return undefined;
155
+ return {
156
+ name: tag.name,
157
+ kind: tag.kind ?? "unknown",
158
+ scope: tag.scope ? [tag.scope] : [],
159
+ range: { startLine: tag.line, endLine: tag.end },
160
+ };
161
+ }
162
+
163
+ function chooseEnclosingSymbol(
164
+ tags: CtagsTag[],
165
+ absolutePath: string,
166
+ cwd: string,
167
+ line: number,
168
+ ): StructureSymbol | undefined {
169
+ const candidates = tags
170
+ .filter((tag) => pathMatches(tag.path, absolutePath, cwd))
171
+ .map(symbolFromTag)
172
+ .filter((symbol): symbol is StructureSymbol => symbol !== undefined)
173
+ .filter((symbol) => symbol.range.startLine <= line && line <= symbol.range.endLine)
174
+ .toSorted((left, right) => {
175
+ const leftSize = left.range.endLine - left.range.startLine;
176
+ const rightSize = right.range.endLine - right.range.startLine;
177
+ if (leftSize !== rightSize) return leftSize - rightSize;
178
+ return right.scope.length - left.scope.length;
179
+ });
180
+ return candidates[0];
181
+ }
182
+
183
+ export function createCtagsStructureProvider(
184
+ options: CtagsStructureProviderOptions = {},
185
+ ): CodeStructureProvider {
186
+ const executable = options.executable ?? "ctags";
187
+ const maxFileBytes = options.maxFileBytes ?? MAX_SOURCE_FILE_BYTES;
188
+ const runCtags =
189
+ options.runCtags ??
190
+ ((absolutePath, cwd, signal) => runCtagsCommand(executable, absolutePath, cwd, signal));
191
+
192
+ return {
193
+ async inspect(request, signal) {
194
+ if (signal?.aborted) throw abortError();
195
+ const currentRevision = await getSourceRevision(request.absolutePath);
196
+ if (!currentRevision) {
197
+ return { details: { status: "source-unavailable", provider: "universal-ctags" } };
198
+ }
199
+ if (
200
+ request.expectedRevision &&
201
+ !sameSourceRevision(request.expectedRevision, currentRevision)
202
+ ) {
203
+ return {
204
+ details: { status: "source-changed", provider: "universal-ctags" },
205
+ currentRevision,
206
+ };
207
+ }
208
+ if (currentRevision.size > maxFileBytes) {
209
+ return {
210
+ details: { status: "file-too-large", provider: "universal-ctags" },
211
+ currentRevision,
212
+ };
213
+ }
214
+
215
+ let tags: CtagsTag[];
216
+ try {
217
+ tags = await runCtags(request.absolutePath, request.cwd, signal);
218
+ } catch (error) {
219
+ if (signal?.aborted || (error instanceof Error && error.name === "AbortError")) {
220
+ throw abortError();
221
+ }
222
+ if (hasCode(error, "ENOENT") || error instanceof CtagsCommandError) {
223
+ return {
224
+ details: {
225
+ status: "provider-unavailable",
226
+ provider: "universal-ctags",
227
+ reason:
228
+ "Universal Ctags is unavailable; install universal-ctags or use JS/TS/Python outline support",
229
+ },
230
+ currentRevision,
231
+ };
232
+ }
233
+ if (error instanceof CtagsProtocolError) {
234
+ return {
235
+ details: {
236
+ status: "parse-error",
237
+ provider: "universal-ctags",
238
+ reason: "Universal Ctags returned invalid JSON output; check the installed provider",
239
+ },
240
+ currentRevision,
241
+ };
242
+ }
243
+ throw error;
244
+ }
245
+
246
+ const symbol = chooseEnclosingSymbol(tags, request.absolutePath, request.cwd, request.line);
247
+ const language = tags.find((tag) => tag.language)?.language;
248
+ return {
249
+ details: {
250
+ status: symbol ? "available" : "no-symbol",
251
+ provider: "universal-ctags",
252
+ ...(language ? { language } : {}),
253
+ ...(symbol ? { symbol, range: symbol.range } : {}),
254
+ },
255
+ currentRevision,
256
+ };
257
+ },
258
+ };
259
+ }
@@ -0,0 +1,96 @@
1
+ import { abortError } from "./errors.js";
2
+ import { excerptText } from "./excerpt.js";
3
+ import { readWorkspaceDocument } from "./source-document.js";
4
+ import { sameSourceRevision } from "./source.js";
5
+ import { MAX_SOURCE_FILE_BYTES, type SearchSnapshot } from "./types.js";
6
+
7
+ function hasBareCarriageReturn(bytes: Uint8Array): boolean {
8
+ for (let index = 0; index < bytes.length; index += 1)
9
+ if (bytes[index] === 13 && bytes[index + 1] !== 10) return true;
10
+ return false;
11
+ }
12
+
13
+ /** At most five reads, two disjoint windows/file, seven lines/window. */
14
+ export async function summarySourcePreviews(
15
+ snapshot: SearchSnapshot,
16
+ paths: string[],
17
+ maxBytes: number,
18
+ cwd: string,
19
+ signal?: AbortSignal,
20
+ ) {
21
+ const rows: string[] = [];
22
+ const indices: number[] = [];
23
+ let bytes = 0;
24
+ let filesRead = 0;
25
+ let windows = 0;
26
+ const reasons: string[] = [];
27
+ for (const path of paths) {
28
+ if (filesRead >= 5 || maxBytes - bytes < 256) break;
29
+ const matches = snapshot.matches.flatMap((match, index) =>
30
+ match.displayPath === path ? [{ match, index }] : [],
31
+ );
32
+ const first = matches[0];
33
+ if (!first) continue;
34
+ if (matches.some(({ match }) => match.lineContent.includes("\r"))) {
35
+ reasons.push(`${path}: preview skipped for non-standard line endings`);
36
+ continue;
37
+ }
38
+ if (matches.some(({ match }) => match.lineTruncated || match.occurrences.length > 20)) {
39
+ reasons.push(`${path}: preview skipped for dense or truncated matching lines`);
40
+ continue;
41
+ }
42
+ const revision = snapshot.sourceRevisions.get(first.match.absolutePath);
43
+ if (!revision || revision.size > MAX_SOURCE_FILE_BYTES) {
44
+ reasons.push(`${path}: preview source unverified or over 5 MiB`);
45
+ continue;
46
+ }
47
+ filesRead += 1;
48
+ try {
49
+ // oxlint-disable-next-line no-await-in-loop -- prior rendered windows determine whether later source reads fit the shared preview budget.
50
+ const document = await readWorkspaceDocument(path, cwd, signal);
51
+ if (
52
+ document.reference.origin.kind !== "worktree" ||
53
+ !sameSourceRevision(revision, document.reference.origin.revision) ||
54
+ !document.utf8
55
+ ) {
56
+ reasons.push(`${path}: preview source changed or is not lossless UTF-8`);
57
+ continue;
58
+ }
59
+ if (hasBareCarriageReturn(document.bytes)) {
60
+ reasons.push(`${path}: preview skipped for non-standard line endings`);
61
+ continue;
62
+ }
63
+ let lastEnd = 0;
64
+ let perFile = 0;
65
+ for (const { match, index } of matches) {
66
+ if (perFile >= 2) break;
67
+ const start = Math.max(1, match.lineNumber - 3);
68
+ const end = Math.min(document.lineStarts.length, start + 6);
69
+ if (start <= lastEnd) continue;
70
+ const lineRows: string[] = [];
71
+ for (let line = start; line <= end; line += 1) {
72
+ const value = document.slice(document.lineRange(line)).replace(/\n$/, "");
73
+ const excerpt = excerptText(value);
74
+ lineRows.push(
75
+ `${line}: ${excerpt.text}${excerpt.truncated ? " [preview line truncated]" : ""}`,
76
+ );
77
+ }
78
+ const row = `${path}:${match.lineNumber} {match #${index + 1}} [source preview lines ${start}-${end}]\n${lineRows.join("\n")}`;
79
+ if (bytes + Buffer.byteLength(row) + 2 > maxBytes) continue;
80
+ rows.push(row);
81
+ indices.push(index + 1);
82
+ bytes += Buffer.byteLength(row) + 2;
83
+ windows += 1;
84
+ perFile += 1;
85
+ lastEnd = end;
86
+ }
87
+ } catch (error) {
88
+ if (signal?.aborted || (error instanceof Error && error.name === "AbortError"))
89
+ throw abortError();
90
+ if (!(error instanceof Error) || !("code" in error || error.name === "SourceDocumentError"))
91
+ throw error;
92
+ reasons.push(`${path}: preview unavailable`);
93
+ }
94
+ }
95
+ return { text: rows.join("\n\n"), indices, windows, filesRead, reasons };
96
+ }
package/src/summary.ts ADDED
@@ -0,0 +1,79 @@
1
+ import {
2
+ DEFAULT_RESULT_TOKEN_BUDGET,
3
+ ESTIMATED_CHARACTERS_PER_TOKEN,
4
+ MAX_RESULT_BYTES,
5
+ type MatchRecord,
6
+ type SearchSnapshot,
7
+ } from "./types.js";
8
+
9
+ const METADATA_CHARACTERS = 2_400;
10
+ const METADATA_BYTES = 3_584;
11
+
12
+ /** File navigation gets its budget before optional source samples. */
13
+ export function formatSummary(
14
+ snapshot: SearchSnapshot,
15
+ fileLimit: number,
16
+ offset = 0,
17
+ resultTokenBudget = DEFAULT_RESULT_TOKEN_BUDGET,
18
+ ) {
19
+ if (!Number.isSafeInteger(fileLimit) || fileLimit <= 0)
20
+ throw new Error("Summary file limit must be a positive safe integer");
21
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > snapshot.fileCounts.size)
22
+ throw new Error("Summary offset is outside the file summary");
23
+ const files = [...snapshot.fileCounts.entries()].toSorted(
24
+ ([left, leftCount], [right, rightCount]) => rightCount - leftCount || left.localeCompare(right),
25
+ );
26
+ const maxCharacters = Math.max(
27
+ 256,
28
+ resultTokenBudget * ESTIMATED_CHARACTERS_PER_TOKEN - METADATA_CHARACTERS,
29
+ );
30
+ const maxBytes = MAX_RESULT_BYTES - METADATA_BYTES;
31
+ const rows: string[] = [];
32
+ const shownPaths: string[] = [];
33
+ const firstMatches = new Map<string, { match: MatchRecord; index: number }>();
34
+ for (const [index, match] of snapshot.matches.entries())
35
+ if (!firstMatches.has(match.displayPath)) firstMatches.set(match.displayPath, { match, index });
36
+ let bytes = 0;
37
+ let characters = 0;
38
+ for (const [file, count] of files.slice(offset, offset + Math.min(30, fileLimit))) {
39
+ const row = `${file} ${String(count).padStart(6)}`;
40
+ if (bytes + Buffer.byteLength(row) + 1 > maxBytes) {
41
+ if (!rows.length)
42
+ throw new Error("A file summary row exceeds the response byte budget; narrow the path");
43
+ break;
44
+ }
45
+ if (rows.length && characters + row.length + 1 > maxCharacters) break;
46
+ rows.push(row);
47
+ shownPaths.push(file);
48
+ bytes += Buffer.byteLength(row) + 1;
49
+ characters += row.length + 1;
50
+ }
51
+ const previews: string[] = [];
52
+ const sampleIndices: number[] = [];
53
+ const sampleBudget = Math.max(0, Math.min(maxBytes - bytes, maxCharacters - characters));
54
+ let sampleBytes = 0;
55
+ for (const path of shownPaths.slice(0, 5)) {
56
+ const retained = firstMatches.get(path);
57
+ if (!retained) continue;
58
+ const preview = `${path}:${retained.match.lineNumber} {match #${retained.index + 1}} ${retained.match.lineContent}`;
59
+ if (sampleBytes + Buffer.byteLength(preview) + 1 > sampleBudget) continue;
60
+ previews.push(preview);
61
+ sampleIndices.push(retained.index + 1);
62
+ sampleBytes += Buffer.byteLength(preview) + 1;
63
+ }
64
+ const nextOffset = offset + rows.length;
65
+ return {
66
+ body: rows.join("\n"),
67
+ previews: previews.join("\n"),
68
+ previewsShown: previews.length,
69
+ previewsOmitted: shownPaths.length - previews.length,
70
+ shown: rows.length,
71
+ offset,
72
+ nextOffset,
73
+ hasNext: nextOffset < files.length,
74
+ omitted: files.length - nextOffset,
75
+ shownPaths,
76
+ sampleIndices,
77
+ previewByteBudget: sampleBudget,
78
+ };
79
+ }