pi-readseek 0.7.7 → 0.7.8

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 (60) hide show
  1. package/README.md +28 -46
  2. package/dist/index.js +7333 -0
  3. package/package.json +8 -7
  4. package/index.ts +0 -147
  5. package/src/binary-detect.ts +0 -22
  6. package/src/check.ts +0 -92
  7. package/src/coerce-obvious-int.ts +0 -41
  8. package/src/confusable-hyphens.ts +0 -6
  9. package/src/def.ts +0 -143
  10. package/src/diff-data.ts +0 -302
  11. package/src/edit-classify.ts +0 -58
  12. package/src/edit-diff.ts +0 -359
  13. package/src/edit-output.ts +0 -110
  14. package/src/edit-render-helpers.ts +0 -141
  15. package/src/edit-syntax-validate.ts +0 -65
  16. package/src/edit.ts +0 -686
  17. package/src/file-map.ts +0 -17
  18. package/src/fs-error.ts +0 -33
  19. package/src/grep-budget.ts +0 -35
  20. package/src/grep-output.ts +0 -181
  21. package/src/grep-render-helpers.ts +0 -77
  22. package/src/grep-symbol-scope.ts +0 -184
  23. package/src/grep.ts +0 -608
  24. package/src/hashline.ts +0 -736
  25. package/src/hover.ts +0 -140
  26. package/src/path-utils.ts +0 -24
  27. package/src/pending-diff-preview.ts +0 -269
  28. package/src/read-local-bundle.ts +0 -75
  29. package/src/read-output.ts +0 -186
  30. package/src/read-render-helpers.ts +0 -104
  31. package/src/read.ts +0 -661
  32. package/src/readseek/constants.ts +0 -17
  33. package/src/readseek/enums.ts +0 -38
  34. package/src/readseek/formatter.ts +0 -350
  35. package/src/readseek/symbol-error-format.ts +0 -18
  36. package/src/readseek/symbol-lookup.ts +0 -240
  37. package/src/readseek/symbol-tree.ts +0 -21
  38. package/src/readseek/types.ts +0 -49
  39. package/src/readseek-client.ts +0 -1250
  40. package/src/readseek-params.ts +0 -39
  41. package/src/readseek-settings.ts +0 -263
  42. package/src/readseek-value.ts +0 -184
  43. package/src/refs-output.ts +0 -47
  44. package/src/refs.ts +0 -161
  45. package/src/register-tool.ts +0 -29
  46. package/src/rename.ts +0 -182
  47. package/src/replace-symbol.ts +0 -77
  48. package/src/runtime.ts +0 -3
  49. package/src/search-output.ts +0 -50
  50. package/src/search.ts +0 -214
  51. package/src/session-anchors.ts +0 -34
  52. package/src/stat-search-path.ts +0 -29
  53. package/src/syntax-validate-mode.ts +0 -15
  54. package/src/tool-prompt-metadata.ts +0 -107
  55. package/src/tool-types.ts +0 -12
  56. package/src/tui-diff-component.ts +0 -108
  57. package/src/tui-diff-renderer.ts +0 -92
  58. package/src/tui-render-utils.ts +0 -286
  59. package/src/view.ts +0 -158
  60. package/src/write.ts +0 -357
package/src/edit-diff.ts DELETED
@@ -1,359 +0,0 @@
1
- import * as Diff from "diff";
2
- import { computeLineHash } from "./hashline.js";
3
- import { CONFUSABLE_HYPHENS_RE } from "./confusable-hyphens.js";
4
-
5
-
6
- export function detectLineEnding(content: string): "\r\n" | "\n" {
7
- let crlf = 0;
8
- let lf = 0;
9
- for (let i = 0; i < content.length; i++) {
10
- if (content[i] === "\r" && i + 1 < content.length && content[i + 1] === "\n") {
11
- crlf++;
12
- i++;
13
- } else if (content[i] === "\n") {
14
- lf++;
15
- }
16
- }
17
- if (lf === 0 && crlf === 0) return "\n";
18
- if (lf === 0) return "\r\n";
19
- if (crlf === 0) return "\n";
20
- return crlf > lf ? "\r\n" : "\n";
21
- }
22
-
23
- export function normalizeToLF(text: string): string {
24
- return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
25
- }
26
-
27
- export function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string {
28
- return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
29
- }
30
-
31
- export function stripBom(content: string): { bom: string; text: string } {
32
- return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
33
- }
34
-
35
- /**
36
- * Detect bare \r characters that are NOT part of \r\n sequences.
37
- * These cause line-count mismatches between normalizeToLF and external tools (ripgrep, wc).
38
- */
39
- export function hasBareCarriageReturn(content: string): boolean {
40
- return content.replace(/\r\n/g, "").includes("\r");
41
- }
42
-
43
- const SINGLE_QUOTES_RE = /[\u2018\u2019\u201A\u201B]/g;
44
- const DOUBLE_QUOTES_RE = /[\u201C\u201D\u201E\u201F]/g;
45
- const UNICODE_SPACES_RE = /[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g;
46
-
47
- function normalizeFuzzyChar(ch: string): string {
48
- return ch.replace(SINGLE_QUOTES_RE, "'").replace(DOUBLE_QUOTES_RE, '"').replace(CONFUSABLE_HYPHENS_RE, "-").replace(UNICODE_SPACES_RE, " ");
49
- }
50
-
51
- function normalizeForFuzzyMatch(text: string): string {
52
- return text
53
- .split("\n")
54
- .map((line) => line.trimEnd())
55
- .join("\n")
56
- .replace(SINGLE_QUOTES_RE, "'")
57
- .replace(DOUBLE_QUOTES_RE, '"')
58
- .replace(CONFUSABLE_HYPHENS_RE, "-")
59
- .replace(UNICODE_SPACES_RE, " ");
60
- }
61
-
62
- function buildNormalizedWithMap(text: string): { normalized: string; indexMap: number[] } {
63
- const lines = text.split("\n");
64
- const normalizedChars: string[] = [];
65
- const indexMap: number[] = [];
66
- let originalOffset = 0;
67
-
68
- for (let i = 0; i < lines.length; i++) {
69
- const line = lines[i]!;
70
- const trimmed = line.replace(/\s+$/u, "");
71
-
72
- for (let j = 0; j < trimmed.length; j++) {
73
- normalizedChars.push(normalizeFuzzyChar(trimmed[j]!));
74
- indexMap.push(originalOffset + j);
75
- }
76
-
77
- if (i < lines.length - 1) {
78
- normalizedChars.push("\n");
79
- indexMap.push(originalOffset + line.length);
80
- }
81
-
82
- originalOffset += line.length + 1;
83
- }
84
-
85
- return { normalized: normalizedChars.join(""), indexMap };
86
- }
87
-
88
- function mapNormalizedSpanToOriginal(
89
- indexMap: number[],
90
- normalizedStart: number,
91
- normalizedLength: number,
92
- ): { index: number; matchLength: number } | null {
93
- if (normalizedStart < 0 || normalizedLength <= 0) return null;
94
- const normalizedEnd = normalizedStart + normalizedLength;
95
- if (normalizedEnd > indexMap.length) return null;
96
-
97
- const start = indexMap[normalizedStart];
98
- const end = indexMap[normalizedEnd - 1];
99
- if (start === undefined || end === undefined || end < start) return null;
100
-
101
- return { index: start, matchLength: end - start + 1 };
102
- }
103
-
104
- /**
105
- * Find `oldText` in `content` with optional fuzzy whitespace/unicode matching.
106
- * Always returns an index/length in the original content.
107
- */
108
- export function fuzzyFindText(
109
- content: string,
110
- oldText: string,
111
- ): { found: boolean; index: number; matchLength: number; usedFuzzyMatch: boolean } {
112
- const exactIndex = content.indexOf(oldText);
113
- if (exactIndex !== -1) {
114
- return { found: true, index: exactIndex, matchLength: oldText.length, usedFuzzyMatch: false };
115
- }
116
-
117
- const normalizedNeedle = normalizeForFuzzyMatch(oldText);
118
- if (!normalizedNeedle.length) return { found: false, index: -1, matchLength: 0, usedFuzzyMatch: false };
119
-
120
- const { normalized, indexMap } = buildNormalizedWithMap(content);
121
- const normalizedIndex = normalized.indexOf(normalizedNeedle);
122
- if (normalizedIndex === -1) {
123
- return { found: false, index: -1, matchLength: 0, usedFuzzyMatch: false };
124
- }
125
-
126
- const mapped = mapNormalizedSpanToOriginal(indexMap, normalizedIndex, normalizedNeedle.length);
127
- if (!mapped) {
128
- return { found: false, index: -1, matchLength: 0, usedFuzzyMatch: false };
129
- }
130
-
131
- return { found: true, index: mapped.index, matchLength: mapped.matchLength, usedFuzzyMatch: true };
132
- }
133
-
134
- /**
135
- * Replace `oldText` with `newText` in `content`.
136
- * Fuzzy matching only determines target spans; replacement always applies to
137
- * the original content (never normalizes the whole file).
138
- */
139
- export type ReplaceTextResult = { content: string; count: number; usedFuzzyMatch: boolean };
140
-
141
- export function replaceText(
142
- content: string,
143
- oldText: string,
144
- newText: string,
145
- opts: { all?: boolean; fuzzy?: boolean },
146
- ): ReplaceTextResult {
147
- if (!oldText.length) return { content, count: 0, usedFuzzyMatch: false };
148
- const normalizedNew = normalizeToLF(newText);
149
-
150
- if (opts.all) {
151
- const exactCount = content.split(oldText).length - 1;
152
- if (exactCount > 0) {
153
- return { content: content.split(oldText).join(normalizedNew), count: exactCount, usedFuzzyMatch: false };
154
- }
155
- if (!opts.fuzzy) return { content, count: 0, usedFuzzyMatch: false };
156
-
157
- const normalizedNeedle = normalizeForFuzzyMatch(oldText);
158
- if (!normalizedNeedle.length) return { content, count: 0, usedFuzzyMatch: false };
159
-
160
- const { normalized, indexMap } = buildNormalizedWithMap(content);
161
- const spans: Array<{ index: number; matchLength: number }> = [];
162
- let searchFrom = 0;
163
-
164
- while (searchFrom <= normalized.length - normalizedNeedle.length) {
165
- const pos = normalized.indexOf(normalizedNeedle, searchFrom);
166
- if (pos === -1) break;
167
- const mapped = mapNormalizedSpanToOriginal(indexMap, pos, normalizedNeedle.length);
168
- if (mapped) {
169
- const prev = spans[spans.length - 1];
170
- if (!prev || mapped.index >= prev.index + prev.matchLength) {
171
- spans.push(mapped);
172
- }
173
- }
174
- searchFrom = pos + Math.max(1, normalizedNeedle.length);
175
- }
176
-
177
- if (!spans.length) return { content, count: 0, usedFuzzyMatch: false };
178
-
179
- let out = content;
180
- for (let i = spans.length - 1; i >= 0; i--) {
181
- const span = spans[i]!;
182
- out = out.substring(0, span.index) + normalizedNew + out.substring(span.index + span.matchLength);
183
- }
184
- return { content: out, count: spans.length, usedFuzzyMatch: true };
185
- }
186
-
187
- const exactIndex = content.indexOf(oldText);
188
- if (exactIndex !== -1) {
189
- return {
190
- content: content.substring(0, exactIndex) + normalizedNew + content.substring(exactIndex + oldText.length),
191
- count: 1,
192
- usedFuzzyMatch: false,
193
- };
194
- }
195
-
196
- if (!opts.fuzzy) return { content, count: 0, usedFuzzyMatch: false };
197
-
198
- const result = fuzzyFindText(content, oldText);
199
- if (!result.found) return { content, count: 0, usedFuzzyMatch: false };
200
-
201
- return {
202
- content: content.substring(0, result.index) + normalizedNew + content.substring(result.index + result.matchLength),
203
- count: 1,
204
- usedFuzzyMatch: result.usedFuzzyMatch,
205
- };
206
- }
207
-
208
-
209
- export function generateDiffString(
210
- oldContent: string,
211
- newContent: string,
212
- contextLines = 4,
213
- ): { diff: string; firstChangedLine: number | undefined } {
214
- const parts = Diff.diffLines(oldContent, newContent);
215
- const output: string[] = [];
216
- const maxLineNum = Math.max(oldContent.split("\n").length, newContent.split("\n").length);
217
- const lineNumWidth = String(maxLineNum).length;
218
- let oldLineNum = 1;
219
- let newLineNum = 1;
220
- let lastWasChange = false;
221
- let firstChangedLine: number | undefined;
222
-
223
- for (let i = 0; i < parts.length; i++) {
224
- const part = parts[i]!;
225
- const raw = part.value.split("\n");
226
- if (raw[raw.length - 1] === "") raw.pop();
227
-
228
- if (part.added || part.removed) {
229
- if (firstChangedLine === undefined) firstChangedLine = newLineNum;
230
- for (const line of raw) {
231
- if (part.added) {
232
- output.push(`+${String(newLineNum).padStart(lineNumWidth, " ")} ${line}`);
233
- newLineNum++;
234
- } else {
235
- output.push(`-${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
236
- oldLineNum++;
237
- }
238
- }
239
- lastWasChange = true;
240
- continue;
241
- }
242
-
243
- const nextPartIsChange = i < parts.length - 1 && (parts[i + 1]!.added || parts[i + 1]!.removed);
244
- if (lastWasChange || nextPartIsChange) {
245
- let linesToShow = raw;
246
- let skipStart = 0;
247
- let skipEnd = 0;
248
-
249
- if (!lastWasChange) {
250
- skipStart = Math.max(0, raw.length - contextLines);
251
- linesToShow = raw.slice(skipStart);
252
- }
253
- if (!nextPartIsChange && linesToShow.length > contextLines) {
254
- skipEnd = linesToShow.length - contextLines;
255
- linesToShow = linesToShow.slice(0, contextLines);
256
- }
257
-
258
- if (skipStart > 0) {
259
- output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
260
- oldLineNum += skipStart;
261
- newLineNum += skipStart;
262
- }
263
- for (const line of linesToShow) {
264
- output.push(` ${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
265
- oldLineNum++;
266
- newLineNum++;
267
- }
268
- if (skipEnd > 0) {
269
- output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
270
- oldLineNum += skipEnd;
271
- newLineNum += skipEnd;
272
- }
273
- } else {
274
- oldLineNum += raw.length;
275
- newLineNum += raw.length;
276
- }
277
- lastWasChange = false;
278
- }
279
-
280
- return { diff: output.join("\n"), firstChangedLine };
281
- }
282
-
283
- /**
284
- * Generate a compact diff for single-line edits, or fall back to the full diff.
285
- *
286
- * - Single-line replacement: `LINE:HASH|old → LINE:HASH|new`
287
- * - Single-line deletion: `LINE:HASH|old → [deleted]`
288
- * - Multi-line changes: full output from generateDiffString()
289
- */
290
- export function generateCompactOrFullDiff(
291
- oldContent: string,
292
- newContent: string,
293
- contextLines = 4,
294
- ): { diff: string; firstChangedLine: number | undefined } {
295
- if (oldContent === newContent) return { diff: "", firstChangedLine: undefined };
296
-
297
- const oldLines = oldContent.split("\n");
298
- const newLines = newContent.split("\n");
299
-
300
- // Case 1: Same line count, exactly one changed line → compact replacement.
301
- if (oldLines.length === newLines.length) {
302
- let changedIndex = -1;
303
- let changeCount = 0;
304
-
305
- for (let i = 0; i < oldLines.length; i++) {
306
- if (oldLines[i] !== newLines[i]) {
307
- changedIndex = i;
308
- changeCount++;
309
- if (changeCount > 1) break;
310
- }
311
- }
312
-
313
- if (changeCount === 1 && changedIndex >= 0) {
314
- const lineNum = changedIndex + 1;
315
- const oldLine = oldLines[changedIndex] ?? "";
316
- const newLine = newLines[changedIndex] ?? "";
317
- const oldHash = computeLineHash(oldLine);
318
- const newHash = computeLineHash(newLine);
319
- return {
320
- diff: `${lineNum}:${oldHash}|${oldLine} → ${lineNum}:${newHash}|${newLine}`,
321
- firstChangedLine: lineNum,
322
- };
323
- }
324
- }
325
-
326
- // Case 2: Exactly one line deleted.
327
- // old has one more line than new, and removing a single line makes them equal.
328
- if (oldLines.length === newLines.length + 1) {
329
- let deletedIndex = -1;
330
- let j = 0;
331
- let failed = false;
332
-
333
- for (let i = 0; i < oldLines.length; i++) {
334
- if (j < newLines.length && oldLines[i] === newLines[j]) {
335
- j++;
336
- continue;
337
- }
338
- if (deletedIndex === -1) {
339
- deletedIndex = i;
340
- continue;
341
- }
342
- failed = true;
343
- break;
344
- }
345
-
346
- if (!failed && deletedIndex !== -1 && j === newLines.length) {
347
- const lineNum = deletedIndex + 1;
348
- const oldLine = oldLines[deletedIndex] ?? "";
349
- const oldHash = computeLineHash(oldLine);
350
- return {
351
- diff: `${lineNum}:${oldHash}|${oldLine} → [deleted]`,
352
- firstChangedLine: lineNum,
353
- };
354
- }
355
- }
356
-
357
- // Fall back to the full (existing) diff format.
358
- return generateDiffString(oldContent, newContent, contextLines);
359
- }
@@ -1,110 +0,0 @@
1
- import { countEditTypes, parseDiffStats } from "./edit-render-helpers.js";
2
- import { buildReadSeekEditResult, type SemanticSummary } from "./readseek-value.js";
3
-
4
- import type { DiffData } from "./diff-data.js";
5
- export interface BuildEditOutputInput {
6
- path: string;
7
- displayPath: string;
8
- diff: string;
9
- patch?: string;
10
- diffData?: DiffData;
11
- firstChangedLine: number | undefined;
12
- warnings: string[];
13
- noopEdits: unknown[];
14
- semanticSummary?: SemanticSummary;
15
- edits?: unknown[];
16
- }
17
- export interface EditOutputResult {
18
- text: string;
19
- patch: string;
20
- readSeekValue: ReturnType<typeof buildReadSeekEditResult>;
21
- }
22
-
23
- const EDIT_OPERATION_NAMES = ["set_line", "replace_lines", "insert_after", "replace"] as const;
24
-
25
- type EditOperationName = (typeof EDIT_OPERATION_NAMES)[number];
26
-
27
- type EditOperationWithNewText = {
28
- new_text: string;
29
- };
30
-
31
- function hasNewText(value: unknown): value is EditOperationWithNewText {
32
- return typeof value === "object" && value !== null && typeof (value as { new_text?: unknown }).new_text === "string";
33
- }
34
-
35
- function getVisibleDiffStats(diff: string): { added: number; removed: number } {
36
- const stats = parseDiffStats(diff);
37
- if (stats.added > 0 || stats.removed > 0) return stats;
38
- if (!diff.includes("→")) return stats;
39
- if (diff.includes("→ [deleted]")) return { added: 0, removed: 1 };
40
- return { added: 1, removed: 1 };
41
- }
42
- function buildVisibleSummary(displayPath: string, diff: string, edits: unknown[] | undefined): string {
43
- let stats = getVisibleDiffStats(diff);
44
- const counts = countEditTypes(edits);
45
- const editCount = counts.total || 1;
46
-
47
- if (
48
- counts.total > 0 &&
49
- counts.insert_after === counts.total &&
50
- stats.removed > 0 &&
51
- stats.added === stats.removed + counts.insert_after
52
- ) {
53
- stats = { added: counts.insert_after, removed: 0 };
54
- }
55
-
56
- const changeWord = editCount === 1 ? "change" : "changes";
57
- const changedLineCount = Math.max(stats.added, stats.removed);
58
- const lineWord = changedLineCount === 1 ? "line" : "lines";
59
- return `Edited ${displayPath} (${editCount} ${changeWord}, +${stats.added} -${stats.removed} ${lineWord})`;
60
- }
61
- function extractNewTextValues(edits: unknown[] | undefined): string[] {
62
- const values: string[] = [];
63
- for (const edit of edits ?? []) {
64
- if (!edit || typeof edit !== "object") continue;
65
-
66
- const operations = edit as Partial<Record<EditOperationName, unknown>>;
67
- for (const operationName of EDIT_OPERATION_NAMES) {
68
- const operation = operations[operationName];
69
- if (hasNewText(operation)) values.push(operation.new_text);
70
- }
71
- }
72
- return values;
73
- }
74
- function formatWhitespaceOnlyWarning(semanticSummary: SemanticSummary | undefined, edits: unknown[] | undefined): string | undefined {
75
- if (semanticSummary?.classification !== "whitespace-only") return undefined;
76
- if (!extractNewTextValues(edits).some((text) => /\S/.test(text))) return undefined;
77
- return "⚠ Edit classified as whitespace-only — if you intended a behavior change, re-read to verify.";
78
- }
79
- function formatReplaceHint(edits: unknown[] | undefined, noopEdits: unknown[]): string | undefined {
80
- if ((noopEdits ?? []).length > 0) return undefined;
81
- const counts = countEditTypes(edits);
82
- if (counts.replace === 0) return undefined;
83
- if (counts.replace !== counts.total) return undefined;
84
- return "[info: this edit used replace (unverified). For safer future edits, prefer set_line/replace_lines with an anchor from readSeek_read/readSeek_grep/readSeek_search.]";
85
- }
86
- export function buildEditOutput(input: BuildEditOutputInput): EditOutputResult {
87
- const summary = `Updated ${input.displayPath}`;
88
- const visibleSummary = buildVisibleSummary(input.displayPath, input.diff, input.edits);
89
- const semanticWarning = formatWhitespaceOnlyWarning(input.semanticSummary, input.edits);
90
- const warningText = input.warnings.length ? `\n\nWarnings:\n${input.warnings.join("\n")}` : "";
91
- const replaceHint = formatReplaceHint(input.edits, input.noopEdits);
92
- let text = visibleSummary;
93
- if (semanticWarning) text += `\n${semanticWarning}`;
94
- text += warningText;
95
- if (replaceHint) text += `\n${replaceHint}`;
96
- return {
97
- text,
98
- patch: input.patch ?? "",
99
- readSeekValue: buildReadSeekEditResult({
100
- path: input.path,
101
- summary,
102
- diff: input.diff,
103
- ...(input.diffData ? { diffData: input.diffData } : {}),
104
- firstChangedLine: input.firstChangedLine,
105
- warnings: input.warnings,
106
- noopEdits: input.noopEdits,
107
- ...(input.semanticSummary ? { semanticSummary: input.semanticSummary } : {}),
108
- }),
109
- };
110
- }
@@ -1,141 +0,0 @@
1
- const VARIANT_KEYS = ["set_line", "replace_lines", "insert_after", "replace"] as const;
2
-
3
- export type EditTypeCounts = {
4
- set_line: number;
5
- replace_lines: number;
6
- insert_after: number;
7
- replace: number;
8
- total: number;
9
- };
10
-
11
- export function countEditTypes(edits: unknown[] | undefined): EditTypeCounts {
12
- const counts: EditTypeCounts = {
13
- set_line: 0,
14
- replace_lines: 0,
15
- insert_after: 0,
16
- replace: 0,
17
- total: 0,
18
- };
19
- if (!edits) return counts;
20
- for (const edit of edits) {
21
- counts.total++;
22
- if (edit && typeof edit === "object") {
23
- for (const key of VARIANT_KEYS) {
24
- if (key in edit) {
25
- counts[key]++;
26
- break;
27
- }
28
- }
29
- }
30
- }
31
- return counts;
32
- }
33
-
34
- export interface DiffStats {
35
- added: number;
36
- removed: number;
37
- }
38
-
39
- export function parseDiffStats(diff: string | undefined): DiffStats {
40
- if (!diff) return { added: 0, removed: 0 };
41
- let added = 0;
42
- let removed = 0;
43
- for (const line of diff.split("\n")) {
44
- if (line.startsWith("+++") || line.startsWith("---")) continue;
45
- if (line.startsWith("+")) added++;
46
- else if (line.startsWith("-")) removed++;
47
- }
48
- return { added, removed };
49
- }
50
-
51
- export interface EditCallTextResult {
52
- path: string | null;
53
- suffix: string | undefined;
54
- }
55
-
56
- export function formatEditCallText(
57
- args: Record<string, unknown> | undefined,
58
- argsComplete: boolean,
59
- ): EditCallTextResult {
60
- const rawPath = typeof args?.path === "string" ? args.path : null;
61
-
62
- if (!argsComplete) {
63
- return { path: rawPath, suffix: undefined };
64
- }
65
-
66
- const edits = args?.edits;
67
- if (!Array.isArray(edits) || edits.length === 0) {
68
- return { path: rawPath, suffix: undefined };
69
- }
70
-
71
- const counts = countEditTypes(edits);
72
- const parts: string[] = [];
73
- for (const key of VARIANT_KEYS) {
74
- if (counts[key] > 0) {
75
- parts.push(`${counts[key]} ${key}`);
76
- }
77
- }
78
- const word = counts.total === 1 ? "edit" : "edits";
79
- const suffix = `${counts.total} ${word} (${parts.join(", ")})`;
80
- return { path: rawPath, suffix };
81
- }
82
-
83
- export interface EditResultTextInput {
84
- isError: boolean;
85
- diff: string;
86
- warnings: string[];
87
- noopEdits: unknown[];
88
- errorText: string;
89
- semanticClassification?: "no-op" | "whitespace-only" | "semantic" | "mixed";
90
- }
91
-
92
- export interface EditResultTextOutput {
93
- diffStats: string | undefined;
94
- noOp: boolean;
95
- warningsBadge: string | undefined;
96
- errorText: string | undefined;
97
- semanticBadge: string | undefined;
98
- }
99
-
100
- export function formatEditResultText(input: EditResultTextInput): EditResultTextOutput {
101
- const { isError, diff, warnings, noopEdits, errorText } = input;
102
-
103
- // No-op detection: error + noopEdits present OR error text contains "No changes made"
104
- const isNoOp = isError && (
105
- (Array.isArray(noopEdits) && noopEdits.length > 0) ||
106
- errorText.includes("No changes made")
107
- );
108
-
109
- // Diff stats
110
- const stats = parseDiffStats(diff);
111
- const hasDiffStats = stats.added > 0 || stats.removed > 0;
112
- const diffStats = hasDiffStats ? `+${stats.added} / -${stats.removed}` : undefined;
113
-
114
- // Warnings badge
115
- let warningsBadge: string | undefined;
116
- if (warnings.length === 1) {
117
- warningsBadge = "\u26a0 1 warning";
118
- } else if (warnings.length > 1) {
119
- warningsBadge = `\u26a0 ${warnings.length} warnings`;
120
- }
121
-
122
- // Error text (only for errors)
123
- const showErrorText = isError ? errorText : undefined;
124
-
125
- // Semantic classification badge (success only)
126
- let semanticBadge: string | undefined;
127
- if (!isError && !isNoOp && input.semanticClassification) {
128
- switch (input.semanticClassification) {
129
- case "whitespace-only": semanticBadge = "ws-only"; break;
130
- case "semantic": semanticBadge = "\u2713 semantic"; break;
131
- case "mixed": semanticBadge = "mixed"; break;
132
- }
133
- }
134
- return {
135
- diffStats,
136
- noOp: isNoOp,
137
- warningsBadge,
138
- errorText: showErrorText,
139
- semanticBadge,
140
- };
141
- }
@@ -1,65 +0,0 @@
1
- import { readSeekCheck, type ReadSeekCheckOutput, type ReadSeekDiagnostic } from "./readseek-client.js";
2
-
3
- export interface ValidateInput {
4
- filePath: string;
5
- before: string | undefined;
6
- after: string;
7
- }
8
-
9
- export interface ValidateResult {
10
- errorLines: string[];
11
- newErrorCount: number;
12
- newMissingCount: number;
13
- }
14
-
15
- function dedupeSortLines(diagnostics: ReadSeekDiagnostic[]): string[] {
16
- const seen = new Set<string>();
17
- const out: Array<{ key: string; start: number }> = [];
18
- for (const diagnostic of diagnostics) {
19
- const key =
20
- diagnostic.start_line === diagnostic.end_line
21
- ? String(diagnostic.start_line)
22
- : `${diagnostic.start_line}-${diagnostic.end_line}`;
23
- if (!seen.has(key)) {
24
- seen.add(key);
25
- out.push({ key, start: diagnostic.start_line });
26
- }
27
- }
28
- out.sort((a, b) => a.start - b.start);
29
- return out.map((o) => o.key);
30
- }
31
-
32
- const EMPTY: ReadSeekCheckOutput = { errorCount: 0, missingCount: 0, diagnostics: [] };
33
-
34
- /**
35
- * Compare parse diagnostics between `before` and `after` and report any newly
36
- * introduced syntax errors. Uses readseek's native `check`, so every language
37
- * with a tree-sitter parser is validated.
38
- *
39
- * Returns `null` when nothing new appears or when readseek cannot parse the
40
- * file, so a validator failure never blocks an edit.
41
- */
42
- export async function validateSyntaxRegression(
43
- input: ValidateInput,
44
- options: { signal?: AbortSignal } = {},
45
- ): Promise<ValidateResult | null> {
46
- let before: ReadSeekCheckOutput;
47
- let after: ReadSeekCheckOutput;
48
- try {
49
- before = input.before === undefined ? EMPTY : await readSeekCheck(input.filePath, input.before, { signal: options.signal });
50
- after = await readSeekCheck(input.filePath, input.after, { signal: options.signal });
51
- } catch {
52
- return null;
53
- }
54
-
55
- const newErrorCount = Math.max(0, after.errorCount - before.errorCount);
56
- const newMissingCount = Math.max(0, after.missingCount - before.missingCount);
57
-
58
- if (newErrorCount === 0 && newMissingCount === 0) return null;
59
-
60
- return {
61
- errorLines: dedupeSortLines(after.diagnostics),
62
- newErrorCount,
63
- newMissingCount,
64
- };
65
- }