@howaboua/pi-codex-conversion 1.0.14 → 1.0.16
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/src/patch/core.ts +2 -1
- package/src/patch/matching.ts +30 -0
- package/src/patch/parser.ts +24 -46
- package/src/tools/apply-patch-tool.ts +48 -2
package/package.json
CHANGED
package/src/patch/core.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
+
import { linesMatch } from "./matching.ts";
|
|
3
4
|
import { parsePatchActions, parseUpdateFile } from "./parser.ts";
|
|
4
5
|
import { openFileAtPath, pathExists, removeFileAtPath, resolvePatchPath, writeFileAtPath } from "./paths.ts";
|
|
5
6
|
import { DiffError, ExecutePatchError, type ExecutePatchResult, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
|
|
@@ -64,7 +65,7 @@ function getUpdatedFile({ text, action, path }: { text: string; action: PatchAct
|
|
|
64
65
|
destIndex += delta;
|
|
65
66
|
|
|
66
67
|
for (const line of chunk.delLines) {
|
|
67
|
-
if (origLines[origIndex]
|
|
68
|
+
if (!linesMatch(origLines[origIndex] ?? "", line)) {
|
|
68
69
|
throw new DiffError(`_get_updated_file: ${path}: Expected ${line} but got ${origLines[origIndex]} at line ${origIndex + 1}`);
|
|
69
70
|
}
|
|
70
71
|
origIndex += 1;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type LinesMatchQuality = {
|
|
2
|
+
fuzz: number;
|
|
3
|
+
worstLineFuzz: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function lineMatchFuzz(left: string, right: string): number | undefined {
|
|
7
|
+
if (left === right) return 0;
|
|
8
|
+
if (left.trimEnd() === right.trimEnd()) return 1;
|
|
9
|
+
if (left.trim() === right.trim()) return 100;
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function linesMatch(left: string, right: string): boolean {
|
|
14
|
+
return left === right || left.trimEnd() === right.trimEnd();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function linesEqualFuzz({ left, right }: { left: string[]; right: string[] }): LinesMatchQuality | undefined {
|
|
18
|
+
if (left.length !== right.length) return undefined;
|
|
19
|
+
|
|
20
|
+
let fuzz = 0;
|
|
21
|
+
let worstLineFuzz = 0;
|
|
22
|
+
for (let index = 0; index < left.length; index++) {
|
|
23
|
+
const lineFuzz = lineMatchFuzz(left[index], right[index]);
|
|
24
|
+
if (lineFuzz === undefined) return undefined;
|
|
25
|
+
fuzz += lineFuzz;
|
|
26
|
+
worstLineFuzz = Math.max(worstLineFuzz, lineFuzz);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { fuzz, worstLineFuzz };
|
|
30
|
+
}
|
package/src/patch/parser.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lineMatchFuzz, linesEqualFuzz } from "./matching.ts";
|
|
1
2
|
import { normalizePatchPath } from "./paths.ts";
|
|
2
3
|
import { DiffError, type Chunk, type ParseMode, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
|
|
3
4
|
|
|
@@ -48,14 +49,6 @@ function splitFileLines(text: string): string[] {
|
|
|
48
49
|
return lines;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
function linesEqual({ left, right }: { left: string[]; right: string[] }): boolean {
|
|
52
|
-
if (left.length !== right.length) return false;
|
|
53
|
-
for (let index = 0; index < left.length; index++) {
|
|
54
|
-
if (left[index] !== right[index]) return false;
|
|
55
|
-
}
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
52
|
function findContextCore({ lines, context, start }: { lines: string[]; context: string[]; start: number }): {
|
|
60
53
|
newIndex: number;
|
|
61
54
|
fuzz: number;
|
|
@@ -64,25 +57,30 @@ function findContextCore({ lines, context, start }: { lines: string[]; context:
|
|
|
64
57
|
return { newIndex: start, fuzz: 0 };
|
|
65
58
|
}
|
|
66
59
|
|
|
67
|
-
for (
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
for (const tier of [0, 1, 100]) {
|
|
61
|
+
for (let index = start; index <= lines.length - context.length; index++) {
|
|
62
|
+
const quality = linesEqualFuzz({ left: lines.slice(index, index + context.length), right: context });
|
|
63
|
+
if (quality?.worstLineFuzz === tier) {
|
|
64
|
+
return { newIndex: index, fuzz: quality.fuzz };
|
|
65
|
+
}
|
|
70
66
|
}
|
|
71
67
|
}
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
return { newIndex: -1, fuzz: 0 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findSectionAnchor({ lines, target, start }: { lines: string[]; target: string; start: number }): { newIndex: number; fuzz: number } {
|
|
73
|
+
for (const tier of [0, 1, 100]) {
|
|
74
|
+
const alreadySeen = lines.slice(0, start).some((line) => lineMatchFuzz(line, target) === tier);
|
|
75
|
+
if (alreadySeen) {
|
|
76
|
+
continue;
|
|
78
77
|
}
|
|
79
|
-
}
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
for (let index = start; index < lines.length; index++) {
|
|
80
|
+
const fuzz = lineMatchFuzz(lines[index], target);
|
|
81
|
+
if (fuzz === tier) {
|
|
82
|
+
return { newIndex: index, fuzz };
|
|
83
|
+
}
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
86
|
|
|
@@ -263,30 +261,10 @@ export function parseUpdateFile({ state, text, path }: { state: ParserState; tex
|
|
|
263
261
|
}
|
|
264
262
|
|
|
265
263
|
if (defStr.trim().length > 0) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
for (let lineIndex = index; lineIndex < lines.length; lineIndex++) {
|
|
271
|
-
if (lines[lineIndex] === defStr) {
|
|
272
|
-
index = lineIndex + 1;
|
|
273
|
-
found = true;
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (!found) {
|
|
280
|
-
const trimAlreadySeen = lines.slice(0, index).some((line) => line.trim() === defStr.trim());
|
|
281
|
-
if (!trimAlreadySeen) {
|
|
282
|
-
for (let lineIndex = index; lineIndex < lines.length; lineIndex++) {
|
|
283
|
-
if (lines[lineIndex].trim() === defStr.trim()) {
|
|
284
|
-
index = lineIndex + 1;
|
|
285
|
-
state.fuzz += 1;
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
264
|
+
const sectionAnchor = findSectionAnchor({ lines, target: defStr, start: index });
|
|
265
|
+
if (sectionAnchor.newIndex !== -1) {
|
|
266
|
+
index = sectionAnchor.newIndex + 1;
|
|
267
|
+
state.fuzz += sectionAnchor.fuzz;
|
|
290
268
|
}
|
|
291
269
|
}
|
|
292
270
|
|
|
@@ -30,6 +30,12 @@ interface ApplyPatchPartialFailureDetails {
|
|
|
30
30
|
result: ExecutePatchResult;
|
|
31
31
|
error: string;
|
|
32
32
|
failedTarget?: string;
|
|
33
|
+
appliedFiles: string[];
|
|
34
|
+
failedFiles: string[];
|
|
35
|
+
recoveryInstructions: {
|
|
36
|
+
mustReadFiles: string[];
|
|
37
|
+
mustNotReadFiles: string[];
|
|
38
|
+
};
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
type ApplyPatchToolDetails = ApplyPatchSuccessDetails | ApplyPatchPartialFailureDetails;
|
|
@@ -177,6 +183,37 @@ function summarizePatchCounts(result: ExecutePatchResult): string {
|
|
|
177
183
|
].join(", ");
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
function uniqueStrings(values: Array<string | undefined>): string[] {
|
|
187
|
+
return Array.from(new Set(values.filter((value): value is string => typeof value === "string" && value.length > 0)));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getFailedPaths(error: ExecutePatchError): string[] {
|
|
191
|
+
if (!error.failedAction) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
return uniqueStrings([
|
|
195
|
+
error.failedAction.path,
|
|
196
|
+
error.failedAction.type === "update" ? error.failedAction.movePath : undefined,
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getAppliedPaths(result: ExecutePatchResult, failedFiles: string[]): string[] {
|
|
201
|
+
return result.changedFiles.filter((path) => !failedFiles.includes(path));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildPartialFailureMessage(message: string, failedFiles: string[], appliedFiles: string[]): string {
|
|
205
|
+
const lines = [message];
|
|
206
|
+
if (failedFiles.length > 0) {
|
|
207
|
+
lines.push(`Failed file${failedFiles.length === 1 ? "" : "s"}: ${failedFiles.join(", ")}`);
|
|
208
|
+
lines.push(`Recovery: MUST read ${failedFiles.join(", ")} before retrying.`);
|
|
209
|
+
}
|
|
210
|
+
if (appliedFiles.length > 0) {
|
|
211
|
+
lines.push("Earlier file actions in this patch were already applied.");
|
|
212
|
+
lines.push("Recovery: MUST NOT reread other files from this patch unless a specific dependency requires it.");
|
|
213
|
+
}
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
180
217
|
function describeFailedAction(error: ExecutePatchError, cwd: string): string | undefined {
|
|
181
218
|
if (!error.failedAction) {
|
|
182
219
|
return undefined;
|
|
@@ -253,14 +290,23 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
|
|
|
253
290
|
: "apply_patch failed";
|
|
254
291
|
const message = failedTarget ? `${prefix} while patching ${failedTarget}: ${error.message}` : `${prefix}: ${error.message}`;
|
|
255
292
|
if (partial) {
|
|
293
|
+
const failedFiles = getFailedPaths(error);
|
|
294
|
+
const appliedFiles = getAppliedPaths(error.result, failedFiles);
|
|
295
|
+
const recoveryMessage = buildPartialFailureMessage(message, failedFiles, appliedFiles);
|
|
256
296
|
markApplyPatchPartialFailure(toolCallId, failedTarget);
|
|
257
297
|
return {
|
|
258
|
-
content: [{ type: "text", text:
|
|
298
|
+
content: [{ type: "text", text: recoveryMessage }],
|
|
259
299
|
details: {
|
|
260
300
|
status: "partial_failure",
|
|
261
301
|
result: error.result,
|
|
262
|
-
error:
|
|
302
|
+
error: recoveryMessage,
|
|
263
303
|
failedTarget,
|
|
304
|
+
appliedFiles,
|
|
305
|
+
failedFiles,
|
|
306
|
+
recoveryInstructions: {
|
|
307
|
+
mustReadFiles: failedFiles,
|
|
308
|
+
mustNotReadFiles: appliedFiles,
|
|
309
|
+
},
|
|
264
310
|
} satisfies ApplyPatchPartialFailureDetails,
|
|
265
311
|
};
|
|
266
312
|
}
|