@howaboua/pi-codex-conversion 1.0.15 → 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.15",
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
 
@@ -187,17 +187,29 @@ function uniqueStrings(values: Array<string | undefined>): string[] {
187
187
  return Array.from(new Set(values.filter((value): value is string => typeof value === "string" && value.length > 0)));
188
188
  }
189
189
 
190
- function buildPartialFailureMessage(message: string, failedTarget: string | undefined, result: ExecutePatchResult): string {
191
- const failedFiles = uniqueStrings([failedTarget]);
192
- const appliedFiles = result.changedFiles.filter((path) => !failedFiles.includes(path));
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 {
193
205
  const lines = [message];
194
206
  if (failedFiles.length > 0) {
195
- lines.push(`Failed file: ${failedFiles.join(", ")}`);
207
+ lines.push(`Failed file${failedFiles.length === 1 ? "" : "s"}: ${failedFiles.join(", ")}`);
196
208
  lines.push(`Recovery: MUST read ${failedFiles.join(", ")} before retrying.`);
197
209
  }
198
210
  if (appliedFiles.length > 0) {
199
- lines.push(`Applied files: ${appliedFiles.join(", ")}`);
200
- lines.push(`Recovery: MUST NOT reread ${appliedFiles.join(", ")} unless a specific dependency requires it.`);
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.");
201
213
  }
202
214
  return lines.join("\n");
203
215
  }
@@ -278,9 +290,9 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
278
290
  : "apply_patch failed";
279
291
  const message = failedTarget ? `${prefix} while patching ${failedTarget}: ${error.message}` : `${prefix}: ${error.message}`;
280
292
  if (partial) {
281
- const failedFiles = uniqueStrings([failedTarget]);
282
- const appliedFiles = error.result.changedFiles.filter((path) => !failedFiles.includes(path));
283
- const recoveryMessage = buildPartialFailureMessage(message, failedTarget, error.result);
293
+ const failedFiles = getFailedPaths(error);
294
+ const appliedFiles = getAppliedPaths(error.result, failedFiles);
295
+ const recoveryMessage = buildPartialFailureMessage(message, failedFiles, appliedFiles);
284
296
  markApplyPatchPartialFailure(toolCallId, failedTarget);
285
297
  return {
286
298
  content: [{ type: "text", text: recoveryMessage }],