@tyvm/knowhow 0.0.114 → 0.0.115

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.
@@ -0,0 +1,283 @@
1
+ import {
2
+ fixPatch,
3
+ parseHunks,
4
+ hunksToPatch,
5
+ Hunk,
6
+ } from "../../src/agents/tools/patch";
7
+ import { applyPatch } from "diff";
8
+
9
+ describe("Patch Engine - Edge Case Regression Suite", () => {
10
+ // --- Test Case 1: Context Block Desynchronization (Ghost Line Bug) ---
11
+ describe("Error Type 1: Context Block Desynchronization", () => {
12
+ it("should reject context lines polluted by other hunks and find the true fallback anchor", () => {
13
+ const originalFileContent = `import React, { useState } from 'react';\n\nexport default function AuthPage() {\n const [email, setEmail] = useState('');\n const [password, setPassword] = useState('');\n const [confirmPassword, setConfirmPassword] = useState('');\n return <div>Welcome to Knowhow</div>;\n}`;
14
+
15
+ // This patch contains a "Ghost Line" inside the context block:
16
+ // "onChange={handlePasswordChange}" does not exist at this position in the original file
17
+ const corruptedPatch = `@@ -5,4 +5,4 @@\n const [password, setPassword] = useState('');\n const [confirmPassword, setConfirmPassword] = useState('');\n- return <div>Welcome to Knowhow</div>;\n+ return <div>Welcome to Knowhow Engine</div>;\n onChange={handlePasswordChange}`;
18
+
19
+ const fixedPatchOutput = fixPatch(originalFileContent, corruptedPatch);
20
+ expect(fixedPatchOutput).toBeDefined();
21
+ expect(fixedPatchOutput).not.toBe("");
22
+
23
+ // Verify that the patch can now be parsed and successfully applied via standard diff utilities
24
+ const finalApplication = applyPatch(
25
+ originalFileContent,
26
+ fixedPatchOutput
27
+ );
28
+ expect(finalApplication).not.toBe(false);
29
+ expect(finalApplication).toContain("Welcome to Knowhow Engine");
30
+ expect(finalApplication).not.toContain("onChange={handlePasswordChange}");
31
+ });
32
+ });
33
+
34
+ // --- Test Case 2: Greedy Line Collision / Duplicate Suffixes ---
35
+ describe("Error Type 2: Greedy Line Collision (Duplicate Suffixes)", () => {
36
+ it("should correctly isolate and anchor matching structures in files with repeating method boundaries", () => {
37
+ // Common signature parameters that repeat sequentially across multiple distinct blocks
38
+ const schemaFileContent = `export const includedTools = [\n {\n name: "astAppendNode",\n required: ["filePath", "astPath", "newContent"]\n },\n {\n name: "astEditNode",\n required: ["filePath", "astPath", "newContent"]\n }\n];`;
39
+
40
+ // Corrupted patch trying to update the SECOND method wrapper block ("astEditNode")
41
+ // but its context strings match both object items natively.
42
+ const ambiguousPatch = `@@ -6,4 +6,4 @@\n {\n- name: "astEditNode",\n+ name: "astEditNodeModified",\n required: ["filePath", "astPath", "newContent"]\n }`;
43
+
44
+ const fixedPatchOutput = fixPatch(schemaFileContent, ambiguousPatch);
45
+ const hunks = parseHunks(fixedPatchOutput);
46
+
47
+ // Verify that the fixing logic anchored correctly to the target block and didn't corrupt the first item
48
+ const finalApplication = applyPatch(schemaFileContent, fixedPatchOutput);
49
+ expect(finalApplication).not.toBe(false);
50
+ expect(finalApplication).toContain('"astAppendNode"');
51
+ expect(finalApplication).toContain('"astEditNodeModified"');
52
+ });
53
+ });
54
+
55
+ // --- Test Case 3: Multiline Hunk Structural Manipulation & Range Counts ---
56
+ describe("Error Type 3: Multiline Hunk Structural Manipulation", () => {
57
+ it("should recalculate valid hunk metadata ranges (+x,y) when code structural look blocks morph sizes", () => {
58
+ const originalSource = `function compute(data) {\n const item = data.value;\n if (item) {\n return item;\n }\n return null;\n}`;
59
+
60
+ // Patch attempts a structural change (wrapping logic into a comprehensive nested structure)
61
+ // but the line counts inside the header definition are severely corrupted.
62
+ const brokenRangePatch = `@@ -3,3 +3,200 @@\n if (item) {\n- return item;\n+ try {\n+ return item.process();\n+ } catch (e) {\n+ return null;\n+ }\n }`;
63
+
64
+ const fixedPatchOutput = fixPatch(originalSource, brokenRangePatch);
65
+ const parsedHunks = parseHunks(fixedPatchOutput);
66
+
67
+ expect(parsedHunks.length).toBeGreaterThan(0);
68
+ // Ensure the line counts have been normalized to reflect the correct sizing changes
69
+ expect(parsedHunks[0].newLineCount).toBe(8); // 2 context before + 5 inside try/catch + 1 context after
70
+
71
+ const finalApplication = applyPatch(originalSource, fixedPatchOutput);
72
+ expect(finalApplication).not.toBe(false);
73
+ expect(finalApplication).toContain("try {");
74
+ });
75
+ });
76
+
77
+ // --- Test Case 4: Duplicate Anchor Blocks / Loop Comment Ambiguity ---
78
+ describe("Error Type 4: Shared Comment / Anchor Block Ambiguity", () => {
79
+ it("should utilize deeper multi-line context scopes to avoid getting hijacked by identical comments or loops", () => {
80
+ const multiLoopSource = `// Process properties loop\nfor (const [key, value] of Object.entries(data)) {\n console.log(key);\n}\n\n// Process properties loop\nfor (const [key, value] of Object.entries(dataToCompress)) {\n this.compress(value);\n}`;
81
+
82
+ // Patch intends to target the SECOND loop structure, but relies on a generic loop comment as an anchor
83
+ const misplacedPatch = `@@ -6,3 +6,3 @@\n // Process properties loop\n-for (const [key, value] of Object.entries(dataToCompress)) {\n+for (const [key, value] of Object.entries(dataToCompress)).map(([key, value]) => {\n this.compress(value);`;
84
+
85
+ const fixedPatchOutput = fixPatch(multiLoopSource, misplacedPatch);
86
+ const finalApplication = applyPatch(multiLoopSource, fixedPatchOutput);
87
+
88
+ expect(finalApplication).not.toBe(false);
89
+ // Ensure the first loop was preserved completely intact
90
+ expect(finalApplication).toContain("console.log(key);");
91
+ // Ensure the second loop was targeted accurately
92
+ expect(finalApplication).toContain("Object.entries(dataToCompress)).map");
93
+ });
94
+ });
95
+
96
+ // --- Test Case 5: Empty File Initialization Support ---
97
+ describe("Error Type 5: Empty and Baseline Structural Edge Cases", () => {
98
+ it("should successfully generate and validate headers when initializing changes on an empty source baseline file", () => {
99
+ const originalFileContent = "";
100
+ const creationPatch = `@@ -0,0 +1,3 @@\n+export type Option = {\n+ label: string;\n+ value: string;\n+}`;
101
+
102
+ const fixedPatchOutput = fixPatch(originalFileContent, creationPatch);
103
+ const finalApplication = applyPatch(
104
+ originalFileContent,
105
+ fixedPatchOutput
106
+ );
107
+
108
+ expect(finalApplication).not.toBe(false);
109
+ expect(finalApplication).toContain("export type Option");
110
+ });
111
+
112
+ it("should gracefully handle patches targeting an absolute trailing EOF with no trailing newline characters", () => {
113
+ const sourceNoNewline = `const host = process.env.HOST || "localhost";\nconst port = process.env.PORT || 4000;`;
114
+ const eofPatch = `@@ -2,2 +2,3 @@\n const port = process.env.PORT || 4000;\n+const httpsPort = Number(port) + 1;`;
115
+
116
+ const fixedPatchOutput = fixPatch(sourceNoNewline, eofPatch);
117
+ const finalApplication = applyPatch(sourceNoNewline, fixedPatchOutput);
118
+
119
+ expect(finalApplication).not.toBe(false);
120
+ expect(finalApplication).toContain("const httpsPort = Number(port) + 1;");
121
+ });
122
+ });
123
+
124
+ // --- Test Case 6: The Interrupted Context Splitting (Dangling Context Blocks) ---
125
+ describe("Error Type 6: Interrupted Context Splitting", () => {
126
+ it("should handle hunks where context chunks are broken by internal function changes without losing line tracking", () => {
127
+ const originalFileContent = `import { embeddings } from "@knowhow/knowhow";\nimport type { types } from "@knowhow/knowhow";\n\nexport class OrgEmbeddingService {\n async embedSource(organizationId: string, source: types.EmbeddingSource): Promise<types.EmbeddingData[]> {\n if (source.kind === "knowhow-file") {\n const downloadedFiles = await Promise.all(\n source.data.map(async (fileId) => {\n const filePath = await this.downloadOrgFile(organizationId, fileId);\n return { id: fileId, path: filePath };\n })\n );\n }\n }\n}`;
128
+
129
+ // This replicates the failure in OrgEmbedding.ts where context blocks are skipped or compressed mid-stream
130
+ const brokenSplitPatch = `@@ -4,11 +4,12 @@\n import { FilterType } from "../util/types";\n-import type { types } from "@knowhow/knowhow";\n+import { embeddings, EmbeddingSource, EmbeddingData } from "@knowhow/knowhow";\n \n async embedSource(\n organizationId: string,\n- source: types.EmbeddingSource\n- ): Promise<types.EmbeddingData[]> {\n+ source: EmbeddingSource\n+ ): Promise<EmbeddingData[]> {\n if (source.kind === "knowhow-file") {`;
131
+
132
+ const fixedPatchOutput = fixPatch(originalFileContent, brokenSplitPatch);
133
+ expect(fixedPatchOutput).toBeDefined();
134
+ expect(fixedPatchOutput).not.toBe("");
135
+
136
+ const finalApplication = applyPatch(
137
+ originalFileContent,
138
+ fixedPatchOutput
139
+ );
140
+ expect(finalApplication).not.toBe(false);
141
+ expect(finalApplication).toContain("source: EmbeddingSource");
142
+ });
143
+ });
144
+
145
+ // --- Test Case 7: Complex Multi-Hunk Re-Ordering Offset Shifts ---
146
+ describe("Error Type 7: Multi-Hunk Re-Ordering Offset Shifts", () => {
147
+ it("should accurately compute subsequent hunk lines when an earlier hunk has altered the global row layout index", () => {
148
+ const componentContent = `import React, { useState } from 'react';\nimport { Button } from "@/components/ui/button";\n\nexport function AccountSwitcher() {\n const [isOpen, setIsOpen] = useState(false);\n const handleCreateOrg = () => {\n const name = prompt("Enter name:");\n };\n return (\n <Button onClick={handleCreateOrg}>Create</Button>\n );\n}`;
149
+
150
+ // A single patch with MULTIPLE hunks where Hunk 1 introduces an offset drift
151
+ // that shifts the physical line target numbers for Hunk 2 down the file stream.
152
+ const multiHunkPatch = `@@ -1,4 +1,5 @@\n import React, { useState } from 'react';\n+import { useToast } from "@/hooks/use-toast";\n import { Button } from "@/components/ui/button";\n@@ -6,4 +7,6 @@\n const handleCreateOrg = () => {\n+ const { toast } = useToast();\n const name = prompt("Enter name:");\n+ toast({ title: "Success" });\n };`;
153
+
154
+ const fixedPatchOutput = fixPatch(componentContent, multiHunkPatch);
155
+ expect(fixedPatchOutput).toBeDefined();
156
+
157
+ const finalApplication = applyPatch(componentContent, fixedPatchOutput);
158
+ expect(finalApplication).not.toBe(false);
159
+ expect(finalApplication).toContain("const { toast } = useToast();");
160
+ });
161
+ });
162
+
163
+ // --- Test Case 8: Subtraction Block Redundancy Over-Matching ---
164
+ describe("Error Type 8: Subtraction Block Redundancy Over-Matching", () => {
165
+ it("should isolate the exact segment to modify even when deletions match completely duplicate layout states elsewhere in the file", () => {
166
+ // Replicates the problem inside InputQueueManager.ts where identical sync logic definitions
167
+ // appear sequentially or closely together inside the class body definition.
168
+ const duplicatedLinesSource = `if (key?.name === "tab") {\n setImmediate(() => {\n this.currentLine = (this.rl as any).line ?? "";\n });\n}\nif (!this.rl || this.stack.length === 0) return;\nthis.currentLine = (this.rl as any).line ?? "";`;
169
+
170
+ const redundantPatch = `@@ -6,3 +6,5 @@\n if (!this.rl || this.stack.length === 0) return;\n-this.currentLine = (this.rl as any).line ?? "";\n+if (key?.name !== "tab") {\n+ this.currentLine = (this.rl as any).line ?? "";\n+}`;
171
+
172
+ const fixedPatchOutput = fixPatch(duplicatedLinesSource, redundantPatch);
173
+ const finalApplication = applyPatch(
174
+ duplicatedLinesSource,
175
+ fixedPatchOutput
176
+ );
177
+
178
+ expect(finalApplication).not.toBe(false);
179
+ // Ensure the first assignment block wasn't touched or stripped out by accident
180
+ expect(finalApplication).toContain("setImmediate(() => {");
181
+ // Ensure the conditional wrapper applied cleanly on the second loop target block
182
+ expect(finalApplication).toContain('if (key?.name !== "tab") {');
183
+ });
184
+ });
185
+
186
+ // --- Test Case 9: Pure Insertion (No Deletions) Count Recalculation ---
187
+ describe("Error Type 9: Pure Insertion Hunk Reconstruction", () => {
188
+ it("should output a precise target header count when a hunk contains zero subtractions and only insertions", () => {
189
+ const baselineContent = `import { services } from "../services";\nimport { patchFile } from "./patch";\n\nexport async function run() {}`;
190
+
191
+ // A patch containing an insertion with a line count error in the header metadata
192
+ const pureInsertionPatch = `@@ -2,0 +3,50 @@\n import { patchFile } from "./patch";\n+import { lintFile } from "./lint";\n export async function run() {}`;
193
+
194
+ const fixedPatchOutput = fixPatch(baselineContent, pureInsertionPatch);
195
+ const parsedHunks = parseHunks(fixedPatchOutput);
196
+
197
+ expect(parsedHunks.length).toBeGreaterThan(0);
198
+
199
+ // Standard unified diff dictates that if original count is 0, the line reference
200
+ // should point to the line *before* the insertion point.
201
+ // Let's verify our engine didn't carry over the broken ",50" insertion metadata count.
202
+ expect(fixedPatchOutput).not.toContain("+3,50");
203
+
204
+ const finalApplication = applyPatch(baselineContent, fixedPatchOutput);
205
+ expect(finalApplication).not.toBe(false);
206
+ expect(finalApplication).toContain('import { lintFile } from "./lint";');
207
+ });
208
+ });
209
+
210
+ // --- Test Case 10: Pure Deletion (No Additions) Header Simplification ---
211
+ describe("Error Type 10: Pure Deletion Header Simplification", () => {
212
+ it("should simplify or format line counts cleanly when an entry block is stripped completely out of the source", () => {
213
+ const sourceWithUnusedImports = `import { services } from "../services";\nimport { unusedUtil } from "./utils";\n\nexport class Worker {}`;
214
+
215
+ // A patch that drops an import statement but provides broken target line count offsets
216
+ const pureDeletionPatch = `@@ -2,1 +2,0 @@\n-import { unusedUtil } from "./utils";`;
217
+
218
+ const fixedPatchOutput = fixPatch(
219
+ sourceWithUnusedImports,
220
+ pureDeletionPatch
221
+ );
222
+ const finalApplication = applyPatch(
223
+ sourceWithUnusedImports,
224
+ fixedPatchOutput
225
+ );
226
+
227
+ expect(finalApplication).not.toBe(false);
228
+ expect(finalApplication).not.toContain("unusedUtil");
229
+ expect(finalApplication).toContain("export class Worker");
230
+ });
231
+ });
232
+
233
+ // --- Test Case 11: Indentation Style Divergence ---
234
+ describe("Error Type 11: Indentation Style Divergence", () => {
235
+ it("should gracefully align and apply patches where the LLM altered leading spaces or tabs in unchanged context lines", () => {
236
+ const originalFileContent = `class Server {\n constructor() {\n this.port = 3000;\n }\n}`;
237
+
238
+ // Notice the context lines have mixed 2-space indents instead of the original 4/8 space layout
239
+ const messyIndentPatch = `@@ -2,3 +2,3 @@\n constructor() {\n- this.port = 3000;\n+ this.port = 8080;\n }`;
240
+
241
+ const fixedPatchOutput = fixPatch(originalFileContent, messyIndentPatch);
242
+ const finalApplication = applyPatch(
243
+ originalFileContent,
244
+ fixedPatchOutput
245
+ );
246
+
247
+ expect(finalApplication).not.toBe(false);
248
+ expect(finalApplication).toContain("this.port = 8080;");
249
+ });
250
+ });
251
+
252
+ // --- Test Case 12: Interleaved Twin-Block Collisions ---
253
+ describe("Error Type 12: Interleaved Twin-Block Collisions", () => {
254
+ it("should maintain perfect boundary tracking when multiple interleaved modifications are separated by single, repeating keywords", () => {
255
+ const complexTwinSource = `setup();\n// Section One\ninit();\n// Section Two\ninit();\ncleanup();`;
256
+
257
+ // Interleaved changes mutating both identical-looking method assignments across a shared keyword line
258
+ const twinHunkPatch = `@@ -2,5 +2,5 @@\n // Section One\n-init();\n+initPrimary();\n // Section Two\n-init();\n+initSecondary();`;
259
+
260
+ const fixedPatchOutput = fixPatch(complexTwinSource, twinHunkPatch);
261
+ const finalApplication = applyPatch(complexTwinSource, fixedPatchOutput);
262
+
263
+ expect(finalApplication).not.toBe(false);
264
+ expect(finalApplication).toContain("initPrimary();");
265
+ expect(finalApplication).toContain("initSecondary();");
266
+ });
267
+ });
268
+
269
+ // --- Test Case 13: Carriage Return CRLF Normalization ---
270
+ describe("Error Type 13: Carriage Return CRLF Normalization", () => {
271
+ it("should preserve original line ending styles without crashing when running patches on strict CRLF text files", () => {
272
+ const winFile = "const a = 1;\r\nconst b = 2;\r\nconst c = 3;\r\n";
273
+ const unixPatch =
274
+ "@@ -2,2 +2,2 @@\n const b = 2;\n-const c = 3;\n+const c = 4;\n";
275
+
276
+ const fixedPatchOutput = fixPatch(winFile, unixPatch);
277
+ const finalApplication = applyPatch(winFile, fixedPatchOutput);
278
+
279
+ expect(finalApplication).not.toBe(false);
280
+ expect(finalApplication).toContain("const c = 4;");
281
+ });
282
+ });
283
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.114",
3
+ "version": "0.0.115",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -196,6 +196,9 @@ function hunkIsEmpty(hunk) {
196
196
  const CONTEXT_LINES = 3;
197
197
  function fixSingleHunk(hunk, originalContent) {
198
198
  const originalLines = (0, utils_1.splitByNewLines)(originalContent);
199
+ if (originalContent === "" && hunk.originalStartLine === 0 && hunk.originalLineCount === 0) {
200
+ return hunk;
201
+ }
199
202
  const deletionLinesContent = hunk.subtractions.map((l) => l.slice(1));
200
203
  const additionLinesContent = hunk.additions.map((l) => l.slice(1));
201
204
  let actualOriginalStartLine = -1;
@@ -205,6 +208,14 @@ function fixSingleHunk(hunk, originalContent) {
205
208
  actualOriginalStartLine = deletionStartIndex + 1;
206
209
  console.log(`Anchor found via deletion sequence at line ${actualOriginalStartLine}`);
207
210
  }
211
+ if (actualOriginalStartLine === -1) {
212
+ const firstDeletionLines = findAllLineNumbers(originalContent, deletionLinesContent[0]);
213
+ const closest = findClosestNumber(firstDeletionLines, hunk.originalStartLine);
214
+ if (closest !== undefined) {
215
+ actualOriginalStartLine = closest;
216
+ console.log(`Anchor found via first deletion line at line ${actualOriginalStartLine}`);
217
+ }
218
+ }
208
219
  }
209
220
  if (actualOriginalStartLine === -1 &&
210
221
  deletionLinesContent.length === 0 &&
@@ -257,29 +268,237 @@ function fixSingleHunk(hunk, originalContent) {
257
268
  }
258
269
  }
259
270
  actualOriginalStartLine = Math.max(1, actualOriginalStartLine);
260
- const contextBeforeStartLine = Math.max(0, actualOriginalStartLine - CONTEXT_LINES - 1);
261
- const contextBeforeEndLine = Math.max(0, actualOriginalStartLine - 1);
262
- const contextBefore = originalLines
263
- .slice(contextBeforeStartLine, contextBeforeEndLine)
264
- .map((l) => ` ${l}`);
271
+ let hasInterleavedChanges = false;
272
+ let seenChange = false;
273
+ let seenContextAfterChange = false;
274
+ for (const line of hunk.lines) {
275
+ if (line.startsWith("+") || line.startsWith("-")) {
276
+ if (seenContextAfterChange) {
277
+ hasInterleavedChanges = true;
278
+ break;
279
+ }
280
+ seenChange = true;
281
+ }
282
+ else if (line.startsWith(" ") && seenChange) {
283
+ seenContextAfterChange = true;
284
+ }
285
+ }
286
+ if (seenContextAfterChange) {
287
+ for (const line of hunk.lines.slice(hunk.lines.findIndex((l, i) => {
288
+ let sc = false;
289
+ for (let j = 0; j <= i; j++) {
290
+ if (hunk.lines[j].startsWith("+") || hunk.lines[j].startsWith("-"))
291
+ sc = true;
292
+ if (sc && hunk.lines[j].startsWith(" ") && j === i)
293
+ return true;
294
+ }
295
+ return false;
296
+ }))) {
297
+ if (line.startsWith("+") || line.startsWith("-")) {
298
+ hasInterleavedChanges = true;
299
+ break;
300
+ }
301
+ }
302
+ }
303
+ if (hasInterleavedChanges) {
304
+ const blocks = [];
305
+ let curBlock = { deletions: [], additions: [] };
306
+ let inBlock = false;
307
+ for (const line of hunk.lines) {
308
+ if (line.startsWith("-")) {
309
+ curBlock.deletions.push(line.slice(1));
310
+ inBlock = true;
311
+ }
312
+ else if (line.startsWith("+")) {
313
+ curBlock.additions.push(line.slice(1));
314
+ inBlock = true;
315
+ }
316
+ else if (inBlock) {
317
+ blocks.push(curBlock);
318
+ curBlock = { deletions: [], additions: [] };
319
+ inBlock = false;
320
+ }
321
+ }
322
+ if (inBlock || curBlock.deletions.length > 0 || curBlock.additions.length > 0)
323
+ blocks.push(curBlock);
324
+ let resultLines = [...originalLines];
325
+ let lineOffset = 0;
326
+ let anyApplied = false;
327
+ for (const block of blocks) {
328
+ if (block.deletions.length === 0)
329
+ continue;
330
+ const seqIdx = findSequenceIndex(resultLines, block.deletions);
331
+ if (seqIdx !== -1) {
332
+ resultLines = [
333
+ ...resultLines.slice(0, seqIdx),
334
+ ...block.additions,
335
+ ...resultLines.slice(seqIdx + block.deletions.length),
336
+ ];
337
+ lineOffset += block.additions.length - block.deletions.length;
338
+ anyApplied = true;
339
+ continue;
340
+ }
341
+ const delContent = block.deletions.join(" ").trim();
342
+ const addContent = block.additions.join(" ").trim();
343
+ const matchIdx = resultLines.findIndex((l) => l.includes(delContent.split(" ")[0]) && block.deletions.every((d) => l.includes(d.trim())));
344
+ if (matchIdx !== -1) {
345
+ let newLine = resultLines[matchIdx];
346
+ for (let i = 0; i < block.deletions.length; i++) {
347
+ newLine = newLine.replace(block.deletions[i].trim(), block.additions[i]?.trim() ?? "");
348
+ }
349
+ resultLines = [...resultLines.slice(0, matchIdx), newLine, ...resultLines.slice(matchIdx + 1)];
350
+ anyApplied = true;
351
+ }
352
+ }
353
+ if (anyApplied) {
354
+ const origStr = originalLines.join("\n");
355
+ const newStr = resultLines.join("\n");
356
+ let firstDiff = 0;
357
+ while (firstDiff < originalLines.length && firstDiff < resultLines.length && originalLines[firstDiff] === resultLines[firstDiff])
358
+ firstDiff++;
359
+ let lastDiffOrig = originalLines.length - 1;
360
+ let lastDiffNew = resultLines.length - 1;
361
+ while (lastDiffOrig > firstDiff && lastDiffNew > firstDiff && originalLines[lastDiffOrig] === resultLines[lastDiffNew]) {
362
+ lastDiffOrig--;
363
+ lastDiffNew--;
364
+ }
365
+ const ctxStart = Math.max(0, firstDiff - 1);
366
+ const ctxEndOrig = Math.min(originalLines.length - 1, lastDiffOrig + 1);
367
+ const ctxEndNew = Math.min(resultLines.length - 1, lastDiffNew + 1);
368
+ const patchLines = [];
369
+ for (let i = ctxStart; i <= ctxEndOrig; i++) {
370
+ if (i >= firstDiff && i <= lastDiffOrig)
371
+ patchLines.push(`-${originalLines[i]}`);
372
+ else
373
+ patchLines.push(` ${originalLines[i]}`);
374
+ }
375
+ const finalLines = [];
376
+ for (let i = ctxStart; i <= ctxEndOrig; i++) {
377
+ if (i >= firstDiff && i <= lastDiffOrig) {
378
+ finalLines.push(`-${originalLines[i]}`);
379
+ }
380
+ else
381
+ finalLines.push(` ${originalLines[i]}`);
382
+ }
383
+ for (let i = firstDiff; i <= lastDiffNew; i++) {
384
+ if (i >= firstDiff && i <= lastDiffNew && (i > lastDiffOrig || originalLines[i] !== resultLines[i])) {
385
+ if (!finalLines.some((l) => l === `+${resultLines[i]}`))
386
+ finalLines.push(`+${resultLines[i]}`);
387
+ }
388
+ }
389
+ const origCount2 = finalLines.filter((l) => !l.startsWith("+")).length;
390
+ const newCount2 = finalLines.filter((l) => !l.startsWith("-")).length;
391
+ const newHeader2 = `@@ -${ctxStart + 1},${origCount2} +${ctxStart + 1},${newCount2} @@`;
392
+ return {
393
+ header: newHeader2,
394
+ originalStartLine: ctxStart + 1,
395
+ originalLineCount: origCount2,
396
+ newStartLine: ctxStart + 1,
397
+ newLineCount: newCount2,
398
+ lines: finalLines,
399
+ additions: finalLines.filter((l) => l.startsWith("+")),
400
+ subtractions: finalLines.filter((l) => l.startsWith("-")),
401
+ contextLines: finalLines.filter((l) => l.startsWith(" ")),
402
+ };
403
+ }
404
+ const validLines = hunk.lines.filter((l) => {
405
+ if (l.startsWith("+") || l.startsWith("-"))
406
+ return true;
407
+ if (!l.startsWith(" ") && l.trim() !== "")
408
+ return false;
409
+ const content = l.startsWith(" ") ? l.slice(1) : l;
410
+ if (content.trim() === "")
411
+ return originalLines.includes(content);
412
+ return originalLines.some((fl) => fl.trim() === content.trim());
413
+ }).map((l) => (!l.startsWith("+") && !l.startsWith("-") && !l.startsWith(" ")) ? ` ${l}` : l);
414
+ const origCount = validLines.filter((l) => !l.startsWith("+")).length;
415
+ const newCount = validLines.filter((l) => !l.startsWith("-")).length;
416
+ const newHeader = `@@ -${actualOriginalStartLine},${origCount} +${actualOriginalStartLine},${newCount} @@`;
417
+ return {
418
+ header: newHeader,
419
+ originalStartLine: actualOriginalStartLine,
420
+ originalLineCount: origCount,
421
+ newStartLine: actualOriginalStartLine,
422
+ newLineCount: newCount,
423
+ lines: validLines,
424
+ additions: hunk.additions,
425
+ subtractions: hunk.subtractions,
426
+ contextLines: validLines.filter((l) => !l.startsWith("+") && !l.startsWith("-")),
427
+ };
428
+ }
429
+ if (deletionLinesContent.length === 0 && additionLinesContent.length > 0) {
430
+ const pureHeader = `@@ -${actualOriginalStartLine},0 +${actualOriginalStartLine},${hunk.additions.length} @@`;
431
+ return {
432
+ header: pureHeader,
433
+ originalStartLine: actualOriginalStartLine,
434
+ originalLineCount: 0,
435
+ newStartLine: actualOriginalStartLine,
436
+ newLineCount: hunk.additions.length,
437
+ lines: hunk.additions,
438
+ additions: hunk.additions,
439
+ subtractions: [],
440
+ contextLines: [],
441
+ };
442
+ }
443
+ const hunkContextBefore = [];
444
+ const hunkContextAfter = [];
445
+ let pastChanges = false;
446
+ for (const line of hunk.lines) {
447
+ if (line.startsWith("+") || line.startsWith("-")) {
448
+ pastChanges = true;
449
+ }
450
+ else if (line.startsWith(" ")) {
451
+ if (!pastChanges)
452
+ hunkContextBefore.push(line);
453
+ else
454
+ hunkContextAfter.push(line);
455
+ }
456
+ }
457
+ const validContextBefore = hunkContextBefore
458
+ .map((l) => {
459
+ const match = originalLines.find((fl) => fl.trim() === l.slice(1).trim() && l.slice(1).trim() !== "");
460
+ return match !== undefined ? ` ${match}` : null;
461
+ })
462
+ .filter((l) => l !== null);
463
+ const validContextAfter = hunkContextAfter
464
+ .map((l) => {
465
+ const match = originalLines.find((fl) => fl.trim() === l.slice(1).trim() && l.slice(1).trim() !== "");
466
+ return match !== undefined ? ` ${match}` : null;
467
+ })
468
+ .filter((l) => l !== null);
469
+ const supplementBeforeIdx = actualOriginalStartLine - 1 - validContextBefore.length - 1;
470
+ const supplementBefore = supplementBeforeIdx >= 0
471
+ ? [` ${originalLines[supplementBeforeIdx]}`]
472
+ : [];
473
+ const contextBefore = [...supplementBefore, ...validContextBefore];
265
474
  const originalContentEndLine = actualOriginalStartLine + deletionLinesContent.length;
266
- const contextAfterStartLine = originalContentEndLine - 1;
267
- const contextAfterEndLine = Math.min(originalLines.length, contextAfterStartLine + CONTEXT_LINES);
268
- const contextAfter = originalLines
269
- .slice(contextAfterStartLine, contextAfterEndLine)
270
- .map((l) => ` ${l}`);
475
+ let contextAfter;
476
+ if (deletionLinesContent.length === 0) {
477
+ const afterIdx = actualOriginalStartLine - 1;
478
+ contextAfter = afterIdx < originalLines.length ? [` ${originalLines[afterIdx]}`] : [];
479
+ }
480
+ else {
481
+ contextAfter = validContextAfter;
482
+ if (contextAfter.length === 0) {
483
+ const afterIdx = originalContentEndLine - 1;
484
+ if (afterIdx < originalLines.length) {
485
+ contextAfter = [` ${originalLines[afterIdx]}`];
486
+ }
487
+ }
488
+ }
489
+ const finalContextBefore = deletionLinesContent.length === 0 ? validContextBefore : contextBefore;
271
490
  const newHunkLines = [
272
- ...contextBefore,
491
+ ...finalContextBefore,
273
492
  ...hunk.subtractions,
274
493
  ...hunk.additions,
275
494
  ...contextAfter,
276
495
  ];
277
- const newOriginalStart = contextBefore.length > 0
278
- ? actualOriginalStartLine - contextBefore.length
496
+ const newOriginalStart = finalContextBefore.length > 0
497
+ ? actualOriginalStartLine - finalContextBefore.length
279
498
  : actualOriginalStartLine;
280
- const newOriginalCount = contextBefore.length + hunk.subtractions.length + contextAfter.length;
499
+ const newOriginalCount = finalContextBefore.length + hunk.subtractions.length + contextAfter.length;
281
500
  const newNewStart = newOriginalStart;
282
- const newNewCount = contextBefore.length + hunk.additions.length + contextAfter.length;
501
+ const newNewCount = finalContextBefore.length + hunk.additions.length + contextAfter.length;
283
502
  const finalOriginalStart = Math.max(1, newOriginalStart);
284
503
  const finalOriginalCount = Math.max(1, newOriginalCount);
285
504
  const finalNewStart = Math.max(1, newNewStart);
@@ -298,7 +517,7 @@ function fixSingleHunk(hunk, originalContent) {
298
517
  lines: newHunkLines,
299
518
  additions: hunk.additions,
300
519
  subtractions: hunk.subtractions,
301
- contextLines: [...contextBefore, ...contextAfter],
520
+ contextLines: [...finalContextBefore, ...contextAfter],
302
521
  };
303
522
  if (hunkIsEmpty(fixedHunk)) {
304
523
  console.log("Hunk became empty after fixing:", fixedHunk.header);