@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
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] !== line) {
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
+ }
@@ -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 (let index = start; index < lines.length; index++) {
68
- if (linesEqual({ left: lines.slice(index, index + context.length), right: context })) {
69
- return { newIndex: index, fuzz: 0 };
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
- for (let index = start; index < lines.length; index++) {
74
- const left = lines.slice(index, index + context.length).map((line) => line.trimEnd());
75
- const right = context.map((line) => line.trimEnd());
76
- if (linesEqual({ left, right })) {
77
- return { newIndex: index, fuzz: 1 };
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
- for (let index = start; index < lines.length; index++) {
82
- const left = lines.slice(index, index + context.length).map((line) => line.trim());
83
- const right = context.map((line) => line.trim());
84
- if (linesEqual({ left, right })) {
85
- return { newIndex: index, fuzz: 100 };
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
- let found = false;
267
-
268
- const exactAlreadySeen = lines.slice(0, index).some((line) => line === defStr);
269
- if (!exactAlreadySeen) {
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: message }],
298
+ content: [{ type: "text", text: recoveryMessage }],
259
299
  details: {
260
300
  status: "partial_failure",
261
301
  result: error.result,
262
- error: message,
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
  }