@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.
- package/package.json +1 -1
- package/scripts/test-repetition-hint.ts +82 -14
- package/src/agents/tools/patch.ts +240 -27
- package/src/processors/CustomVariables.ts +51 -2
- package/tests/patching/regression-2026.test.ts +283 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/tools/patch.js +235 -16
- package/ts_build/src/agents/tools/patch.js.map +1 -1
- package/ts_build/src/processors/CustomVariables.d.ts +3 -0
- package/ts_build/src/processors/CustomVariables.js +31 -2
- package/ts_build/src/processors/CustomVariables.js.map +1 -1
- package/ts_build/tests/patching/regression-2026.test.d.ts +1 -0
- package/ts_build/tests/patching/regression-2026.test.js +163 -0
- package/ts_build/tests/patching/regression-2026.test.js.map +1 -0
|
@@ -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
|
+
});
|
package/ts_build/package.json
CHANGED
|
@@ -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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
.
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
.
|
|
270
|
-
|
|
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
|
-
...
|
|
491
|
+
...finalContextBefore,
|
|
273
492
|
...hunk.subtractions,
|
|
274
493
|
...hunk.additions,
|
|
275
494
|
...contextAfter,
|
|
276
495
|
];
|
|
277
|
-
const newOriginalStart =
|
|
278
|
-
? actualOriginalStartLine -
|
|
496
|
+
const newOriginalStart = finalContextBefore.length > 0
|
|
497
|
+
? actualOriginalStartLine - finalContextBefore.length
|
|
279
498
|
: actualOriginalStartLine;
|
|
280
|
-
const newOriginalCount =
|
|
499
|
+
const newOriginalCount = finalContextBefore.length + hunk.subtractions.length + contextAfter.length;
|
|
281
500
|
const newNewStart = newOriginalStart;
|
|
282
|
-
const newNewCount =
|
|
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: [...
|
|
520
|
+
contextLines: [...finalContextBefore, ...contextAfter],
|
|
302
521
|
};
|
|
303
522
|
if (hunkIsEmpty(fixedHunk)) {
|
|
304
523
|
console.log("Hunk became empty after fixing:", fixedHunk.header);
|