@howaboua/pi-codex-conversion 1.0.15 → 1.0.17

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.17",
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,8 +1,9 @@
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
- import { DiffError, ExecutePatchError, type ExecutePatchResult, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
6
+ import { DiffError, ExecutePatchError, type ExecutePatchFailure, type ExecutePatchResult, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
6
7
 
7
8
  export const patchFsOps = {
8
9
  mkdirSync: fs.mkdirSync,
@@ -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;
@@ -217,6 +218,14 @@ function applyAction({
217
218
  return fuzz;
218
219
  }
219
220
 
221
+ function getActionPaths(action: ParsedPatchAction): string[] {
222
+ return [action.path, action.type === "update" ? action.movePath : undefined].filter((path): path is string => typeof path === "string");
223
+ }
224
+
225
+ function getCanonicalActionPaths({ cwd, action }: { cwd: string; action: ParsedPatchAction }): string[] {
226
+ return getActionPaths(action).map((path) => resolvePatchPath({ cwd, patchPath: path }));
227
+ }
228
+
220
229
  export function executePatch({ cwd, patchText }: { cwd: string; patchText: string }): ExecutePatchResult {
221
230
  if (!patchText.startsWith("*** Begin Patch")) {
222
231
  throw new DiffError("Patch must start with '*** Begin Patch'");
@@ -227,9 +236,22 @@ export function executePatch({ cwd, patchText }: { cwd: string; patchText: strin
227
236
  const createdFiles = new Set<string>();
228
237
  const deletedFiles = new Set<string>();
229
238
  const movedFiles = new Set<string>();
239
+ const blockedPaths = new Set<string>();
240
+ const failures: ExecutePatchFailure[] = [];
230
241
  let fuzz = 0;
231
242
 
232
243
  for (const action of actions) {
244
+ const actionPaths = getActionPaths(action);
245
+ const canonicalActionPaths = getCanonicalActionPaths({ cwd, action });
246
+ const overlappingPaths = canonicalActionPaths.filter((path) => blockedPaths.has(path));
247
+ if (overlappingPaths.length > 0) {
248
+ failures.push({
249
+ action,
250
+ message: `Skipped because an earlier failed action affected ${actionPaths.filter((_, index) => overlappingPaths.includes(canonicalActionPaths[index])).join(", ")}`,
251
+ });
252
+ continue;
253
+ }
254
+
233
255
  try {
234
256
  fuzz += applyAction({
235
257
  cwd,
@@ -241,20 +263,31 @@ export function executePatch({ cwd, patchText }: { cwd: string; patchText: strin
241
263
  });
242
264
  } catch (error) {
243
265
  const message = error instanceof Error ? error.message : String(error);
244
- throw new ExecutePatchError(
245
- message,
246
- buildExecutePatchResult({
247
- changedFiles,
248
- createdFiles,
249
- deletedFiles,
250
- movedFiles,
251
- fuzz,
252
- }),
253
- action,
254
- );
266
+ for (const path of canonicalActionPaths) {
267
+ blockedPaths.add(path);
268
+ }
269
+ failures.push({ action, message });
255
270
  }
256
271
  }
257
272
 
273
+ if (failures.length > 0) {
274
+ const message =
275
+ failures.length === 1
276
+ ? failures[0].message
277
+ : failures.map(({ action, message: failureMessage }) => `${action.path}: ${failureMessage}`).join("\n");
278
+ throw new ExecutePatchError(
279
+ message,
280
+ buildExecutePatchResult({
281
+ changedFiles,
282
+ createdFiles,
283
+ deletedFiles,
284
+ movedFiles,
285
+ fuzz,
286
+ }),
287
+ failures,
288
+ );
289
+ }
290
+
258
291
  return buildExecutePatchResult({
259
292
  changedFiles,
260
293
  createdFiles,
@@ -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
 
@@ -36,6 +36,11 @@ export interface ExecutePatchResult {
36
36
  fuzz: number;
37
37
  }
38
38
 
39
+ export interface ExecutePatchFailure {
40
+ action: ParsedPatchAction;
41
+ message: string;
42
+ }
43
+
39
44
  export class DiffError extends Error {
40
45
  constructor(message: string) {
41
46
  super(message);
@@ -46,12 +51,14 @@ export class DiffError extends Error {
46
51
  export class ExecutePatchError extends DiffError {
47
52
  result: ExecutePatchResult;
48
53
  failedAction?: ParsedPatchAction;
54
+ failures: ExecutePatchFailure[];
49
55
 
50
- constructor(message: string, result: ExecutePatchResult, failedAction?: ParsedPatchAction) {
56
+ constructor(message: string, result: ExecutePatchResult, failures: ExecutePatchFailure[] = []) {
51
57
  super(message);
52
58
  this.name = "ExecutePatchError";
53
59
  this.result = result;
54
- this.failedAction = failedAction;
60
+ this.failures = failures;
61
+ this.failedAction = failures[0]?.action;
55
62
  }
56
63
 
57
64
  hasPartialSuccess(): boolean {
@@ -17,7 +17,7 @@ interface ApplyPatchRenderState {
17
17
  collapsed: string;
18
18
  expanded: string;
19
19
  status: "pending" | "partial_failure" | "failed";
20
- failedTarget?: string;
20
+ failedTargets?: string[];
21
21
  }
22
22
 
23
23
  interface ApplyPatchSuccessDetails {
@@ -29,7 +29,7 @@ interface ApplyPatchPartialFailureDetails {
29
29
  status: "partial_failure";
30
30
  result: ExecutePatchResult;
31
31
  error: string;
32
- failedTarget?: string;
32
+ failedTargets?: string[];
33
33
  appliedFiles: string[];
34
34
  failedFiles: string[];
35
35
  recoveryInstructions: {
@@ -65,7 +65,7 @@ function setApplyPatchRenderState(
65
65
  patchText: string,
66
66
  cwd: string,
67
67
  status: "pending" | "partial_failure" | "failed" = "pending",
68
- failedTarget?: string,
68
+ failedTargets?: string[],
69
69
  ): void {
70
70
  const collapsed = formatApplyPatchSummary(patchText, cwd);
71
71
  const expanded = renderApplyPatchCall(patchText, cwd);
@@ -75,15 +75,15 @@ function setApplyPatchRenderState(
75
75
  collapsed,
76
76
  expanded,
77
77
  status,
78
- failedTarget,
78
+ failedTargets,
79
79
  });
80
80
  }
81
81
 
82
- function markApplyPatchPartialFailure(toolCallId: string, failedTarget?: string): void {
83
- markApplyPatchFailure(toolCallId, "partial_failure", failedTarget);
82
+ function markApplyPatchPartialFailure(toolCallId: string, failedTargets?: string[]): void {
83
+ markApplyPatchFailure(toolCallId, "partial_failure", failedTargets);
84
84
  }
85
85
 
86
- function markApplyPatchFailure(toolCallId: string, status: "partial_failure" | "failed", failedTarget?: string): void {
86
+ function markApplyPatchFailure(toolCallId: string, status: "partial_failure" | "failed", failedTargets?: string[]): void {
87
87
  const existing = applyPatchRenderStates.get(toolCallId);
88
88
  if (!existing) {
89
89
  return;
@@ -91,14 +91,14 @@ function markApplyPatchFailure(toolCallId: string, status: "partial_failure" | "
91
91
  applyPatchRenderStates.set(toolCallId, {
92
92
  ...existing,
93
93
  status,
94
- failedTarget,
94
+ failedTargets,
95
95
  });
96
96
  }
97
97
 
98
98
  function renderPartialFailureCall(
99
99
  text: string,
100
100
  theme: { fg(role: string, text: string): string },
101
- failedTarget?: string,
101
+ failedTargets?: string[],
102
102
  ): string {
103
103
  const lines = text.split("\n");
104
104
  if (lines.length === 0) {
@@ -106,12 +106,15 @@ function renderPartialFailureCall(
106
106
  }
107
107
  lines[0] = lines[0].replace(/^• (Added|Edited|Deleted)\b/, "• Edit partially failed");
108
108
  const failedLineIndexes = new Set<number>();
109
- if (failedTarget) {
109
+ if (failedTargets) {
110
110
  for (let i = 0; i < lines.length; i += 1) {
111
- const failedLine = markFailedTargetLine(lines[i], failedTarget);
112
- if (failedLine) {
113
- lines[i] = failedLine;
114
- failedLineIndexes.add(i);
111
+ for (const failedTarget of failedTargets) {
112
+ const failedLine = markFailedTargetLine(lines[i], failedTarget);
113
+ if (failedLine) {
114
+ lines[i] = failedLine;
115
+ failedLineIndexes.add(i);
116
+ break;
117
+ }
115
118
  }
116
119
  }
117
120
  }
@@ -131,7 +134,7 @@ function renderPartialFailureCall(
131
134
  function renderFailedCall(
132
135
  text: string,
133
136
  theme: { fg(role: string, text: string): string },
134
- failedTarget?: string,
137
+ failedTargets?: string[],
135
138
  ): string {
136
139
  const lines = text.split("\n");
137
140
  if (lines.length === 0) {
@@ -139,12 +142,15 @@ function renderFailedCall(
139
142
  }
140
143
  lines[0] = lines[0].replace(/^• (Added|Edited|Deleted)\b/, "• Edit failed");
141
144
  const failedLineIndexes = new Set<number>();
142
- if (failedTarget) {
145
+ if (failedTargets) {
143
146
  for (let i = 0; i < lines.length; i += 1) {
144
- const failedLine = markFailedTargetLine(lines[i], failedTarget);
145
- if (failedLine) {
146
- lines[i] = failedLine;
147
- failedLineIndexes.add(i);
147
+ for (const failedTarget of failedTargets) {
148
+ const failedLine = markFailedTargetLine(lines[i], failedTarget);
149
+ if (failedLine) {
150
+ lines[i] = failedLine;
151
+ failedLineIndexes.add(i);
152
+ break;
153
+ }
148
154
  }
149
155
  }
150
156
  }
@@ -187,26 +193,33 @@ function uniqueStrings(values: Array<string | undefined>): string[] {
187
193
  return Array.from(new Set(values.filter((value): value is string => typeof value === "string" && value.length > 0)));
188
194
  }
189
195
 
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));
196
+ function getFailedPaths(error: ExecutePatchError): string[] {
197
+ return uniqueStrings(
198
+ error.failures.flatMap(({ action }) => [action.path, action.type === "update" ? action.movePath : undefined]),
199
+ );
200
+ }
201
+
202
+ function getAppliedPaths(result: ExecutePatchResult, failedFiles: string[]): string[] {
203
+ return result.changedFiles.filter((path) => !failedFiles.includes(path));
204
+ }
205
+
206
+ function buildPartialFailureMessage(message: string, failedFiles: string[], appliedFiles: string[]): string {
193
207
  const lines = [message];
194
208
  if (failedFiles.length > 0) {
195
- lines.push(`Failed file: ${failedFiles.join(", ")}`);
209
+ lines.push(`Failed file${failedFiles.length === 1 ? "" : "s"}: ${failedFiles.join(", ")}`);
196
210
  lines.push(`Recovery: MUST read ${failedFiles.join(", ")} before retrying.`);
197
211
  }
198
212
  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.`);
213
+ lines.push("Earlier file actions in this patch were already applied.");
214
+ lines.push("Recovery: MUST NOT reread other files from this patch unless a specific dependency requires it.");
201
215
  }
202
216
  return lines.join("\n");
203
217
  }
204
218
 
205
- function describeFailedAction(error: ExecutePatchError, cwd: string): string | undefined {
206
- if (!error.failedAction) {
207
- return undefined;
208
- }
209
- return formatPatchTarget(error.failedAction.path, error.failedAction.type === "update" ? error.failedAction.movePath : undefined, cwd);
219
+ function describeFailedActions(error: ExecutePatchError, cwd: string): string[] {
220
+ return uniqueStrings(
221
+ error.failures.map(({ action }) => formatPatchTarget(action.path, action.type === "update" ? action.movePath : undefined, cwd)),
222
+ );
210
223
  }
211
224
 
212
225
  export type { ExecutePatchResult } from "../patch/types.ts";
@@ -241,9 +254,9 @@ const renderApplyPatchCallWithOptionalContext: any = (
241
254
  }
242
255
  const text =
243
256
  cached?.status === "partial_failure"
244
- ? renderPartialFailureCall(baseText, theme, cached.failedTarget)
257
+ ? renderPartialFailureCall(baseText, theme, cached.failedTargets)
245
258
  : cached?.status === "failed"
246
- ? renderFailedCall(baseText, theme, cached.failedTarget)
259
+ ? renderFailedCall(baseText, theme, cached.failedTargets)
247
260
  : baseText;
248
261
  return new Text(text, 0, 0);
249
262
  };
@@ -272,23 +285,24 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
272
285
  } catch (error) {
273
286
  if (error instanceof ExecutePatchError) {
274
287
  const partial = error.hasPartialSuccess();
275
- const failedTarget = describeFailedAction(error, ctx.cwd);
288
+ const failedTargets = describeFailedActions(error, ctx.cwd);
289
+ const failedTargetSummary = failedTargets.join(", ");
276
290
  const prefix = partial
277
291
  ? `apply_patch partially failed after ${summarizePatchCounts(error.result)}`
278
292
  : "apply_patch failed";
279
- const message = failedTarget ? `${prefix} while patching ${failedTarget}: ${error.message}` : `${prefix}: ${error.message}`;
293
+ const message = failedTargetSummary ? `${prefix} while patching ${failedTargetSummary}: ${error.message}` : `${prefix}: ${error.message}`;
280
294
  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);
284
- markApplyPatchPartialFailure(toolCallId, failedTarget);
295
+ const failedFiles = getFailedPaths(error);
296
+ const appliedFiles = getAppliedPaths(error.result, failedFiles);
297
+ const recoveryMessage = buildPartialFailureMessage(message, failedFiles, appliedFiles);
298
+ markApplyPatchPartialFailure(toolCallId, failedTargets);
285
299
  return {
286
300
  content: [{ type: "text", text: recoveryMessage }],
287
301
  details: {
288
302
  status: "partial_failure",
289
303
  result: error.result,
290
304
  error: recoveryMessage,
291
- failedTarget,
305
+ failedTargets,
292
306
  appliedFiles,
293
307
  failedFiles,
294
308
  recoveryInstructions: {
@@ -298,7 +312,7 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
298
312
  } satisfies ApplyPatchPartialFailureDetails,
299
313
  };
300
314
  }
301
- markApplyPatchFailure(toolCallId, "failed", failedTarget);
315
+ markApplyPatchFailure(toolCallId, "failed", failedTargets);
302
316
  throw new Error(message);
303
317
  }
304
318
  markApplyPatchFailure(toolCallId, "failed");