@hheei/pi-apply-patch 0.0.0-stage → 0.1.0

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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +3 -0
  4. package/dist/apply-patch/executor.d.ts +15 -0
  5. package/dist/apply-patch/executor.js +407 -0
  6. package/dist/apply-patch/fs.d.ts +37 -0
  7. package/dist/apply-patch/fs.js +186 -0
  8. package/dist/apply-patch/glyph.d.ts +8 -0
  9. package/dist/apply-patch/glyph.js +23 -0
  10. package/dist/apply-patch/index.d.ts +6 -0
  11. package/dist/apply-patch/index.js +5 -0
  12. package/dist/apply-patch/jsdiff-matching.d.ts +13 -0
  13. package/dist/apply-patch/jsdiff-matching.js +613 -0
  14. package/dist/apply-patch/jsdiff-worker.d.ts +1 -0
  15. package/dist/apply-patch/jsdiff-worker.js +18 -0
  16. package/dist/apply-patch/jsdiff.d.ts +9 -0
  17. package/dist/apply-patch/jsdiff.js +219 -0
  18. package/dist/apply-patch/outcome.d.ts +84 -0
  19. package/dist/apply-patch/outcome.js +1 -0
  20. package/dist/apply-patch/parser.d.ts +85 -0
  21. package/dist/apply-patch/parser.js +486 -0
  22. package/dist/apply-patch/paths.d.ts +5 -0
  23. package/dist/apply-patch/paths.js +23 -0
  24. package/dist/apply-patch/policy.d.ts +11 -0
  25. package/dist/apply-patch/policy.js +44 -0
  26. package/dist/apply-patch/presentation.d.ts +7 -0
  27. package/dist/apply-patch/presentation.js +127 -0
  28. package/dist/apply-patch/settings.d.ts +5 -0
  29. package/dist/apply-patch/settings.js +33 -0
  30. package/dist/apply-patch/tool.d.ts +18 -0
  31. package/dist/apply-patch/tool.js +178 -0
  32. package/dist/apply-patch/view.d.ts +34 -0
  33. package/dist/apply-patch/view.js +256 -0
  34. package/dist/apply-patch-tool.d.ts +1 -0
  35. package/dist/apply-patch-tool.js +1 -0
  36. package/dist/extension.d.ts +2 -0
  37. package/dist/extension.js +22 -0
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.js +1 -0
  40. package/package.json +57 -5
@@ -0,0 +1,613 @@
1
+ import { applyPatch, parsePatch, structuredPatch, } from "diff";
2
+ const MAX_FILE_SIZE = 32 * 1024 * 1024;
3
+ const SNAPSHOT_CONTEXT = 3;
4
+ function stripEol(text) {
5
+ return text.replace(/\r\n$|\n$|\r$/, "");
6
+ }
7
+ function findLastLine(lines, excludedKind) {
8
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
9
+ const line = lines[index];
10
+ if (line !== undefined && line.kind !== excludedKind)
11
+ return line;
12
+ }
13
+ return undefined;
14
+ }
15
+ function oldSideTexts(hunk) {
16
+ return hunk.lines.filter((line) => line.kind !== "add").map((line) => stripEol(line.text));
17
+ }
18
+ function textLines(bytes) {
19
+ const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
20
+ const parts = text.split(/(\r\n|\n|\r)/);
21
+ const lines = [];
22
+ for (let index = 0; index < parts.length; index += 2) {
23
+ const value = parts[index];
24
+ if (value === undefined || (index === parts.length - 1 && value === ""))
25
+ continue;
26
+ const bom = lines.length === 0 && value.startsWith("\uFEFF");
27
+ lines.push({
28
+ text: bom ? value.slice(1) : value,
29
+ end: parts[index + 1] ?? "",
30
+ ...(bom ? { bom: true } : {}),
31
+ });
32
+ }
33
+ return lines;
34
+ }
35
+ function encodeLines(lines) {
36
+ const text = lines.map((line) => `${line.text}${line.end}`).join("");
37
+ return new TextEncoder().encode(`${lines[0]?.bom === true ? "\uFEFF" : ""}${text}`);
38
+ }
39
+ function lfView(lines) {
40
+ if (lines.length === 0)
41
+ return "";
42
+ let out = lines.map((line) => line.text).join("\n");
43
+ if (lines.at(-1)?.end !== "")
44
+ out += "\n";
45
+ return out;
46
+ }
47
+ function normalizeUnicode(value) {
48
+ return value
49
+ .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/gu, "-")
50
+ .replace(/[\u2018\u2019\u201a\u201b]/gu, "'")
51
+ .replace(/[\u201c\u201d\u201e\u201f]/gu, '"')
52
+ .replace(/[\u00a0\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000]/gu, " ");
53
+ }
54
+ function findTieredStarts(haystack, needle) {
55
+ if (needle.length === 0)
56
+ return { starts: [], tier: "exact" };
57
+ // Tier 1: exact match
58
+ const exactStarts = [];
59
+ for (let index = 0; index + needle.length <= haystack.length; index += 1) {
60
+ let matched = true;
61
+ for (let offset = 0; offset < needle.length; offset += 1) {
62
+ if (haystack[index + offset] !== needle[offset]) {
63
+ matched = false;
64
+ break;
65
+ }
66
+ }
67
+ if (matched)
68
+ exactStarts.push(index);
69
+ }
70
+ if (exactStarts.length === 1)
71
+ return { starts: exactStarts, tier: "exact" };
72
+ if (exactStarts.length > 1)
73
+ return { starts: exactStarts, tier: "ambiguous" };
74
+ // Tier 2: trimEnd + unicode normalization
75
+ const normHaystack2 = haystack.map((line) => normalizeUnicode(line).trimEnd());
76
+ const normNeedle2 = needle.map((line) => normalizeUnicode(line).trimEnd());
77
+ const tier2Starts = [];
78
+ for (let index = 0; index + needle.length <= haystack.length; index += 1) {
79
+ let matched = true;
80
+ for (let offset = 0; offset < needle.length; offset += 1) {
81
+ if (normHaystack2[index + offset] !== normNeedle2[offset]) {
82
+ matched = false;
83
+ break;
84
+ }
85
+ }
86
+ if (matched)
87
+ tier2Starts.push(index);
88
+ }
89
+ if (tier2Starts.length === 1)
90
+ return { starts: tier2Starts, tier: "trimEnd" };
91
+ if (tier2Starts.length > 1)
92
+ return { starts: tier2Starts, tier: "ambiguous" };
93
+ // Tier 3: trim (indentation tolerance) + unicode normalization
94
+ const normHaystack3 = haystack.map((line) => normalizeUnicode(line).trim());
95
+ const normNeedle3 = needle.map((line) => normalizeUnicode(line).trim());
96
+ const tier3Starts = [];
97
+ for (let index = 0; index + needle.length <= haystack.length; index += 1) {
98
+ let matched = true;
99
+ for (let offset = 0; offset < needle.length; offset += 1) {
100
+ if (normHaystack3[index + offset] !== normNeedle3[offset]) {
101
+ matched = false;
102
+ break;
103
+ }
104
+ }
105
+ if (matched)
106
+ tier3Starts.push(index);
107
+ }
108
+ if (tier3Starts.length === 1) {
109
+ return { starts: tier3Starts, tier: "trim" };
110
+ }
111
+ if (tier3Starts.length > 1)
112
+ return { starts: tier3Starts, tier: "ambiguous" };
113
+ // EOF Tail match: when LLM context extends past EOF with trailing empty/whitespace lines
114
+ const trimmedNeedle = [...needle];
115
+ while (trimmedNeedle.length > 0 && trimmedNeedle[trimmedNeedle.length - 1]?.trim() === "") {
116
+ trimmedNeedle.pop();
117
+ }
118
+ if (trimmedNeedle.length > 0 && trimmedNeedle.length < needle.length) {
119
+ const tailMatch = findTieredStarts(haystack, trimmedNeedle);
120
+ if (tailMatch.starts.length === 1) {
121
+ const start = tailMatch.starts[0] ?? 0;
122
+ if (start + trimmedNeedle.length === haystack.length) {
123
+ return { ...tailMatch, eofTrimmedCount: needle.length - trimmedNeedle.length };
124
+ }
125
+ }
126
+ }
127
+ return { starts: [], tier: "none" };
128
+ }
129
+ function adjustIndent(cleanText, lastOrigIndent, lastHunkIndent, indentMap) {
130
+ if (cleanText.trim() === "")
131
+ return "";
132
+ const curHunkIndent = cleanText.match(/^\s*/)?.[0] ?? "";
133
+ if (indentMap?.has(curHunkIndent)) {
134
+ const mapped = indentMap.get(curHunkIndent) ?? "";
135
+ return `${mapped}${cleanText.trimStart()}`;
136
+ }
137
+ const diff = curHunkIndent.length - lastHunkIndent.length;
138
+ const targetLen = Math.max(0, lastOrigIndent.length + diff);
139
+ if (targetLen === 0)
140
+ return cleanText.trimStart();
141
+ if (targetLen <= lastOrigIndent.length) {
142
+ return `${lastOrigIndent.slice(0, targetLen)}${cleanText.trimStart()}`;
143
+ }
144
+ const extra = curHunkIndent.slice(lastHunkIndent.length);
145
+ return `${lastOrigIndent}${extra}${cleanText.trimStart()}`;
146
+ }
147
+ function findDiagnosticHint(searchable, needle, prefixLines) {
148
+ if (needle.length === 0)
149
+ return undefined;
150
+ if (searchable.length < needle.length) {
151
+ return `context exceeds file length (file has ${prefixLines + searchable.length} lines, but hunk context expects ${needle.length} lines)`;
152
+ }
153
+ const firstNeedle = needle[0] ?? "";
154
+ const normNeedle = normalizeUnicode(firstNeedle).trim();
155
+ if (normNeedle === "")
156
+ return undefined;
157
+ for (let index = 0; index < searchable.length; index += 1) {
158
+ const line = searchable[index] ?? "";
159
+ if (normalizeUnicode(line).trim() === normNeedle) {
160
+ const lineNum = prefixLines + index + 1;
161
+ return `found similar line with different indentation/whitespace at line ${lineNum}: expected "${firstNeedle.trim()}", actual "${line.trim()}"`;
162
+ }
163
+ }
164
+ return undefined;
165
+ }
166
+ function isEofAnchor(anchor) {
167
+ const trimmed = anchor.trim();
168
+ return /^(?:end\s+of\s+file|eof|\*{0,3}\s*end\s+of\s+file\*{0,3})$/i.test(trimmed);
169
+ }
170
+ function anchorConstraint(lines, hunk) {
171
+ let start = 0;
172
+ let endOfFile = hunk.endOfFile;
173
+ const anchors = hunk.anchors ?? (hunk.anchor === undefined ? [] : [hunk.anchor]);
174
+ for (let anchor of anchors) {
175
+ if (isEofAnchor(anchor)) {
176
+ endOfFile = true;
177
+ continue;
178
+ }
179
+ const lineNumMatch = anchor.match(/^(?:line\s*)?(\d+)$/i);
180
+ if (lineNumMatch?.[1]) {
181
+ const lineNum = Number.parseInt(lineNumMatch[1], 10);
182
+ if (lineNum > lines.length) {
183
+ endOfFile = true;
184
+ }
185
+ else {
186
+ start = Math.max(0, lineNum - 1);
187
+ }
188
+ continue;
189
+ }
190
+ // Handle git hunk headers: @@ -x,y +x,y @@ text or @@ -x,y +x,y @@
191
+ const gitRangeMatch = anchor.match(/^-(?<oldStart>\d+)(?:,\d+)?\s+\+\d+(?:,\d+)?(?:\s*@@\s*(?<trailing>.*))?$/);
192
+ if (gitRangeMatch?.groups) {
193
+ const trailing = gitRangeMatch.groups.trailing?.trim();
194
+ if (trailing) {
195
+ anchor = trailing;
196
+ }
197
+ else {
198
+ const oldStart = Number.parseInt(gitRangeMatch.groups.oldStart ?? "0", 10);
199
+ if (oldStart > lines.length) {
200
+ endOfFile = true;
201
+ }
202
+ else {
203
+ start = Math.max(0, oldStart - 1);
204
+ }
205
+ continue;
206
+ }
207
+ }
208
+ const candidates = [];
209
+ for (let index = start; index < lines.length; index += 1)
210
+ if (lines[index]?.text.includes(anchor))
211
+ candidates.push(index);
212
+ if (candidates.length === 0) {
213
+ // Try normalized anchor
214
+ const normAnchor = normalizeUnicode(anchor).trim();
215
+ for (let index = start; index < lines.length; index += 1) {
216
+ const line = lines[index]?.text ?? "";
217
+ if (normalizeUnicode(line).includes(normAnchor))
218
+ candidates.push(index);
219
+ }
220
+ if (candidates.length === 0) {
221
+ return { kind: "context_not_found", hunkIndex: 0, hint: `anchor "${anchor}" not found` };
222
+ }
223
+ }
224
+ if (candidates.length > 1)
225
+ return {
226
+ kind: "ambiguous_exact",
227
+ hunkIndex: 0,
228
+ candidateStartLines: candidates.map((line) => line + 1),
229
+ };
230
+ start = (candidates[0] ?? 0) + 1;
231
+ }
232
+ return { prefixLines: start, ...(endOfFile === true ? { endOfFile: true } : {}) };
233
+ }
234
+ function nearbyEnd(original, start, oldLength) {
235
+ return (original[start]?.end || original[start - 1]?.end || original[start + oldLength]?.end || "\n");
236
+ }
237
+ function changeCounts(changes) {
238
+ let addedLines = 0;
239
+ let removedLines = 0;
240
+ for (const change of changes)
241
+ for (const line of change.lines) {
242
+ if (line.startsWith("+") && !line.startsWith("+++"))
243
+ addedLines += 1;
244
+ else if (line.startsWith("-") && !line.startsWith("---"))
245
+ removedLines += 1;
246
+ }
247
+ return { addedLines, removedLines };
248
+ }
249
+ function replacementTexts(change) {
250
+ const texts = [];
251
+ for (const line of change.lines) {
252
+ if (line.startsWith("\\"))
253
+ continue;
254
+ if (line.startsWith("+") || line.startsWith(" "))
255
+ texts.push(line.slice(1));
256
+ }
257
+ return texts;
258
+ }
259
+ function applyLastNewline(result, original, hunk, afterView) {
260
+ if (result.length === 0)
261
+ return;
262
+ const last = result[result.length - 1];
263
+ const newSide = findLastLine(hunk.lines, "remove");
264
+ const oldSide = findLastLine(hunk.lines, "add");
265
+ if (last === undefined)
266
+ return;
267
+ if (newSide?.noNewline === true)
268
+ last.end = "";
269
+ else if (oldSide?.noNewline === true && newSide?.noNewline !== true)
270
+ last.end = last.end || nearbyEnd(original, original.length - 1, 0) || "\n";
271
+ else if (original.at(-1)?.end === "" &&
272
+ newSide?.noNewline !== true &&
273
+ afterView?.endsWith("\n") !== true)
274
+ last.end = "";
275
+ else if (afterView?.endsWith("\n") === true && last.end === "")
276
+ last.end = nearbyEnd(original, original.length - 1, 0) || "\n";
277
+ for (let index = 0; index < result.length - 1; index += 1) {
278
+ const line = result[index];
279
+ if (line !== undefined && line.end === "")
280
+ line.end = nearbyEnd(original, index, 0) || "\n";
281
+ }
282
+ }
283
+ function rebuildExactBytes(original, hunk, start, afterView, tier = "exact") {
284
+ const result = original.map((line) => ({ ...line }));
285
+ let cursor = start;
286
+ let removedEnds = [];
287
+ let addedCount = 0;
288
+ let lastOrigIndent = "";
289
+ let lastHunkIndent = "";
290
+ const indentMap = new Map();
291
+ for (const line of hunk.lines) {
292
+ const cleanText = stripEol(line.text);
293
+ if (line.kind === "remove") {
294
+ const origText = result[cursor]?.text ?? "";
295
+ lastOrigIndent = origText.match(/^\s*/)?.[0] ?? "";
296
+ lastHunkIndent = cleanText.match(/^\s*/)?.[0] ?? "";
297
+ indentMap.set(lastHunkIndent, lastOrigIndent);
298
+ removedEnds.push(result[cursor]?.end ?? "");
299
+ result.splice(cursor, 1);
300
+ continue;
301
+ }
302
+ if (line.kind === "context") {
303
+ if (cursor < result.length) {
304
+ const origText = result[cursor]?.text ?? "";
305
+ lastOrigIndent = origText.match(/^\s*/)?.[0] ?? "";
306
+ lastHunkIndent = cleanText.match(/^\s*/)?.[0] ?? "";
307
+ indentMap.set(lastHunkIndent, lastOrigIndent);
308
+ cursor += 1;
309
+ removedEnds = [];
310
+ addedCount = 0;
311
+ }
312
+ continue;
313
+ }
314
+ if (cursor > result.length)
315
+ cursor = result.length;
316
+ const end = line.noNewline === true
317
+ ? ""
318
+ : (removedEnds[addedCount] ??
319
+ removedEnds.at(-1) ??
320
+ result[cursor]?.end ??
321
+ nearbyEnd(result, cursor, 0));
322
+ let addText = cleanText;
323
+ if (tier === "trim" && (lastOrigIndent !== "" || lastHunkIndent !== "")) {
324
+ addText = adjustIndent(cleanText, lastOrigIndent, lastHunkIndent, indentMap);
325
+ }
326
+ result.splice(cursor, 0, { text: addText, end });
327
+ cursor += 1;
328
+ addedCount += 1;
329
+ }
330
+ applyLastNewline(result, original, hunk, afterView);
331
+ return encodeLines(result);
332
+ }
333
+ function rebuildBytes(original, hunk, changes, afterView) {
334
+ if (changes.length === 0)
335
+ return encodeLines(original);
336
+ const result = original.map((line) => ({ ...line }));
337
+ let shift = 0;
338
+ for (const change of changes) {
339
+ const start = Math.max(0, change.oldStart - 1 + shift);
340
+ const oldLength = change.oldLines;
341
+ const preferred = nearbyEnd(result, start, oldLength);
342
+ const texts = replacementTexts(change);
343
+ const replacement = texts.map((text, index) => ({
344
+ text,
345
+ end: result[start + index]?.end || preferred,
346
+ }));
347
+ result.splice(start, oldLength, ...replacement);
348
+ shift += replacement.length - oldLength;
349
+ }
350
+ applyLastNewline(result, original, hunk, afterView);
351
+ return encodeLines(result);
352
+ }
353
+ function snapshotFromChange(path, hunkIndex, change, beforeLines, afterLines) {
354
+ const beforeStart = Math.max(0, change.oldStart - 1 - SNAPSHOT_CONTEXT);
355
+ const afterStart = Math.max(0, change.newStart - 1 - SNAPSHOT_CONTEXT);
356
+ const beforeEnd = Math.min(beforeLines.length, change.oldStart - 1 + Math.max(change.oldLines, 0) + SNAPSHOT_CONTEXT);
357
+ const afterEnd = Math.min(afterLines.length, change.newStart - 1 + Math.max(change.newLines, 0) + SNAPSHOT_CONTEXT);
358
+ return Object.freeze({
359
+ path,
360
+ hunkIndex,
361
+ startLine: beforeStart + 1,
362
+ afterStartLine: afterStart + 1,
363
+ before: Object.freeze(beforeLines.slice(beforeStart, beforeEnd)),
364
+ after: Object.freeze(afterLines.slice(afterStart, afterEnd)),
365
+ });
366
+ }
367
+ function parseOneHunk(parsePatch, unifiedDiff, hunk) {
368
+ let parsed;
369
+ try {
370
+ parsed = parsePatch(unifiedDiff);
371
+ }
372
+ catch {
373
+ return { kind: "context_not_found", hunkIndex: 0 };
374
+ }
375
+ const file = parsed[0];
376
+ const parsedHunk = file?.hunks[0];
377
+ if (parsed.length !== 1 ||
378
+ file === undefined ||
379
+ file.hunks.length !== 1 ||
380
+ parsedHunk === undefined ||
381
+ file.isBinary === true ||
382
+ file.isRename === true ||
383
+ file.isCopy === true)
384
+ return { kind: "context_not_found", hunkIndex: 0 };
385
+ const oldCount = hunk.lines.filter((line) => line.kind !== "add").length;
386
+ const newCount = hunk.lines.filter((line) => line.kind !== "remove").length;
387
+ if (parsedHunk.oldLines !== oldCount || parsedHunk.newLines !== newCount)
388
+ return { kind: "context_not_found", hunkIndex: 0 };
389
+ return file;
390
+ }
391
+ export function prepareJsDiffUpdate(data) {
392
+ if (data.before.length > MAX_FILE_SIZE)
393
+ throw new Error(`file_too_large: ${data.before.length} bytes`);
394
+ const outcomes = [];
395
+ const rejected = [];
396
+ const snapshots = [];
397
+ let addedLines = 0;
398
+ let removedLines = 0;
399
+ let mode;
400
+ let after = data.before;
401
+ for (const [index, input] of data.hunks.entries()) {
402
+ const hunkIndex = index + 1;
403
+ const hunk = input.hunk;
404
+ if (after.length > MAX_FILE_SIZE)
405
+ throw new Error(`file_too_large: ${after.length} bytes`);
406
+ let original;
407
+ try {
408
+ original = textLines(after);
409
+ }
410
+ catch {
411
+ throw new Error("Patch update is not valid UTF-8 text");
412
+ }
413
+ const constraint = anchorConstraint(original, hunk);
414
+ if ("kind" in constraint) {
415
+ rejected.push({ ...constraint, hunkIndex });
416
+ continue;
417
+ }
418
+ const oldTexts = oldSideTexts(hunk);
419
+ const oldLength = oldTexts.length;
420
+ const isEof = constraint.endOfFile === true || hunk.endOfFile === true;
421
+ const anchored = original.slice(constraint.prefixLines);
422
+ const eofLines = isEof ? Math.max(0, anchored.length - oldLength) : 0;
423
+ const prefixLines = constraint.prefixLines + eofLines;
424
+ const searchable = original.slice(prefixLines);
425
+ let insertAt = prefixLines;
426
+ if (oldLength === 0) {
427
+ if (isEof || prefixLines >= original.length)
428
+ insertAt = original.length;
429
+ else
430
+ insertAt = prefixLines;
431
+ }
432
+ const tieredMatch = findTieredStarts(searchable.map((line) => line.text), oldTexts);
433
+ const matchedStarts = tieredMatch.starts.map((start) => start + prefixLines);
434
+ if (oldLength > 0 && tieredMatch.tier === "ambiguous") {
435
+ rejected.push({
436
+ kind: "ambiguous_exact",
437
+ hunkIndex,
438
+ candidateStartLines: matchedStarts.map((start) => start + 1),
439
+ });
440
+ continue;
441
+ }
442
+ const sourceView = lfView(original);
443
+ let parsed;
444
+ const shouldUseSynthDiff = oldLength > 0 &&
445
+ (tieredMatch.tier === "trimEnd" ||
446
+ tieredMatch.tier === "trim" ||
447
+ (tieredMatch.eofTrimmedCount ?? 0) > 0);
448
+ if (shouldUseSynthDiff) {
449
+ // Synthesize a structured patch aligned with the original lines so applyPatch succeeds
450
+ const matchStart = matchedStarts[0] ?? prefixLines;
451
+ let origCursor = matchStart;
452
+ let lastOrigIndent = "";
453
+ let lastHunkIndent = "";
454
+ const indentMap = new Map();
455
+ const bodyLines = [];
456
+ let emittedOld = 0;
457
+ let emittedNew = 0;
458
+ for (const line of hunk.lines) {
459
+ const cleanText = stripEol(line.text);
460
+ if (line.kind === "context") {
461
+ if (origCursor < original.length) {
462
+ const origLine = original[origCursor];
463
+ const text = origLine?.text ?? cleanText;
464
+ lastOrigIndent = text.match(/^\s*/)?.[0] ?? "";
465
+ lastHunkIndent = cleanText.match(/^\s*/)?.[0] ?? "";
466
+ indentMap.set(lastHunkIndent, lastOrigIndent);
467
+ bodyLines.push(` ${text}\n`);
468
+ if (origLine?.end === "")
469
+ bodyLines.push("\\n");
470
+ origCursor += 1;
471
+ emittedOld += 1;
472
+ emittedNew += 1;
473
+ }
474
+ continue;
475
+ }
476
+ if (line.kind === "remove") {
477
+ const origLine = original[origCursor];
478
+ const text = origLine?.text ?? cleanText;
479
+ lastOrigIndent = text.match(/^\s*/)?.[0] ?? "";
480
+ lastHunkIndent = cleanText.match(/^\s*/)?.[0] ?? "";
481
+ indentMap.set(lastHunkIndent, lastOrigIndent);
482
+ bodyLines.push(`-${text}\n`);
483
+ if (origLine?.end === "" || line.noNewline === true) {
484
+ bodyLines.push("\\n");
485
+ }
486
+ origCursor += 1;
487
+ emittedOld += 1;
488
+ continue;
489
+ }
490
+ let addText = cleanText;
491
+ if (tieredMatch.tier === "trim" && (lastOrigIndent !== "" || lastHunkIndent !== "")) {
492
+ addText = adjustIndent(cleanText, lastOrigIndent, lastHunkIndent, indentMap);
493
+ }
494
+ bodyLines.push(`+${addText}\n`);
495
+ if (line.noNewline === true)
496
+ bodyLines.push("\\n");
497
+ emittedNew += 1;
498
+ }
499
+ const diffLines = [
500
+ "--- a/target\n",
501
+ "+++ b/target\n",
502
+ `@@ -${matchStart + 1},${emittedOld} +${matchStart + 1},${emittedNew} @@\n`,
503
+ ...bodyLines,
504
+ ];
505
+ const synthDiff = diffLines.join("");
506
+ try {
507
+ parsed = parsePatch(synthDiff)[0] ?? { kind: "context_not_found", hunkIndex: 0 };
508
+ }
509
+ catch {
510
+ parsed = parseOneHunk(parsePatch, input.unifiedDiff, hunk);
511
+ }
512
+ }
513
+ else {
514
+ parsed = parseOneHunk(parsePatch, input.unifiedDiff, hunk);
515
+ }
516
+ if ("kind" in parsed) {
517
+ rejected.push({ ...parsed, hunkIndex });
518
+ continue;
519
+ }
520
+ const parsedHunk = parsed.hunks[0];
521
+ if (parsedHunk === undefined) {
522
+ const hint = findDiagnosticHint(searchable.map((line) => line.text), oldTexts, prefixLines);
523
+ rejected.push({
524
+ kind: "context_not_found",
525
+ hunkIndex,
526
+ ...(hint !== undefined ? { hint } : {}),
527
+ });
528
+ continue;
529
+ }
530
+ if (oldLength === 0) {
531
+ parsedHunk.oldStart = insertAt + 1;
532
+ parsedHunk.newStart = insertAt + 1;
533
+ }
534
+ else if (matchedStarts.length === 1) {
535
+ const start = matchedStarts[0] ?? 0;
536
+ parsedHunk.oldStart = start + 1;
537
+ parsedHunk.newStart = start + 1;
538
+ }
539
+ let applied = applyPatch(sourceView, parsed, {
540
+ fuzzFactor: 0,
541
+ autoConvertLineEndings: false,
542
+ });
543
+ let match = "exact";
544
+ if (applied === false && matchedStarts.length === 0 && data.fuzzFactor > 0) {
545
+ applied = applyPatch(sourceView, parsed, {
546
+ fuzzFactor: data.fuzzFactor,
547
+ autoConvertLineEndings: false,
548
+ });
549
+ match = "fuzzy";
550
+ }
551
+ if (applied === false) {
552
+ const hint = findDiagnosticHint(searchable.map((line) => line.text), oldTexts, prefixLines);
553
+ rejected.push({
554
+ kind: "context_not_found",
555
+ hunkIndex,
556
+ ...(hint !== undefined ? { hint } : {}),
557
+ });
558
+ continue;
559
+ }
560
+ const changes = structuredPatch("before", "after", sourceView, applied, undefined, undefined, {
561
+ context: 0,
562
+ }).hunks.filter((change) => change.lines.some((line) => line.startsWith("+") || line.startsWith("-")));
563
+ const canUseExactRebuild = matchedStarts.length === 1 || oldLength === 0;
564
+ const rebuilt = canUseExactRebuild
565
+ ? rebuildExactBytes(original, hunk, oldLength === 0 ? insertAt : (matchedStarts[0] ?? insertAt), applied, tieredMatch.tier)
566
+ : rebuildBytes(original, hunk, changes, applied);
567
+ if (rebuilt.length > MAX_FILE_SIZE)
568
+ throw new Error(`file_too_large: ${rebuilt.length} bytes`);
569
+ const rebuiltLines = textLines(rebuilt);
570
+ const rebuiltView = lfView(rebuiltLines);
571
+ const finalChanges = structuredPatch("before", "after", sourceView, rebuiltView, undefined, undefined, {
572
+ context: 0,
573
+ }).hunks.filter((change) => change.lines.some((line) => line.startsWith("+") || line.startsWith("-")));
574
+ const counts = changeCounts(finalChanges);
575
+ addedLines += counts.addedLines;
576
+ removedLines += counts.removedLines;
577
+ let startLine;
578
+ let length;
579
+ if (finalChanges.length === 0) {
580
+ startLine = oldLength === 0 ? insertAt + 1 : (matchedStarts[0] ?? prefixLines) + 1;
581
+ length = 0;
582
+ }
583
+ else {
584
+ const starts = finalChanges.map((change) => change.oldStart);
585
+ const ends = finalChanges.map((change) => change.oldLines === 0 ? change.oldStart : change.oldStart + change.oldLines);
586
+ startLine = Math.min(...starts);
587
+ const end = Math.max(...ends);
588
+ length = finalChanges.every((change) => change.oldLines === 0) ? 0 : end - startLine;
589
+ }
590
+ const userVisibleMatch = match === "fuzzy" || tieredMatch.tier !== "exact" || (tieredMatch.eofTrimmedCount ?? 0) > 0
591
+ ? "fuzzy"
592
+ : "exact";
593
+ outcomes.push({ kind: "applied", hunkIndex, startLine, length, match: userVisibleMatch });
594
+ const beforeTexts = original.map((line) => line.text);
595
+ const afterTexts = rebuiltLines.map((line) => line.text);
596
+ for (const change of finalChanges)
597
+ snapshots.push(snapshotFromChange(data.snapshotPath, hunkIndex, change, beforeTexts, afterTexts));
598
+ if (match === "fuzzy")
599
+ mode = "fuzzy";
600
+ else
601
+ mode ??= "exact";
602
+ after = rebuilt;
603
+ }
604
+ return {
605
+ after,
606
+ mode,
607
+ addedLines,
608
+ removedLines,
609
+ outcomes: Object.freeze(outcomes),
610
+ rejected: Object.freeze(rejected),
611
+ snapshots: Object.freeze(snapshots),
612
+ };
613
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ // Keep worker imports lean: the ext-core barrel adds ~220ms of startup per update.
2
+ import { parentPort, workerData } from "node:worker_threads";
3
+ import { errorMessage } from "@hheei/pi-ext-core/errors";
4
+ try {
5
+ // Node runs source workers directly in tests, without Vite's .js -> .ts resolution.
6
+ const matchingUrl = new URL(import.meta.url.endsWith(".ts") ? "./jsdiff-matching.ts" : "./jsdiff-matching.js", import.meta.url);
7
+ const { prepareJsDiffUpdate } = await import(matchingUrl.href);
8
+ parentPort?.postMessage({
9
+ kind: "result",
10
+ value: prepareJsDiffUpdate(workerData),
11
+ });
12
+ }
13
+ catch (error) {
14
+ parentPort?.postMessage({ kind: "error", message: errorMessage(error) });
15
+ }
16
+ finally {
17
+ parentPort?.close();
18
+ }
@@ -0,0 +1,9 @@
1
+ import type { PreparedPatchUpdate } from "./outcome.js";
2
+ interface RunJsDiffUpdateOptions {
3
+ readonly before: Uint8Array;
4
+ readonly operation: import("./parser.js").V4aUpdateOperation;
5
+ readonly fuzzFactor: number;
6
+ readonly signal?: AbortSignal;
7
+ }
8
+ export declare function runJsDiffUpdate(options: RunJsDiffUpdateOptions): Promise<PreparedPatchUpdate>;
9
+ export {};