@hheei/pi-apply-patch 0.2.0 → 0.3.1
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/dist/apply-patch/executor.js +181 -133
- package/dist/apply-patch/index.d.ts +1 -0
- package/dist/apply-patch/index.js +1 -0
- package/dist/apply-patch/jsdiff-matching.d.ts +8 -1
- package/dist/apply-patch/jsdiff-matching.js +34 -20
- package/dist/apply-patch/jsdiff.d.ts +1 -1
- package/dist/apply-patch/jsdiff.js +1 -0
- package/dist/apply-patch/outcome.d.ts +6 -1
- package/dist/apply-patch/outcome.js +10 -1
- package/dist/apply-patch/presentation.js +9 -3
- package/dist/apply-patch/tool.js +25 -7
- package/dist/apply-patch/view.d.ts +6 -0
- package/dist/apply-patch/view.js +10 -0
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { errorMessage } from "@hheei/pi-ext-core";
|
|
2
2
|
import { APPLY_PATCH_MAX_FILE_SIZE, createLocalPatchFs, FsTransportError, fileTooLarge, gcAgentPatchTemps, publishPreparedFile, sftpTimeoutMs, } from "./fs.js";
|
|
3
|
-
import { runJsDiffUpdate } from "./jsdiff.js";
|
|
3
|
+
import { diagnoseRejectedHunk, runJsDiffUpdate } from "./jsdiff.js";
|
|
4
|
+
import { CONFIRMED_STATUSES } from "./outcome.js";
|
|
4
5
|
import { findV4aPatchConflicts, operationTouchedPaths, parseV4aPatch, } from "./parser.js";
|
|
5
6
|
import { assertPatchPath } from "./paths.js";
|
|
6
7
|
const SNAPSHOT_MAX_LINES = 150;
|
|
@@ -36,20 +37,15 @@ function plannedDelta(operation, content) {
|
|
|
36
37
|
return { addedLines: 0, removedLines: lineCount(content) };
|
|
37
38
|
return hunkDelta(operation.hunks);
|
|
38
39
|
}
|
|
40
|
+
/** Totals sum the rows, so the header can never disagree with the table drawn below it. */
|
|
39
41
|
function progressTotals(operations) {
|
|
40
|
-
const paths = new Set();
|
|
41
42
|
let addedLines = 0;
|
|
42
43
|
let removedLines = 0;
|
|
43
44
|
for (const operation of operations) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
operation.status === "fuzzy" ||
|
|
47
|
-
operation.status === "partial") {
|
|
48
|
-
addedLines += operation.addedLines;
|
|
49
|
-
removedLines += operation.removedLines;
|
|
50
|
-
}
|
|
45
|
+
addedLines += operation.addedLines;
|
|
46
|
+
removedLines += operation.removedLines;
|
|
51
47
|
}
|
|
52
|
-
return {
|
|
48
|
+
return { addedLines, removedLines };
|
|
53
49
|
}
|
|
54
50
|
function splitTextLines(content) {
|
|
55
51
|
const lines = new TextDecoder().decode(content).split(/\r\n|\n|\r/);
|
|
@@ -218,6 +214,50 @@ async function applyUpdate(fs, index, operation, policy, signal) {
|
|
|
218
214
|
};
|
|
219
215
|
}
|
|
220
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* A rejection reports lines of the file it left behind. Later operations on the same path publish
|
|
219
|
+
* their own shifts over those lines, so a diagnostic taken mid-patch can end up naming lines the
|
|
220
|
+
* model cannot find. Resolving it once more against the published file keeps the numbers usable.
|
|
221
|
+
* Best effort: an unreadable or vanished file keeps the original diagnostic.
|
|
222
|
+
*/
|
|
223
|
+
async function relocateRejections(fs, patch, rejected, applied, signal) {
|
|
224
|
+
for (const [position, entry] of rejected.entries()) {
|
|
225
|
+
if (entry.diagnostics.length === 0)
|
|
226
|
+
continue;
|
|
227
|
+
const lastIndex = Math.max(...entry.operationIndices);
|
|
228
|
+
const shifted = applied.some((operation) => operation.operationIndex > lastIndex &&
|
|
229
|
+
operation.paths.some((path) => entry.paths.includes(path)));
|
|
230
|
+
if (!shifted)
|
|
231
|
+
continue;
|
|
232
|
+
const path = entry.paths[0];
|
|
233
|
+
if (path === undefined)
|
|
234
|
+
continue;
|
|
235
|
+
let content;
|
|
236
|
+
try {
|
|
237
|
+
content = await fs.readFollow(path, signal);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const update = entry.operationIndices
|
|
243
|
+
.map((index) => patch.operations[index])
|
|
244
|
+
.find((operation) => operation?.kind === "update");
|
|
245
|
+
if (update?.kind !== "update")
|
|
246
|
+
continue;
|
|
247
|
+
const diagnostics = entry.diagnostics.map((diagnostic) => {
|
|
248
|
+
const hunk = update.hunks[diagnostic.hunkIndex - 1];
|
|
249
|
+
if (hunk === undefined)
|
|
250
|
+
return diagnostic;
|
|
251
|
+
try {
|
|
252
|
+
return diagnoseRejectedHunk(content, hunk, diagnostic.hunkIndex);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return diagnostic;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
rejected[position] = Object.freeze({ ...entry, diagnostics: Object.freeze(diagnostics) });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
221
261
|
export async function applyPatchInWorkspace(options) {
|
|
222
262
|
options.signal?.throwIfAborted();
|
|
223
263
|
const fs = options.fs ?? createLocalPatchFs(options.workspaceRoot);
|
|
@@ -238,20 +278,35 @@ export async function applyPatchInWorkspace(options) {
|
|
|
238
278
|
operationIndex: index,
|
|
239
279
|
kind: operation.kind,
|
|
240
280
|
path: operation.kind === "update" ? (operation.moveTo ?? operation.path) : operation.path,
|
|
241
|
-
|
|
281
|
+
// A pending row previews the bytes it is about to write; a row refused before execution
|
|
282
|
+
// reports none, because it wrote nothing.
|
|
283
|
+
...(rejectedIndices.has(index)
|
|
284
|
+
? { addedLines: 0, removedLines: 0 }
|
|
285
|
+
: plannedDelta(operation, undefined)),
|
|
242
286
|
status: rejectedIndices.has(index) ? "rejected" : "pending",
|
|
243
287
|
}));
|
|
288
|
+
/**
|
|
289
|
+
* The bytes one row reports, which are always this operation's own outcome rather than the
|
|
290
|
+
* request that asked for it. A pending row previews what it is about to write and a partial
|
|
291
|
+
* update counts only the hunks that landed; a row refused, never sent or never confirmed
|
|
292
|
+
* reports nothing, because nothing there is known to have been written.
|
|
293
|
+
*/
|
|
294
|
+
const rowDelta = (current, status, update, prepared) => {
|
|
295
|
+
if (status !== "pending" && !CONFIRMED_STATUSES.has(status))
|
|
296
|
+
return { addedLines: 0, removedLines: 0 };
|
|
297
|
+
if (update !== undefined)
|
|
298
|
+
return update.kind === "update" && prepared !== undefined
|
|
299
|
+
? { addedLines: prepared.addedLines, removedLines: prepared.removedLines }
|
|
300
|
+
: plannedDelta(update, undefined);
|
|
301
|
+
return { addedLines: current.addedLines, removedLines: current.removedLines };
|
|
302
|
+
};
|
|
244
303
|
const setStatus = (index, status, outcome, update, partialReason, prepared) => {
|
|
245
304
|
const current = progressOperations[index];
|
|
246
305
|
if (current === undefined)
|
|
247
306
|
throw new Error(`Missing patch progress operation: ${index}`);
|
|
248
307
|
progressOperations[index] = Object.freeze({
|
|
249
308
|
...current,
|
|
250
|
-
...(update
|
|
251
|
-
? {}
|
|
252
|
-
: update.kind !== "update" || prepared === undefined
|
|
253
|
-
? plannedDelta(update, undefined)
|
|
254
|
-
: { addedLines: prepared.addedLines, removedLines: prepared.removedLines }),
|
|
309
|
+
...rowDelta(current, status, update, prepared),
|
|
255
310
|
status,
|
|
256
311
|
...(status === "partial" && update !== undefined && outcome !== undefined
|
|
257
312
|
? {
|
|
@@ -274,134 +329,127 @@ export async function applyPatchInWorkspace(options) {
|
|
|
274
329
|
setStatus(index, "not_applied");
|
|
275
330
|
notApplied.push(rejection(index, operationTouchedPaths(operation), "NotApplied"));
|
|
276
331
|
};
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
332
|
+
await gcAgentPatchTemps(fs, patch.operations.flatMap((operation) => operationTouchedPaths(operation).map((path) => {
|
|
333
|
+
const separator = path.lastIndexOf("/");
|
|
334
|
+
return separator < 0 ? "." : path.slice(0, separator);
|
|
335
|
+
})), options.signal);
|
|
336
|
+
emit("parsed");
|
|
337
|
+
for (const [index, operation] of patch.operations.entries()) {
|
|
338
|
+
if (rejectedIndices.has(index))
|
|
339
|
+
continue;
|
|
340
|
+
if (halt !== undefined) {
|
|
341
|
+
markNotApplied(index, operation);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (options.signal?.aborted) {
|
|
345
|
+
if (applied.length === 0 && unconfirmed.length === 0)
|
|
346
|
+
throw options.signal.reason ?? new Error("aborted");
|
|
347
|
+
halt = "not_applied";
|
|
348
|
+
markNotApplied(index, operation);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
if (operation.kind === "add") {
|
|
353
|
+
const outcome = await applyAdd(fs, index, operation, options.signal);
|
|
354
|
+
applied.push(outcome);
|
|
355
|
+
setStatus(index, "applied", outcome);
|
|
297
356
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
357
|
+
else if (operation.kind === "delete") {
|
|
358
|
+
const deleted = await applyDelete(fs, index, operation, options.signal);
|
|
359
|
+
applied.push(deleted.outcome);
|
|
360
|
+
const current = progressOperations[index];
|
|
361
|
+
if (current !== undefined) {
|
|
362
|
+
progressOperations[index] = Object.freeze({
|
|
363
|
+
...current,
|
|
364
|
+
...plannedDelta(operation, deleted.before),
|
|
365
|
+
status: "applied",
|
|
366
|
+
});
|
|
303
367
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (published.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
? undefined
|
|
325
|
-
: published.rejectedHunks[0]?.kind === "context_not_found"
|
|
326
|
-
? "context not found"
|
|
327
|
-
: "exact context is ambiguous";
|
|
328
|
-
if (published.rejectedHunks.length > 0)
|
|
329
|
-
rejected.push(rejection(index, operationTouchedPaths(operation), "One or more update hunks failed", published.rejectedHunks));
|
|
330
|
-
if (published.sourceError !== undefined) {
|
|
331
|
-
if (published.sourceUnknown === true) {
|
|
332
|
-
unconfirmed.push(rejection(index, [operation.path], published.sourceError));
|
|
333
|
-
setStatus(index, "unconfirmed", published.outcome, operation, undefined, published.prepared);
|
|
334
|
-
halt = "unconfirmed";
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
rejected.push(rejection(index, [operation.path], published.sourceError));
|
|
338
|
-
setStatus(index, "partial", published.outcome, operation, partialReason ?? String(published.sourceError), published.prepared);
|
|
339
|
-
}
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
const published = await applyUpdate(fs, index, operation, options.policy, options.signal);
|
|
371
|
+
if (published.mode === "exact")
|
|
372
|
+
exactUpdateCount += 1;
|
|
373
|
+
if (published.mode === "fuzzy")
|
|
374
|
+
fuzzyUpdateCount += 1;
|
|
375
|
+
applied.push(published.outcome);
|
|
376
|
+
const partialReason = published.rejectedHunks.length === 0
|
|
377
|
+
? undefined
|
|
378
|
+
: published.rejectedHunks[0]?.kind === "context_not_found"
|
|
379
|
+
? "context not found"
|
|
380
|
+
: "exact context is ambiguous";
|
|
381
|
+
if (published.rejectedHunks.length > 0)
|
|
382
|
+
rejected.push(rejection(index, operationTouchedPaths(operation), "One or more update hunks failed", published.rejectedHunks));
|
|
383
|
+
if (published.sourceError !== undefined) {
|
|
384
|
+
if (published.sourceUnknown === true) {
|
|
385
|
+
unconfirmed.push(rejection(index, [operation.path], published.sourceError));
|
|
386
|
+
setStatus(index, "unconfirmed", published.outcome, operation, undefined, published.prepared);
|
|
387
|
+
halt = "unconfirmed";
|
|
340
388
|
}
|
|
341
389
|
else {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
? "fuzzy"
|
|
345
|
-
: "applied"
|
|
346
|
-
: "partial", published.outcome, operation, partialReason, published.prepared);
|
|
390
|
+
rejected.push(rejection(index, [operation.path], published.sourceError));
|
|
391
|
+
setStatus(index, "partial", published.outcome, operation, partialReason ?? String(published.sourceError), published.prepared);
|
|
347
392
|
}
|
|
348
393
|
}
|
|
349
|
-
}
|
|
350
|
-
catch (error) {
|
|
351
|
-
if (options.signal?.aborted && applied.length === 0 && unconfirmed.length === 0)
|
|
352
|
-
throw options.signal.reason ?? error;
|
|
353
|
-
if (error instanceof FsTransportError) {
|
|
354
|
-
const bucket = error.phase === "write" ? notApplied : unconfirmed;
|
|
355
|
-
const status = error.phase === "write" ? "not_applied" : "unconfirmed";
|
|
356
|
-
bucket.push(rejection(index, operationTouchedPaths(operation), error));
|
|
357
|
-
setStatus(index, status);
|
|
358
|
-
halt = status;
|
|
359
|
-
}
|
|
360
|
-
else if (error instanceof PatchUpdateError) {
|
|
361
|
-
rejected.push(rejection(index, operationTouchedPaths(operation), error, error.diagnostics));
|
|
362
|
-
rejectedIndices.add(index);
|
|
363
|
-
setStatus(index, "rejected");
|
|
364
|
-
}
|
|
365
|
-
else if (options.signal?.aborted) {
|
|
366
|
-
notApplied.push(rejection(index, operationTouchedPaths(operation), error));
|
|
367
|
-
setStatus(index, "not_applied");
|
|
368
|
-
halt = "not_applied";
|
|
369
|
-
}
|
|
370
394
|
else {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
395
|
+
setStatus(index, partialReason === undefined
|
|
396
|
+
? published.mode === "fuzzy"
|
|
397
|
+
? "fuzzy"
|
|
398
|
+
: "applied"
|
|
399
|
+
: "partial", published.outcome, operation, partialReason, published.prepared);
|
|
374
400
|
}
|
|
375
401
|
}
|
|
376
|
-
emit("publishing");
|
|
377
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
378
402
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (options.signal?.aborted && applied.length === 0 && unconfirmed.length === 0)
|
|
405
|
+
throw options.signal.reason ?? error;
|
|
406
|
+
if (error instanceof FsTransportError) {
|
|
407
|
+
const bucket = error.phase === "write" ? notApplied : unconfirmed;
|
|
408
|
+
const status = error.phase === "write" ? "not_applied" : "unconfirmed";
|
|
409
|
+
bucket.push(rejection(index, operationTouchedPaths(operation), error));
|
|
410
|
+
setStatus(index, status);
|
|
411
|
+
halt = status;
|
|
412
|
+
}
|
|
413
|
+
else if (error instanceof PatchUpdateError) {
|
|
414
|
+
rejected.push(rejection(index, operationTouchedPaths(operation), error, error.diagnostics));
|
|
415
|
+
rejectedIndices.add(index);
|
|
416
|
+
setStatus(index, "rejected");
|
|
417
|
+
}
|
|
418
|
+
else if (options.signal?.aborted) {
|
|
419
|
+
notApplied.push(rejection(index, operationTouchedPaths(operation), error));
|
|
420
|
+
setStatus(index, "not_applied");
|
|
421
|
+
halt = "not_applied";
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
rejected.push(rejection(index, operationTouchedPaths(operation), error));
|
|
425
|
+
rejectedIndices.add(index);
|
|
426
|
+
setStatus(index, "rejected");
|
|
384
427
|
}
|
|
385
428
|
}
|
|
386
|
-
emit("
|
|
387
|
-
|
|
388
|
-
return Object.freeze({
|
|
389
|
-
changedPaths: Object.freeze([
|
|
390
|
-
...new Set(applied.flatMap((operation) => [...operation.paths])),
|
|
391
|
-
]),
|
|
392
|
-
addedLines: totals.addedLines,
|
|
393
|
-
removedLines: totals.removedLines,
|
|
394
|
-
operations: Object.freeze([...progressOperations]),
|
|
395
|
-
operationCount: patch.operations.length,
|
|
396
|
-
exactUpdateCount,
|
|
397
|
-
fuzzyUpdateCount,
|
|
398
|
-
applied: Object.freeze(applied),
|
|
399
|
-
rejected: Object.freeze(rejected),
|
|
400
|
-
unconfirmed: Object.freeze(unconfirmed),
|
|
401
|
-
notApplied: Object.freeze(notApplied),
|
|
402
|
-
});
|
|
429
|
+
emit("publishing");
|
|
430
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
403
431
|
}
|
|
404
|
-
|
|
405
|
-
|
|
432
|
+
if (halt !== undefined) {
|
|
433
|
+
for (const [index, operation] of patch.operations.entries()) {
|
|
434
|
+
if (progressOperations[index]?.status !== "pending")
|
|
435
|
+
continue;
|
|
436
|
+
markNotApplied(index, operation);
|
|
437
|
+
}
|
|
406
438
|
}
|
|
439
|
+
await relocateRejections(fs, patch, rejected, applied, options.signal);
|
|
440
|
+
emit("done");
|
|
441
|
+
const totals = progressTotals(progressOperations);
|
|
442
|
+
return Object.freeze({
|
|
443
|
+
changedPaths: Object.freeze([...new Set(applied.flatMap((operation) => [...operation.paths]))]),
|
|
444
|
+
addedLines: totals.addedLines,
|
|
445
|
+
removedLines: totals.removedLines,
|
|
446
|
+
operations: Object.freeze([...progressOperations]),
|
|
447
|
+
operationCount: patch.operations.length,
|
|
448
|
+
exactUpdateCount,
|
|
449
|
+
fuzzyUpdateCount,
|
|
450
|
+
applied: Object.freeze(applied),
|
|
451
|
+
rejected: Object.freeze(rejected),
|
|
452
|
+
unconfirmed: Object.freeze(unconfirmed),
|
|
453
|
+
notApplied: Object.freeze(notApplied),
|
|
454
|
+
});
|
|
407
455
|
}
|
|
@@ -2,5 +2,6 @@ export { type ApplyPatchInWorkspaceOptions, applyPatchInWorkspace, } from "./exe
|
|
|
2
2
|
export { APPLY_PATCH_MAX_FILE_SIZE, createLocalPatchFs, FsTransportError, fileTooLarge, gcAgentPatchTemps, type PatchFs, publishPreparedFile, } from "./fs.js";
|
|
3
3
|
export { MUTATION_GLYPH, MUTATION_TONE } from "./glyph.js";
|
|
4
4
|
export type { ApplyPatchAppliedOperation, ApplyPatchHunkSnapshot, ApplyPatchInWorkspaceResult, ApplyPatchOperationProgress, ApplyPatchProgress, ApplyPatchRejection, PatchHunkOutcome, PreparedPatchUpdate, } from "./outcome.js";
|
|
5
|
+
export { CONFIRMED_STATUSES } from "./outcome.js";
|
|
5
6
|
export { compileV4aUpdateToUnifiedDiff, createV4aPreviewCursor, findV4aPatchConflicts, MAX_V4A_HUNK_LINES, MAX_V4A_HUNKS_PER_UPDATE, MAX_V4A_OPERATIONS, MAX_V4A_PATCH_BYTES, MAX_V4A_PATH_BYTES, MAX_V4A_PATH_SEGMENT_BYTES, operationTouchedPaths, parseV4aPatch, parseV4aPatchProgressively, previewV4aPatchFileCount, previewV4aPatchPrefix, type V4aAddedLine, type V4aAddOperation, type V4aContextLine, type V4aDeleteOperation, type V4aPatch, type V4aPatchConflict, type V4aPatchOperation, type V4aPreviewCursor, type V4aPreviewOperation, type V4aRemovedLine, type V4aUpdateHunk, type V4aUpdateLine, type V4aUpdateOperation, } from "./parser.js";
|
|
6
7
|
export { type ApplyPatchPolicy, DEFAULT_APPLY_PATCH_POLICY, type LoadApplyPatchPolicyOptions, loadApplyPatchPolicy, } from "./policy.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { applyPatchInWorkspace, } from "./executor.js";
|
|
2
2
|
export { APPLY_PATCH_MAX_FILE_SIZE, createLocalPatchFs, FsTransportError, fileTooLarge, gcAgentPatchTemps, publishPreparedFile, } from "./fs.js";
|
|
3
3
|
export { MUTATION_GLYPH, MUTATION_TONE } from "./glyph.js";
|
|
4
|
+
export { CONFIRMED_STATUSES } from "./outcome.js";
|
|
4
5
|
export { compileV4aUpdateToUnifiedDiff, createV4aPreviewCursor, findV4aPatchConflicts, MAX_V4A_HUNK_LINES, MAX_V4A_HUNKS_PER_UPDATE, MAX_V4A_OPERATIONS, MAX_V4A_PATCH_BYTES, MAX_V4A_PATH_BYTES, MAX_V4A_PATH_SEGMENT_BYTES, operationTouchedPaths, parseV4aPatch, parseV4aPatchProgressively, previewV4aPatchFileCount, previewV4aPatchPrefix, } from "./parser.js";
|
|
5
6
|
export { DEFAULT_APPLY_PATCH_POLICY, loadApplyPatchPolicy, } from "./policy.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PreparedPatchUpdate } from "./outcome.js";
|
|
1
|
+
import type { PreparedPatchUpdate, RejectedPatchHunk } from "./outcome.js";
|
|
2
2
|
import type { V4aUpdateHunk } from "./parser.js";
|
|
3
3
|
export interface JsDiffHunkInput {
|
|
4
4
|
readonly unifiedDiff: string;
|
|
@@ -10,4 +10,11 @@ export interface JsDiffUpdateInput {
|
|
|
10
10
|
readonly fuzzFactor: number;
|
|
11
11
|
readonly snapshotPath: string;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Why one hunk cannot land in `content`, with any line numbers taken from that same content. Hunks
|
|
15
|
+
* and operations run in order over one growing copy, so a diagnostic computed when the hunk was
|
|
16
|
+
* tried would name lines that later writes had already moved; resolving it against the final file is
|
|
17
|
+
* what keeps the numbers readable by the model that has to retry.
|
|
18
|
+
*/
|
|
19
|
+
export declare function diagnoseRejectedHunk(content: Uint8Array, hunk: V4aUpdateHunk, hunkIndex: number): RejectedPatchHunk;
|
|
13
20
|
export declare function prepareJsDiffUpdate(data: JsDiffUpdateInput): PreparedPatchUpdate;
|
|
@@ -364,6 +364,33 @@ function snapshotFromChange(path, hunkIndex, change, beforeLines, afterLines) {
|
|
|
364
364
|
after: Object.freeze(afterLines.slice(afterStart, afterEnd)),
|
|
365
365
|
});
|
|
366
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Why one hunk cannot land in `content`, with any line numbers taken from that same content. Hunks
|
|
369
|
+
* and operations run in order over one growing copy, so a diagnostic computed when the hunk was
|
|
370
|
+
* tried would name lines that later writes had already moved; resolving it against the final file is
|
|
371
|
+
* what keeps the numbers readable by the model that has to retry.
|
|
372
|
+
*/
|
|
373
|
+
export function diagnoseRejectedHunk(content, hunk, hunkIndex) {
|
|
374
|
+
const lines = textLines(content);
|
|
375
|
+
const constraint = anchorConstraint(lines, hunk);
|
|
376
|
+
if ("kind" in constraint)
|
|
377
|
+
return { ...constraint, hunkIndex };
|
|
378
|
+
const oldTexts = oldSideTexts(hunk);
|
|
379
|
+
const isEof = constraint.endOfFile === true || hunk.endOfFile === true;
|
|
380
|
+
const anchored = lines.slice(constraint.prefixLines);
|
|
381
|
+
const eofLines = isEof ? Math.max(0, anchored.length - oldTexts.length) : 0;
|
|
382
|
+
const prefixLines = constraint.prefixLines + eofLines;
|
|
383
|
+
const searchable = lines.slice(prefixLines).map((line) => line.text);
|
|
384
|
+
const tiered = findTieredStarts(searchable, oldTexts);
|
|
385
|
+
if (oldTexts.length > 0 && tiered.tier === "ambiguous")
|
|
386
|
+
return {
|
|
387
|
+
kind: "ambiguous_exact",
|
|
388
|
+
hunkIndex,
|
|
389
|
+
candidateStartLines: tiered.starts.map((start) => start + prefixLines + 1),
|
|
390
|
+
};
|
|
391
|
+
const hint = findDiagnosticHint(searchable, oldTexts, prefixLines);
|
|
392
|
+
return { kind: "context_not_found", hunkIndex, ...(hint === undefined ? {} : { hint }) };
|
|
393
|
+
}
|
|
367
394
|
function parseOneHunk(parsePatch, unifiedDiff, hunk) {
|
|
368
395
|
let parsed;
|
|
369
396
|
try {
|
|
@@ -392,7 +419,7 @@ export function prepareJsDiffUpdate(data) {
|
|
|
392
419
|
if (data.before.length > MAX_FILE_SIZE)
|
|
393
420
|
throw new Error(`file_too_large: ${data.before.length} bytes`);
|
|
394
421
|
const outcomes = [];
|
|
395
|
-
const
|
|
422
|
+
const pending = [];
|
|
396
423
|
const snapshots = [];
|
|
397
424
|
let addedLines = 0;
|
|
398
425
|
let removedLines = 0;
|
|
@@ -412,7 +439,7 @@ export function prepareJsDiffUpdate(data) {
|
|
|
412
439
|
}
|
|
413
440
|
const constraint = anchorConstraint(original, hunk);
|
|
414
441
|
if ("kind" in constraint) {
|
|
415
|
-
|
|
442
|
+
pending.push({ hunkIndex, hunk });
|
|
416
443
|
continue;
|
|
417
444
|
}
|
|
418
445
|
const oldTexts = oldSideTexts(hunk);
|
|
@@ -432,11 +459,7 @@ export function prepareJsDiffUpdate(data) {
|
|
|
432
459
|
const tieredMatch = findTieredStarts(searchable.map((line) => line.text), oldTexts);
|
|
433
460
|
const matchedStarts = tieredMatch.starts.map((start) => start + prefixLines);
|
|
434
461
|
if (oldLength > 0 && tieredMatch.tier === "ambiguous") {
|
|
435
|
-
|
|
436
|
-
kind: "ambiguous_exact",
|
|
437
|
-
hunkIndex,
|
|
438
|
-
candidateStartLines: matchedStarts.map((start) => start + 1),
|
|
439
|
-
});
|
|
462
|
+
pending.push({ hunkIndex, hunk });
|
|
440
463
|
continue;
|
|
441
464
|
}
|
|
442
465
|
const sourceView = lfView(original);
|
|
@@ -514,17 +537,12 @@ export function prepareJsDiffUpdate(data) {
|
|
|
514
537
|
parsed = parseOneHunk(parsePatch, input.unifiedDiff, hunk);
|
|
515
538
|
}
|
|
516
539
|
if ("kind" in parsed) {
|
|
517
|
-
|
|
540
|
+
pending.push({ hunkIndex, hunk });
|
|
518
541
|
continue;
|
|
519
542
|
}
|
|
520
543
|
const parsedHunk = parsed.hunks[0];
|
|
521
544
|
if (parsedHunk === undefined) {
|
|
522
|
-
|
|
523
|
-
rejected.push({
|
|
524
|
-
kind: "context_not_found",
|
|
525
|
-
hunkIndex,
|
|
526
|
-
...(hint !== undefined ? { hint } : {}),
|
|
527
|
-
});
|
|
545
|
+
pending.push({ hunkIndex, hunk });
|
|
528
546
|
continue;
|
|
529
547
|
}
|
|
530
548
|
if (oldLength === 0) {
|
|
@@ -549,12 +567,7 @@ export function prepareJsDiffUpdate(data) {
|
|
|
549
567
|
match = "fuzzy";
|
|
550
568
|
}
|
|
551
569
|
if (applied === false) {
|
|
552
|
-
|
|
553
|
-
rejected.push({
|
|
554
|
-
kind: "context_not_found",
|
|
555
|
-
hunkIndex,
|
|
556
|
-
...(hint !== undefined ? { hint } : {}),
|
|
557
|
-
});
|
|
570
|
+
pending.push({ hunkIndex, hunk });
|
|
558
571
|
continue;
|
|
559
572
|
}
|
|
560
573
|
const changes = structuredPatch("before", "after", sourceView, applied, undefined, undefined, {
|
|
@@ -601,6 +614,7 @@ export function prepareJsDiffUpdate(data) {
|
|
|
601
614
|
mode ??= "exact";
|
|
602
615
|
after = rebuilt;
|
|
603
616
|
}
|
|
617
|
+
const rejected = pending.map((item) => diagnoseRejectedHunk(after, item.hunk, item.hunkIndex));
|
|
604
618
|
return {
|
|
605
619
|
after,
|
|
606
620
|
mode,
|
|
@@ -51,6 +51,12 @@ export interface ApplyPatchRejection {
|
|
|
51
51
|
}
|
|
52
52
|
export type ApplyPatchProgressStage = "parsed" | "publishing" | "done";
|
|
53
53
|
export type ApplyPatchOperationStatus = "pending" | "applied" | "partial" | "fuzzy" | "rejected" | "unconfirmed" | "not_applied";
|
|
54
|
+
/**
|
|
55
|
+
* Rows whose delta is a confirmed write. `changedPaths` comes from these rows only, so the model
|
|
56
|
+
* never reads a path as changed before the Publish that changed it, and the header totals count
|
|
57
|
+
* these rows plus the pending preview.
|
|
58
|
+
*/
|
|
59
|
+
export declare const CONFIRMED_STATUSES: ReadonlySet<ApplyPatchOperationStatus>;
|
|
54
60
|
export type ApplyPatchOperationProgress = {
|
|
55
61
|
readonly operationIndex: number;
|
|
56
62
|
readonly kind: "add" | "delete" | "update";
|
|
@@ -64,7 +70,6 @@ export type ApplyPatchOperationProgress = {
|
|
|
64
70
|
};
|
|
65
71
|
export interface ApplyPatchProgress {
|
|
66
72
|
readonly stage: ApplyPatchProgressStage;
|
|
67
|
-
readonly files: number;
|
|
68
73
|
readonly addedLines: number;
|
|
69
74
|
readonly removedLines: number;
|
|
70
75
|
readonly operations: readonly ApplyPatchOperationProgress[];
|
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Rows whose delta is a confirmed write. `changedPaths` comes from these rows only, so the model
|
|
3
|
+
* never reads a path as changed before the Publish that changed it, and the header totals count
|
|
4
|
+
* these rows plus the pending preview.
|
|
5
|
+
*/
|
|
6
|
+
export const CONFIRMED_STATUSES = new Set([
|
|
7
|
+
"applied",
|
|
8
|
+
"fuzzy",
|
|
9
|
+
"partial",
|
|
10
|
+
]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
2
2
|
import { agentResultText, isRecord, } from "@hheei/pi-ext-core";
|
|
3
3
|
import { createV4aPreviewCursor, previewV4aPatchPrefix } from "./parser.js";
|
|
4
|
-
import { applyPatchResultFacts, applyPatchSummary, formatApplyPatchOperationRow, formatApplyPatchRejectionRows, formatApplyPatchSnapshotRows, } from "./view.js";
|
|
4
|
+
import { applyPatchFilesInPlay, applyPatchResultFacts, applyPatchSummary, formatApplyPatchOperationRow, formatApplyPatchRejectionRows, formatApplyPatchSnapshotRows, } from "./view.js";
|
|
5
5
|
/**
|
|
6
6
|
* Preview cursors live in this module's own map, keyed by the row's shared render state: the cursor
|
|
7
7
|
* is display-only progress, so it is never persisted and never read back from the session.
|
|
@@ -63,7 +63,9 @@ function resultStatus(facts, isError, isPartial) {
|
|
|
63
63
|
return "error";
|
|
64
64
|
if (facts.status === "partial")
|
|
65
65
|
return "warn";
|
|
66
|
-
|
|
66
|
+
// Live progress carries no verdict of its own, so a streamed success still reads as running
|
|
67
|
+
// until the result replaces it.
|
|
68
|
+
return isPartial ? "running" : "none";
|
|
67
69
|
}
|
|
68
70
|
/** Result sections: operation rows, one row block per applied snapshot, then the failure rows. */
|
|
69
71
|
function resultSections(facts, theme) {
|
|
@@ -115,8 +117,12 @@ export const applyPatchPresentation = (args, result, theme, context) => {
|
|
|
115
117
|
};
|
|
116
118
|
}
|
|
117
119
|
const rows = operationRows(facts.operations, theme);
|
|
120
|
+
// A live frame has no confirmed paths yet, so its file count comes from the rows it shows.
|
|
121
|
+
const files = context.isPartial
|
|
122
|
+
? applyPatchFilesInPlay(facts.operations)
|
|
123
|
+
: new Set(facts.changedPaths).size;
|
|
118
124
|
return {
|
|
119
|
-
summary: applyPatchSummary(
|
|
125
|
+
summary: applyPatchSummary(files, facts.addedLines, facts.removedLines),
|
|
120
126
|
status: resultStatus(facts, isError, context.isPartial),
|
|
121
127
|
replaceCallOnPartial: true,
|
|
122
128
|
...(output === "" ? {} : { output }),
|
package/dist/apply-patch/tool.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createOptionalToolRenderers, createToolPresentationRenderers, defaultExtensionSettingsPaths, errorMessage, textToolResult, } from "@hheei/pi-ext-core";
|
|
1
|
+
import { createOptionalToolRenderers, createRenderThrottle, createToolPresentationRenderers, defaultExtensionSettingsPaths, errorMessage, textToolResult, } from "@hheei/pi-ext-core";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { applyPatchInWorkspace, loadApplyPatchPolicy, } from "./index.js";
|
|
3
|
+
import { applyPatchInWorkspace, CONFIRMED_STATUSES, loadApplyPatchPolicy, } from "./index.js";
|
|
4
4
|
import { isPatchPathOutsideWorkspace } from "./paths.js";
|
|
5
5
|
import { applyPatchPresentation } from "./presentation.js";
|
|
6
6
|
const MAX_CANDIDATES = 6;
|
|
@@ -108,7 +108,10 @@ function externalPathWarning(workspaceRoot, result) {
|
|
|
108
108
|
}
|
|
109
109
|
function progressDetails(progress) {
|
|
110
110
|
return {
|
|
111
|
-
|
|
111
|
+
// Live details report confirmed writes only, the same rows the header totals count.
|
|
112
|
+
changedPaths: progress.operations
|
|
113
|
+
.filter((operation) => CONFIRMED_STATUSES.has(operation.status))
|
|
114
|
+
.map((operation) => operation.path),
|
|
112
115
|
addedLines: progress.addedLines,
|
|
113
116
|
removedLines: progress.removedLines,
|
|
114
117
|
operations: progress.operations,
|
|
@@ -134,6 +137,16 @@ export function createApplyPatchTool() {
|
|
|
134
137
|
executionMode: "sequential",
|
|
135
138
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
136
139
|
const { patch } = parseApplyPatchParameters(params);
|
|
140
|
+
// One operation per repaint used to rebuild the whole operation table. A patch with many
|
|
141
|
+
// operations now repaints on the shared streaming window; the returned result stays the
|
|
142
|
+
// only authority for the finished call.
|
|
143
|
+
let latestProgress;
|
|
144
|
+
const repaint = createRenderThrottle(() => {
|
|
145
|
+
const progress = latestProgress;
|
|
146
|
+
if (progress === undefined)
|
|
147
|
+
return;
|
|
148
|
+
onUpdate?.({ content: [], details: progressDetails(progress) });
|
|
149
|
+
});
|
|
137
150
|
try {
|
|
138
151
|
const policy = await loadApplyPatchPolicy({
|
|
139
152
|
paths: defaultExtensionSettingsPaths(ctx.cwd),
|
|
@@ -144,10 +157,10 @@ export function createApplyPatchTool() {
|
|
|
144
157
|
patch,
|
|
145
158
|
policy,
|
|
146
159
|
...(signal === undefined ? {} : { signal }),
|
|
147
|
-
onProgress: (progress) =>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
160
|
+
onProgress: (progress) => {
|
|
161
|
+
latestProgress = progress;
|
|
162
|
+
repaint.request();
|
|
163
|
+
},
|
|
151
164
|
});
|
|
152
165
|
const warning = externalPathWarning(ctx.cwd, result);
|
|
153
166
|
const status = statusFor(result);
|
|
@@ -164,6 +177,11 @@ export function createApplyPatchTool() {
|
|
|
164
177
|
const message = errorMessage(error);
|
|
165
178
|
throw new Error(`apply_patch failed: ${message}`);
|
|
166
179
|
}
|
|
180
|
+
finally {
|
|
181
|
+
// The result below is the only authority for the finished call, so the window closes
|
|
182
|
+
// here: a repaint after it is built would rewrite a settled body.
|
|
183
|
+
repaint.dispose();
|
|
184
|
+
}
|
|
167
185
|
},
|
|
168
186
|
};
|
|
169
187
|
}
|
|
@@ -20,6 +20,12 @@ export interface ApplyPatchResultFacts {
|
|
|
20
20
|
export declare function applyPatchResultFacts(details: unknown): ApplyPatchResultFacts | undefined;
|
|
21
21
|
/** How one mutation outcome is drawn. One outcome never draws two symbols across the tools. */
|
|
22
22
|
export declare function applyPatchGlyph(status: string, theme: Theme): string;
|
|
23
|
+
/**
|
|
24
|
+
* Distinct paths a live frame still has in play: a pending row previews the write it is about to
|
|
25
|
+
* make, and a row that certainly wrote nothing is not counted. The finished result counts
|
|
26
|
+
* `changedPaths` instead, so a preview never claims a path was changed.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyPatchFilesInPlay(operations: readonly ApplyPatchOperationProgress[]): number;
|
|
23
29
|
/** The header summary every apply_patch row shares, including nested calls. */
|
|
24
30
|
export declare function applyPatchSummary(files: number, added: number, removed: number): string;
|
|
25
31
|
/** One operation row: glyph, verb, path, delta, fuzzy marker, hunk summary. */
|
package/dist/apply-patch/view.js
CHANGED
|
@@ -165,6 +165,16 @@ export function applyPatchGlyph(status, theme) {
|
|
|
165
165
|
const tone = MUTATION_TONE[status] ?? "error";
|
|
166
166
|
return theme.fg(tone, glyph);
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Distinct paths a live frame still has in play: a pending row previews the write it is about to
|
|
170
|
+
* make, and a row that certainly wrote nothing is not counted. The finished result counts
|
|
171
|
+
* `changedPaths` instead, so a preview never claims a path was changed.
|
|
172
|
+
*/
|
|
173
|
+
export function applyPatchFilesInPlay(operations) {
|
|
174
|
+
return new Set(operations
|
|
175
|
+
.filter((operation) => operation.status !== "rejected" && operation.status !== "not_applied")
|
|
176
|
+
.map((operation) => operation.path)).size;
|
|
177
|
+
}
|
|
168
178
|
/** The header summary every apply_patch row shares, including nested calls. */
|
|
169
179
|
export function applyPatchSummary(files, added, removed) {
|
|
170
180
|
return `modify ${String(files)}${files === 1 ? " file" : " files"} (+${String(added)} -${String(removed)})`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hheei/pi-apply-patch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "V4A apply_patch editing tool with worker matching and per-path publishing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@hheei/pi-ext-core": "^0.
|
|
39
|
+
"@hheei/pi-ext-core": "^0.6.1",
|
|
40
40
|
"diff": "^9.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|