typebulb 0.20.4 → 0.21.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.
@@ -0,0 +1,2332 @@
1
+ // Generated by typebulb — do not edit; overwritten on each typebulb run.
2
+
3
+ // cli/agents/pi/server/piPatcherExtension.ts
4
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { Type } from "typebox";
7
+
8
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/utils/textUtils.js
9
+ var TextUtils = class _TextUtils {
10
+ static NormalizeToLf(text) {
11
+ return (text ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
12
+ }
13
+ static DetectNewline(text) {
14
+ if (!text || text.length === 0)
15
+ return "\n";
16
+ const idx = text.search(/[\r\n]/);
17
+ if (idx < 0)
18
+ return "\n";
19
+ return text[idx] === "\r" ? idx + 1 < text.length && text[idx + 1] === "\n" ? "\r\n" : "\r" : "\n";
20
+ }
21
+ /// <summary>Normalize + split to LF-only lines.</summary>
22
+ static ToLines(text) {
23
+ return _TextUtils.NormalizeToLf(text).split("\n");
24
+ }
25
+ /// <summary>Count of leading characters drawn from `chars` (default: space or tab).</summary>
26
+ static CountLeadingWhitespace(s, chars = " ") {
27
+ let i = 0;
28
+ while (i < s.length && chars.includes(s[i]))
29
+ i++;
30
+ return i;
31
+ }
32
+ /// <summary>Maps chat-layer typography (smart quotes, Unicode dashes, special
33
+ /// spaces) to ASCII. Match-only: output text is never built from this.
34
+ /// Numeric code points throughout: invisible/lookalike literals would make this
35
+ /// file unmatchable by byte-exact edit tools, including this patcher's own MCP.</summary>
36
+ static NormalizeHomoglyphs(line) {
37
+ return [...line].some((c) => c.charCodeAt(0) >= 160) ? [...line].map((c) => _TextUtils.MapHomoglyph(c)).join("") : line;
38
+ }
39
+ static MapHomoglyph(c) {
40
+ const cp = c.codePointAt(0);
41
+ if (cp >= 8216 && cp <= 8219)
42
+ return "'";
43
+ if (cp >= 8220 && cp <= 8223)
44
+ return '"';
45
+ if (cp >= 8208 && cp <= 8213 || cp === 8722)
46
+ return "-";
47
+ if (cp === 160 || cp >= 8194 && cp <= 8202 || cp === 8239 || cp === 8287 || cp === 12288)
48
+ return " ";
49
+ return c;
50
+ }
51
+ /// <summary>Removes zero-width/control code points for match-tolerant
52
+ /// comparison. Match-only, like NormalizeHomoglyphs: output text is never
53
+ /// built from this.</summary>
54
+ static StripInvisibles(line) {
55
+ return [...line].some((c) => _TextUtils.IsInvisible(c)) ? [...line].filter((c) => !_TextUtils.IsInvisible(c)).join("") : line;
56
+ }
57
+ /// <summary>Folds NFC canonical equivalence and fullwidth ASCII forms
58
+ /// (U+FF01..U+FF5E) for match-tolerant comparison. Match-only, like
59
+ /// NormalizeHomoglyphs. The NFKC compat remainder (ligatures, superscripts)
60
+ /// stays unfolded: visually distinct content must not cross-match.</summary>
61
+ static FoldCanonicalAndFullwidth(line) {
62
+ if (![...line].some((c) => c.charCodeAt(0) >= 768))
63
+ return line;
64
+ const folded = [...line].map((c) => {
65
+ const cp = c.charCodeAt(0);
66
+ return cp >= 65281 && cp <= 65374 ? String.fromCharCode(cp - 65248) : c;
67
+ }).join("");
68
+ return _TextUtils.HasUnpairedSurrogate(folded) ? folded : folded.normalize("NFC");
69
+ }
70
+ static HasUnpairedSurrogate(s) {
71
+ for (let i = 0; i < s.length; i++) {
72
+ const cp = s.charCodeAt(i);
73
+ if (cp < 55296 || cp > 57343)
74
+ continue;
75
+ const next = i + 1 < s.length ? s.charCodeAt(i + 1) : 0;
76
+ if (cp > 56319 || next < 56320 || next > 57343)
77
+ return true;
78
+ i++;
79
+ }
80
+ return false;
81
+ }
82
+ // Zero-width/control code points that render as nothing (or reorder text):
83
+ // the classes JSON-escape mangling degrades into. Tab excluded: real content.
84
+ // Numeric code points, not escapes or literals: they survive tool boundaries
85
+ // that decode backslash escapes or eat raw control chars.
86
+ static IsInvisible(c) {
87
+ const cp = c.codePointAt(0);
88
+ if (cp === 9)
89
+ return false;
90
+ return cp < 32 || cp >= 127 && cp <= 159 || cp === 173 || cp === 1564 || cp === 6158 || cp >= 8203 && cp <= 8207 || cp >= 8234 && cp <= 8238 || cp >= 8288 && cp <= 8292 || cp >= 8294 && cp <= 8297 || cp === 65279;
91
+ }
92
+ /// <summary>Round-trip variant that accepts updated LF lines.</summary>
93
+ static RoundTripWhitespace(originalSnapshot, updatedLines) {
94
+ const joined = updatedLines.join("\n");
95
+ const origHadTrailing = originalSnapshot.endsWith("\n") || originalSnapshot.endsWith("\r\n") || originalSnapshot.length === 0;
96
+ let result = joined;
97
+ if (origHadTrailing && !joined.endsWith("\n") && joined.length > 0)
98
+ result += "\n";
99
+ else if (!origHadTrailing && joined.endsWith("\n"))
100
+ result = result.slice(0, -1);
101
+ const newline = _TextUtils.DetectNewline(originalSnapshot);
102
+ return newline === "\n" ? result : result.replace(/\n/g, newline);
103
+ }
104
+ };
105
+
106
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/utils/arrayUtils.js
107
+ var ArrayUtils = class {
108
+ /// <summary>
109
+ /// Groups items by key, preserving first-seen key order (like LINQ's GroupBy).
110
+ /// </summary>
111
+ static GroupBy(items, keySelector) {
112
+ const map = /* @__PURE__ */ new Map();
113
+ for (const item of items) {
114
+ const key = keySelector(item);
115
+ const arr = map.get(key) || [];
116
+ arr.push(item);
117
+ map.set(key, arr);
118
+ }
119
+ const result = [];
120
+ for (const [key, values] of map.entries())
121
+ result.push({ key, values });
122
+ return result;
123
+ }
124
+ /// <summary>
125
+ /// Returns a new array sorted ascending by key (stable, non-mutating, like LINQ's OrderBy).
126
+ /// </summary>
127
+ static OrderBy(items, keySelector) {
128
+ return [...items].sort((a, b) => {
129
+ const ka = keySelector(a), kb = keySelector(b);
130
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
131
+ });
132
+ }
133
+ /// <summary>
134
+ /// Returns a new array sorted descending by key (stable, non-mutating, like LINQ's OrderByDescending).
135
+ /// </summary>
136
+ static OrderByDescending(items, keySelector) {
137
+ return [...items].sort((a, b) => {
138
+ const ka = keySelector(a), kb = keySelector(b);
139
+ return ka > kb ? -1 : ka < kb ? 1 : 0;
140
+ });
141
+ }
142
+ };
143
+
144
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/models.js
145
+ var PatchInputFile = class {
146
+ Key;
147
+ InputFullText;
148
+ InputSelectedText;
149
+ constructor(Key, InputFullText = "", InputSelectedText = "") {
150
+ this.Key = Key;
151
+ this.InputFullText = InputFullText;
152
+ this.InputSelectedText = InputSelectedText;
153
+ }
154
+ };
155
+ var PatchOptions = class {
156
+ ContinueOnError;
157
+ MaxErrorIterations;
158
+ ContextWindowMax;
159
+ MaxChunksPerHunk;
160
+ SanitizeDiff;
161
+ Truncation;
162
+ ControlChars;
163
+ constructor(ContinueOnError = true, MaxErrorIterations = 10, ContextWindowMax = 8, MaxChunksPerHunk = 64, SanitizeDiff = true, Truncation = "warn", ControlChars = "error") {
164
+ this.ContinueOnError = ContinueOnError;
165
+ this.MaxErrorIterations = MaxErrorIterations;
166
+ this.ContextWindowMax = ContextWindowMax;
167
+ this.MaxChunksPerHunk = MaxChunksPerHunk;
168
+ this.SanitizeDiff = SanitizeDiff;
169
+ this.Truncation = Truncation;
170
+ this.ControlChars = ControlChars;
171
+ }
172
+ };
173
+ var PatchOutputFile = class {
174
+ Key;
175
+ Fuzz;
176
+ Edits;
177
+ InputSelectedText;
178
+ InputFullText;
179
+ OutputFullText;
180
+ Errors;
181
+ AlreadyAppliedCount;
182
+ CollapsedMarkerLines;
183
+ TruncationSuspected;
184
+ ControlCharsSuspected;
185
+ constructor(Key, Fuzz, Edits, InputSelectedText, InputFullText, OutputFullText, Errors, AlreadyAppliedCount = 0, CollapsedMarkerLines = [], TruncationSuspected = false, ControlCharsSuspected = false) {
186
+ this.Key = Key;
187
+ this.Fuzz = Fuzz;
188
+ this.Edits = Edits;
189
+ this.InputSelectedText = InputSelectedText;
190
+ this.InputFullText = InputFullText;
191
+ this.OutputFullText = OutputFullText;
192
+ this.Errors = Errors;
193
+ this.AlreadyAppliedCount = AlreadyAppliedCount;
194
+ this.CollapsedMarkerLines = CollapsedMarkerLines;
195
+ this.TruncationSuspected = TruncationSuspected;
196
+ this.ControlCharsSuspected = ControlCharsSuspected;
197
+ }
198
+ };
199
+ var PatchOutput = class {
200
+ Files;
201
+ constructor(Files) {
202
+ this.Files = Files;
203
+ }
204
+ // Patch-scoped errors that belong to no input file: hunks naming files outside
205
+ // the input set (FileMismatch). Kept off the per-file channel so one foreign
206
+ // hunk cannot mark otherwise-clean files as failed — per-file Errors are about
207
+ // that file only. Loudness is preserved: throw-mode still throws these, and
208
+ // callers composing success replies must consult BOTH channels.
209
+ Errors = [];
210
+ };
211
+ var LineType;
212
+ (function(LineType2) {
213
+ LineType2[LineType2["Context"] = 0] = "Context";
214
+ LineType2[LineType2["Delete"] = 1] = "Delete";
215
+ LineType2[LineType2["Insert"] = 2] = "Insert";
216
+ })(LineType || (LineType = {}));
217
+ var DiffLine = class {
218
+ Type;
219
+ Text;
220
+ CollapsedFrom;
221
+ constructor(Type2, Text, CollapsedFrom = null) {
222
+ this.Type = Type2;
223
+ this.Text = Text;
224
+ this.CollapsedFrom = CollapsedFrom;
225
+ }
226
+ };
227
+ var Hunk = class {
228
+ Key;
229
+ OldText;
230
+ NewText;
231
+ OldStart;
232
+ constructor(Key, OldText, NewText, OldStart = -1) {
233
+ this.Key = Key;
234
+ this.OldText = OldText;
235
+ this.NewText = NewText;
236
+ this.OldStart = OldStart;
237
+ }
238
+ Lines = [];
239
+ /// <summary>
240
+ /// 1-based start position for the new side when available (from +c,d header), otherwise -1.
241
+ /// Unused, but here for Unified Diff completeness.
242
+ /// </summary>
243
+ NewStart = -1;
244
+ /// <summary>
245
+ /// 0-based line number in the original diff text where this hunk's body (first diff body line) starts.
246
+ /// -1 when unavailable (e.g., parser could not determine it).
247
+ /// </summary>
248
+ DiffBodyStartLine = -1;
249
+ // Set by the parser under TruncationPolicy 'warn' on the final hunk when it
250
+ // carried the tear signature.
251
+ TruncationSuspected = false;
252
+ // Set by the parser under ControlCharPolicy 'warn' when this hunk's insert
253
+ // lines carried raw control characters.
254
+ ControlCharsSuspected = false;
255
+ };
256
+ var Edit = class _Edit {
257
+ LineIndex;
258
+ DeleteLines;
259
+ InsertLines;
260
+ Fuzz;
261
+ constructor(LineIndex, DeleteLines, InsertLines, Fuzz = 0) {
262
+ this.LineIndex = LineIndex;
263
+ this.DeleteLines = DeleteLines;
264
+ this.InsertLines = InsertLines;
265
+ this.Fuzz = Fuzz;
266
+ }
267
+ Shift(lineDelta) {
268
+ return new _Edit(this.LineIndex + lineDelta, this.DeleteLines, this.InsertLines, this.Fuzz);
269
+ }
270
+ ApplyTo(lines) {
271
+ if (this.LineIndex < 0 || this.LineIndex > lines.length)
272
+ return;
273
+ const deleteCount = Math.min(this.DeleteLines.length, lines.length - this.LineIndex);
274
+ if (deleteCount > 0)
275
+ lines.splice(this.LineIndex, deleteCount);
276
+ if (this.InsertLines.length > 0)
277
+ lines.splice(this.LineIndex, 0, ...this.InsertLines);
278
+ }
279
+ };
280
+ var DiffLocation = class {
281
+ StartLine;
282
+ Length;
283
+ constructor(startOrHunk, lengthOrStartLine, endLine) {
284
+ if (typeof startOrHunk === "number") {
285
+ this.StartLine = startOrHunk;
286
+ this.Length = lengthOrStartLine ?? 0;
287
+ } else {
288
+ const hunk = startOrHunk;
289
+ const startLine = lengthOrStartLine ?? 0;
290
+ const end = endLine ?? hunk.Lines.length - 1;
291
+ this.StartLine = hunk.DiffBodyStartLine + startLine + 1;
292
+ this.Length = end - startLine + 1;
293
+ }
294
+ }
295
+ };
296
+ var FileHunkGroup = class {
297
+ Key;
298
+ Hunks;
299
+ constructor(Key, Hunks) {
300
+ this.Key = Key;
301
+ this.Hunks = Hunks;
302
+ }
303
+ };
304
+ var Chunk = class _Chunk {
305
+ ContextBefore;
306
+ DeleteLines;
307
+ InsertLines;
308
+ ContextAfter;
309
+ Match;
310
+ DiffLocation;
311
+ constructor(ContextBefore, DeleteLines, InsertLines, ContextAfter, Match2, DiffLocation2) {
312
+ this.ContextBefore = ContextBefore;
313
+ this.DeleteLines = DeleteLines;
314
+ this.InsertLines = InsertLines;
315
+ this.ContextAfter = ContextAfter;
316
+ this.Match = Match2;
317
+ this.DiffLocation = DiffLocation2;
318
+ }
319
+ get IsPureInsert() {
320
+ return this.DeleteLines.length == 0 && this.InsertLines.length > 0;
321
+ }
322
+ get IsPureDelete() {
323
+ return this.DeleteLines.length > 0 && this.InsertLines.length == 0;
324
+ }
325
+ HasContextLines() {
326
+ return this.ContextBefore.length > 0 || this.ContextAfter.length > 0;
327
+ }
328
+ DeleteLinesWithContext() {
329
+ return [...this.ContextBefore, ...this.DeleteLines, ...this.ContextAfter];
330
+ }
331
+ InsertLinesWithContext() {
332
+ return [...this.ContextBefore, ...this.InsertLines, ...this.ContextAfter];
333
+ }
334
+ with(values) {
335
+ return new _Chunk(values.ContextBefore ?? this.ContextBefore, values.DeleteLines ?? this.DeleteLines, values.InsertLines ?? this.InsertLines, values.ContextAfter ?? this.ContextAfter, values.Match ?? this.Match, values.DiffLocation ?? this.DiffLocation);
336
+ }
337
+ };
338
+ var MatchState;
339
+ (function(MatchState2) {
340
+ MatchState2[MatchState2["Success"] = 0] = "Success";
341
+ MatchState2[MatchState2["Ambiguous"] = 1] = "Ambiguous";
342
+ MatchState2[MatchState2["NotFound"] = 2] = "NotFound";
343
+ })(MatchState || (MatchState = {}));
344
+ var UniqueMatch = class _UniqueMatch {
345
+ State;
346
+ LineIndex;
347
+ Fuzz;
348
+ constructor(State, LineIndex = -1, Fuzz = 0) {
349
+ this.State = State;
350
+ this.LineIndex = LineIndex;
351
+ this.Fuzz = Fuzz;
352
+ }
353
+ get IsAmbiguous() {
354
+ return this.State == MatchState.Ambiguous;
355
+ }
356
+ get IsNotFound() {
357
+ return this.State == MatchState.NotFound;
358
+ }
359
+ get IsSuccess() {
360
+ return this.State == MatchState.Success;
361
+ }
362
+ static get Ambiguous() {
363
+ return new _UniqueMatch(MatchState.Ambiguous);
364
+ }
365
+ static get NotFound() {
366
+ return new _UniqueMatch(MatchState.NotFound);
367
+ }
368
+ with(values) {
369
+ return new _UniqueMatch(values.State ?? this.State, values.LineIndex ?? this.LineIndex, values.Fuzz ?? this.Fuzz);
370
+ }
371
+ };
372
+
373
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/utils/yaml.js
374
+ var Yaml = class _Yaml {
375
+ _sb = [];
376
+ // -------- Public API --------
377
+ AppendLine(text) {
378
+ this._sb.push(text + "\n");
379
+ }
380
+ Section(keyWithIndent) {
381
+ const [indent, key] = _Yaml.SplitIndentAndKey(keyWithIndent);
382
+ this._sb.push(`${indent}${key}:
383
+ `);
384
+ }
385
+ Scalar(keyWithIndent, value) {
386
+ const [indent, key] = _Yaml.SplitIndentAndKey(keyWithIndent);
387
+ this._sb.push(`${indent}${key}: ${value}
388
+ `);
389
+ }
390
+ Folded(keyWithIndent, value, contentIndent = 2, chompStrip = true) {
391
+ this.WriteBlockScalar(">", keyWithIndent, _Yaml.SplitLines(value), contentIndent, chompStrip);
392
+ }
393
+ Block(keyWithIndent, lines, contentIndent = 2, chompStrip = true) {
394
+ this.WriteBlockScalar("|", keyWithIndent, lines ?? [], contentIndent, chompStrip);
395
+ }
396
+ ToString() {
397
+ return this._sb.join("");
398
+ }
399
+ static SplitIndentAndKey(keyWithIndent) {
400
+ const leadingSpaces = TextUtils.CountLeadingWhitespace(keyWithIndent, " ");
401
+ const indentStr = " ".repeat(leadingSpaces);
402
+ const key = keyWithIndent.replace(/^\s+/, "");
403
+ return [indentStr, key];
404
+ }
405
+ static SplitLines(value) {
406
+ if (value == null || value.length == 0)
407
+ return [];
408
+ const lf = value.indexOf("\r") >= 0 ? value.replace(/\r\n/g, "\n").replace(/\r/g, "\n") : value;
409
+ return lf.split("\n");
410
+ }
411
+ WriteBlockScalar(style, keyWithIndent, lines, contentIndent, chompStrip) {
412
+ const [indent, key] = _Yaml.SplitIndentAndKey(keyWithIndent);
413
+ const chomp = chompStrip ? "-" : "+";
414
+ this._sb.push(`${indent}${key}: ${style}${contentIndent}${chomp}
415
+ `);
416
+ const pad = " ".repeat(contentIndent);
417
+ let any = false;
418
+ for (const line of lines) {
419
+ any = true;
420
+ this._sb.push(`${indent}${pad}${line}
421
+ `);
422
+ }
423
+ if (!any) {
424
+ this._sb.push(`${indent}${pad}
425
+ `);
426
+ }
427
+ }
428
+ };
429
+
430
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/exceptions.js
431
+ var PatchException = class extends Error {
432
+ Error;
433
+ constructor(error) {
434
+ super(error.SuggestedFixYaml);
435
+ this.Error = error;
436
+ }
437
+ };
438
+ var PatchParserException = class extends Error {
439
+ constructor(message) {
440
+ super(message ?? "Malformed diff patch");
441
+ }
442
+ };
443
+ var PatchError = class {
444
+ Type;
445
+ FailedMatch;
446
+ // For FileMismatch: the foreign file key the hunk group named, so callers can
447
+ // say WHICH file's hunks were ignored (and match it against their own target).
448
+ FileKey;
449
+ constructor(errorType, chunk, fileKey = null) {
450
+ this.Type = errorType;
451
+ this.FailedMatch = chunk;
452
+ this.FileKey = fileKey;
453
+ }
454
+ toString() {
455
+ return `Failed to patch text with the diff provided.
456
+ ${this.SuggestedFixYaml}`;
457
+ }
458
+ get SuggestedFixYaml() {
459
+ const SummaryFor = (type) => ({
460
+ "MatchNotFound": "Matching lines not found; make sure the diff's context and deleted lines exactly match the original text.",
461
+ "MatchAmbiguous": "Matched multiple locations; make sure there are enough context lines to uniquely identify an edit.",
462
+ "ChunkDuplicated": "Duplicate chunks.",
463
+ "ChunkOverlapping": "Overlapping chunks.",
464
+ "FileMismatch": "The hunk's file header names a file that is not being patched, so it was not applied. Send hunks only for the file(s) being patched."
465
+ })[type] ?? "Patch failed.";
466
+ const diffLoc = this.FailedMatch.DiffLocation;
467
+ const y = new Yaml();
468
+ y.AppendLine("PatchError:");
469
+ y.Scalar(" Type", this.Type.toString());
470
+ if (this.FileKey != null)
471
+ y.Scalar(" File", this.FileKey);
472
+ y.Folded(" Summary", SummaryFor(this.Type));
473
+ y.Section(" Details");
474
+ y.Section(" FailedMatch");
475
+ y.Block(" ContextBefore", this.FailedMatch.ContextBefore);
476
+ y.Block(" DeleteLines", this.FailedMatch.DeleteLines);
477
+ y.Block(" InsertLines", this.FailedMatch.InsertLines);
478
+ y.Block(" ContextAfter", this.FailedMatch.ContextAfter);
479
+ y.Section(" DiffLocation");
480
+ y.Scalar(" StartLine", diffLoc.StartLine.toString());
481
+ y.Scalar(" Length", diffLoc.Length.toString());
482
+ return y.ToString();
483
+ }
484
+ };
485
+
486
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/applier/guards.js
487
+ var DuplicateGuard = class _DuplicateGuard {
488
+ // Two chunks that resolve to the same anchor with the same deletes and inserts are
489
+ // provably the same edit (an LLM re-emitting a hunk). Applying it once is
490
+ // unambiguously correct, so keep the first occurrence and drop the rest — the lenient
491
+ // move, with no false-positive risk: genuinely distinct edits differ in key
492
+ // (anchor or content) and fall through to OverlapGuard.
493
+ static Dedupe(args) {
494
+ const seen = /* @__PURE__ */ new Set();
495
+ const kept = [];
496
+ for (const chunk of args.Chunks) {
497
+ const key = _DuplicateGuard.BuildKey(chunk);
498
+ if (!seen.has(key)) {
499
+ seen.add(key);
500
+ kept.push(chunk);
501
+ }
502
+ }
503
+ return new InputChunks(args.InputFullText, kept, args.Options);
504
+ }
505
+ static BuildKey(c) {
506
+ let sb = "";
507
+ sb += `${c.Match?.LineIndex ?? -1}
508
+ `;
509
+ for (const l of c.DeleteLines)
510
+ sb += `${l}
511
+ `;
512
+ sb += `\u2192
513
+ `;
514
+ for (const l of c.InsertLines)
515
+ sb += `${l}
516
+ `;
517
+ return sb.toString();
518
+ }
519
+ };
520
+ var OverlapGuard = class {
521
+ static Guard(args) {
522
+ const chunks = ArrayUtils.OrderBy(args.Chunks, (c) => c.Match?.LineIndex ?? -1);
523
+ let currentEnd = 0;
524
+ for (const chunk of chunks) {
525
+ const idx = chunk.Match?.LineIndex ?? -1;
526
+ if (idx < 0)
527
+ continue;
528
+ if (idx < currentEnd)
529
+ throw new PatchException(new PatchError("ChunkOverlapping", chunk));
530
+ if (chunk.DeleteLines.length > 0)
531
+ currentEnd = idx + chunk.DeleteLines.length;
532
+ }
533
+ }
534
+ };
535
+
536
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/applier/editMinimizer.js
537
+ var EditMinimizer = class _EditMinimizer {
538
+ static Same(a, b) {
539
+ return a === b;
540
+ }
541
+ /// <summary>
542
+ /// Convert an edit into a sequence of minimal edits, by excluding all unchanged lines.
543
+ /// </summary>
544
+ static *ToMinimalEdits(edit) {
545
+ const deleted = edit.DeleteLines;
546
+ const inserted = edit.InsertLines;
547
+ const maxLength = Math.max(deleted.length, inserted.length);
548
+ let diffStart = -1;
549
+ for (let i = 0; i <= maxLength; i++) {
550
+ const withinDeleted = i < deleted.length;
551
+ const withinInserted = i < inserted.length;
552
+ const atEnd = i == maxLength;
553
+ const isMatch = withinDeleted && withinInserted && _EditMinimizer.Same(deleted[i], inserted[i]);
554
+ const isDiff = !atEnd && (!withinDeleted || !withinInserted || !isMatch);
555
+ const isBoundary = atEnd || isMatch;
556
+ if (diffStart == -1 && isDiff) {
557
+ diffStart = i;
558
+ } else if (diffStart != -1 && isBoundary) {
559
+ const delCount = Math.max(0, Math.min(i, deleted.length) - diffStart);
560
+ const insCount = Math.max(0, Math.min(i, inserted.length) - diffStart);
561
+ const delBlock = delCount <= 0 ? [] : deleted.slice(diffStart, diffStart + delCount);
562
+ const insBlock = insCount <= 0 ? [] : inserted.slice(diffStart, diffStart + insCount);
563
+ yield new Edit(edit.LineIndex + diffStart, delBlock, insBlock, edit.Fuzz);
564
+ diffStart = -1;
565
+ }
566
+ }
567
+ }
568
+ };
569
+
570
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/applier/indenter.js
571
+ var Anchor = class {
572
+ BaseIndent;
573
+ Shift;
574
+ constructor(BaseIndent, Shift) {
575
+ this.BaseIndent = BaseIndent;
576
+ this.Shift = Shift;
577
+ }
578
+ };
579
+ var Indenter = class _Indenter {
580
+ static DefaultTabSize = 4;
581
+ /// <summary>
582
+ /// Aligns an inserted block (<see cref="Chunk.InsertLines"/>) to the file’s
583
+ /// indent context at <see cref="Chunk.LineIndex"/>, leveraging
584
+ /// ContextBefore / ContextAfter when available.
585
+ /// </summary>
586
+ static AlignInsert(lines, chunk) {
587
+ const insert = chunk.InsertLines;
588
+ if (insert.length == 0)
589
+ return insert;
590
+ const tabSize = _Indenter.InferTabSize(lines.concat(insert));
591
+ const anchor = _Indenter.FindAnchor(lines, chunk.ContextBefore, chunk.Match.LineIndex - 1, true, tabSize) ?? _Indenter.FindAnchor(lines, chunk.ContextAfter, chunk.Match.LineIndex, false, tabSize) ?? new Anchor("", 0);
592
+ const reindented = new Set(chunk.DeleteLines.map((l) => _Indenter.SplitIndent(l)[1]));
593
+ const blankKeep = new Set(insert.filter((l) => !_Indenter.IsBlank(l)).map(_Indenter.LeadingWs).concat([anchor.BaseIndent]));
594
+ const tabStyle = anchor.BaseIndent.length > 0 ? anchor.BaseIndent.indexOf(" ") >= 0 : lines.some((l) => l.startsWith(" "));
595
+ return insert.map((l) => _Indenter.Adjust(l, anchor, tabSize, tabStyle, reindented, blankKeep));
596
+ }
597
+ static FindAnchor(lines, contextLines, startIndex, backward, tabSize) {
598
+ if (contextLines.length == 0)
599
+ return null;
600
+ const [ctxWs, ctxTrim] = _Indenter.SplitIndent(contextLines[backward ? contextLines.length - 1 : 0]);
601
+ const ctxW = _Indenter.VisualWidth(ctxWs, tabSize);
602
+ const step = backward ? -1 : 1;
603
+ const end = backward ? -1 : lines.length;
604
+ for (let i = startIndex; i != end; i += step) {
605
+ const [ws, trim] = _Indenter.SplitIndent(lines[i]);
606
+ if (trim != ctxTrim)
607
+ continue;
608
+ return new Anchor(ws, _Indenter.VisualWidth(ws, tabSize) - ctxW);
609
+ }
610
+ return null;
611
+ }
612
+ static Adjust(line, anchor, tabSize, tabStyle, reindented, blankKeep) {
613
+ if (_Indenter.IsBlank(line))
614
+ return anchor.Shift == 0 && blankKeep.has(line) ? line : "";
615
+ const [ws, content] = _Indenter.SplitIndent(line);
616
+ const wsWidth = _Indenter.VisualWidth(ws, tabSize);
617
+ const target = Math.max(0, wsWidth + anchor.Shift);
618
+ if (wsWidth == target && (ws.indexOf(" ") >= 0 == tabStyle || reindented.has(content)))
619
+ return ws + content;
620
+ return _Indenter.BuildIndent(anchor.BaseIndent, target, tabSize) + content;
621
+ }
622
+ static LeadingWs(line) {
623
+ return _Indenter.SplitIndent(line)[0];
624
+ }
625
+ static IsBlank(s) {
626
+ return s == null || s.length == 0 || Array.from(s).every((c) => c === " " || c === " ");
627
+ }
628
+ static SplitIndent(line) {
629
+ if (line == null || line.length == 0)
630
+ return ["", ""];
631
+ const i = TextUtils.CountLeadingWhitespace(line);
632
+ return [line.substring(0, i), line.substring(i)];
633
+ }
634
+ static InferTabSize(lines) {
635
+ for (const ws of Array.from(lines).map(_Indenter.LeadingWs)) {
636
+ const lastTab = ws.lastIndexOf(" ");
637
+ if (lastTab >= 0) {
638
+ const spaces = ws.length - lastTab - 1;
639
+ if (spaces > 0)
640
+ return spaces;
641
+ }
642
+ }
643
+ return _Indenter.DefaultTabSize;
644
+ }
645
+ static VisualWidth(indent, tabSize) {
646
+ return Array.from(indent).map((c) => c == " " ? tabSize : 1).reduce((a, b) => a + b, 0);
647
+ }
648
+ static BuildIndent(baseIndent, targetW, tabSize) {
649
+ if (targetW <= 0)
650
+ return "";
651
+ const baseW = _Indenter.VisualWidth(baseIndent, tabSize);
652
+ const delta = targetW - baseW;
653
+ if (delta == 0)
654
+ return baseIndent;
655
+ if (delta > 0)
656
+ return baseIndent + " ".repeat(delta);
657
+ return baseIndent.indexOf(" ") >= 0 ? " ".repeat(Math.floor(targetW / tabSize)) + " ".repeat(targetW % tabSize) : " ".repeat(targetW);
658
+ }
659
+ };
660
+
661
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/applier/chunkApplier.js
662
+ var InputChunks = class {
663
+ InputFullText;
664
+ Chunks;
665
+ Options;
666
+ constructor(InputFullText, Chunks, Options) {
667
+ this.InputFullText = InputFullText;
668
+ this.Chunks = Chunks;
669
+ this.Options = Options;
670
+ }
671
+ };
672
+ var OutputEdits = class {
673
+ OutputText;
674
+ Edits;
675
+ Errors;
676
+ constructor(OutputText, Edits, Errors) {
677
+ this.OutputText = OutputText;
678
+ this.Edits = Edits;
679
+ this.Errors = Errors;
680
+ }
681
+ };
682
+ var ChunkApplier = class _ChunkApplier {
683
+ // NOTE: on partial failure, Apply is all-or-nothing — it returns the ORIGINAL
684
+ // text untouched alongside the errors, never a best-effort partial application.
685
+ static Apply(targetText, chunks, options) {
686
+ const inputChunks = new InputChunks(targetText, chunks, options);
687
+ if (!options.ContinueOnError)
688
+ return _ChunkApplier.ApplyChunks(inputChunks);
689
+ const errors = [];
690
+ for (let attempt = 0; attempt < options.MaxErrorIterations; attempt++) {
691
+ try {
692
+ const r = _ChunkApplier.ApplyChunks(inputChunks);
693
+ if (errors.length)
694
+ return new OutputEdits(targetText, [], errors);
695
+ return r;
696
+ } catch (ex) {
697
+ if (ex instanceof PatchException) {
698
+ errors.push(ex.Error);
699
+ const idx = chunks.indexOf(ex.Error.FailedMatch);
700
+ if (idx >= 0)
701
+ chunks.splice(idx, 1);
702
+ } else
703
+ throw ex;
704
+ }
705
+ }
706
+ return new OutputEdits(targetText, [], errors);
707
+ }
708
+ static ApplyChunks(args) {
709
+ args = DuplicateGuard.Dedupe(args);
710
+ OverlapGuard.Guard(args);
711
+ const lines = TextUtils.ToLines(args.InputFullText);
712
+ const edits = args.Chunks.map((chunk) => _ChunkApplier.CreateEdit(chunk, lines));
713
+ const workingLines = Array.from(lines);
714
+ for (const edit of ArrayUtils.OrderByDescending(edits, (e) => e.LineIndex))
715
+ edit.ApplyTo(workingLines);
716
+ const patched = TextUtils.RoundTripWhitespace(args.InputFullText, workingLines);
717
+ const minimalEdits = edits.flatMap((e) => Array.from(EditMinimizer.ToMinimalEdits(e)));
718
+ return new OutputEdits(patched, minimalEdits, []);
719
+ }
720
+ static CreateEdit(chunk, lines) {
721
+ const match = chunk.Match;
722
+ if (match.IsAmbiguous)
723
+ throw new PatchException(new PatchError("MatchAmbiguous", chunk));
724
+ if (match.IsNotFound)
725
+ throw new PatchException(new PatchError("MatchNotFound", chunk));
726
+ return new Edit(match.LineIndex, Array.from(chunk.DeleteLines), Indenter.AlignInsert(lines, chunk), match.Fuzz);
727
+ }
728
+ };
729
+
730
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/heuristics.js
731
+ var DiffContentHeuristics = class _DiffContentHeuristics {
732
+ /**
733
+ * Checks if a line starts with a doubled marker (++ or --)
734
+ */
735
+ static IsDoubleMarker(line) {
736
+ return line.length > 1 && line[0] == line[1] && (line[0] == "-" || line[0] == "+");
737
+ }
738
+ /**
739
+ * Determines if a doubled marker represents content rather than diff syntax.
740
+ *
741
+ * Examples:
742
+ * - `--bg-color` → true (CSS variable, content)
743
+ * - `--` → true (exactly two hyphens, content)
744
+ * - `-- text` → false (deletion of markdown list `- text`, diff syntax)
745
+ * - `---comment` → false (deletion of `--comment`, diff syntax)
746
+ * - `----` → false (deletion of `--`, diff syntax)
747
+ * - `++var` → true (C++ code, content)
748
+ * - `++ text` → false (insertion of `+ text`, diff syntax)
749
+ * - `+++` → false (insertion of `+`, diff syntax)
750
+ */
751
+ static IsDoubleMarkerContent(line) {
752
+ if (!_DiffContentHeuristics.IsDoubleMarker(line))
753
+ return false;
754
+ if (line.length == 2)
755
+ return true;
756
+ return line[2] != line[0] && line[2] != " ";
757
+ }
758
+ /**
759
+ * A '++X' line that is a candidate for the sloppy doubled-insert-marker
760
+ * collapse ('++X' → insert 'X'). Tripled markers are proper syntax
761
+ * ('+++X' = insert '++X') and never candidates.
762
+ */
763
+ static IsSloppyDoublePlus(line) {
764
+ return line.length > 1 && line[0] == "+" && line[1] == "+" && (line.length == 2 || line[2] != "+");
765
+ }
766
+ /**
767
+ * True when the hunk body proves the edit region is itself diff-shaped:
768
+ * a context or deletion line whose CONTENT starts with '+' or '-' exists
769
+ * verbatim in the file. In such a region, doubled markers are diff syntax
770
+ * over diff-shaped content, not sloppy doubling.
771
+ */
772
+ static IsDiffShapedRegion(rawLines, fileLines) {
773
+ if (!fileLines || fileLines.length == 0)
774
+ return false;
775
+ for (const l of rawLines) {
776
+ if (l.length < 2 || l[0] != " " && l[0] != "-")
777
+ continue;
778
+ if (l[1] != "+" && l[1] != "-")
779
+ continue;
780
+ const content = l.substring(1);
781
+ if (fileLines.some((f) => f == content))
782
+ return true;
783
+ }
784
+ return false;
785
+ }
786
+ /**
787
+ * Validates whether a line (after stripping indentation) represents valid diff syntax.
788
+ * Used by StripIndent to determine if indentation should be removed.
789
+ *
790
+ * Returns false for content that looks like diff markers:
791
+ * - `--text` (CSS variables)
792
+ * - `-abc-*` (hyphenated identifiers like -webkit-*)
793
+ */
794
+ static IsValidDiffLineStart(line) {
795
+ if (line.length == 0)
796
+ return false;
797
+ const first = line[0];
798
+ if (first == " " || first == "+")
799
+ return true;
800
+ if (first == "-") {
801
+ if (line.length == 1)
802
+ return true;
803
+ if (line[1] == "-")
804
+ return line.length > 2 && line[2] == "-";
805
+ if (_DiffContentHeuristics.IsHyphenatedIdentifier(line))
806
+ return false;
807
+ return true;
808
+ }
809
+ return false;
810
+ }
811
+ /**
812
+ * Pattern: -[a-z]+- (e.g., -webkit-*, -moz-*)
813
+ * If hyphen is followed by letters then another hyphen, it's structural content.
814
+ */
815
+ static IsHyphenatedIdentifier(line) {
816
+ if (line.length < 3 || line[0] != "-")
817
+ return false;
818
+ let i = 1;
819
+ while (i < line.length && line[i] >= "a" && line[i] <= "z")
820
+ i++;
821
+ return i > 1 && i < line.length && line[i] == "-";
822
+ }
823
+ /**
824
+ * Applies heuristics to classify raw lines (lines without explicit diff markers).
825
+ *
826
+ * Heuristic: Raw lines are treated as context until we've seen explicit context markers.
827
+ * Once explicit context has been seen, a raw line followed by an insertion before a
828
+ * context/deletion boundary is treated as a deletion.
829
+ *
830
+ * This helps handle sloppy diffs where deletion markers are omitted but can be inferred.
831
+ */
832
+ static ApplyRawLineHeuristic(raw, rawLines, index, seenExplicitContext, fileLines) {
833
+ const insAhead = _DiffContentHeuristics.HasInsertionAheadBeforeContextOrDeletion(rawLines, index);
834
+ if (insAhead && seenExplicitContext) {
835
+ return new DiffLine(LineType.Delete, _DiffContentHeuristics.StripInlineComment(raw, fileLines));
836
+ }
837
+ if (insAhead) {
838
+ const verified = _DiffContentHeuristics.TryVerifiedCommentStrip(raw, fileLines);
839
+ if (verified !== null)
840
+ return new DiffLine(LineType.Delete, verified);
841
+ }
842
+ return new DiffLine(LineType.Context, raw);
843
+ }
844
+ static InlineCommentMarkers = [" //", " --", " #"];
845
+ /**
846
+ * Returns the comment-stripped form of a line when file content verifies it:
847
+ * the full line must NOT exist in the file and the stripped variant MUST — so
848
+ * real content containing " --" (SQL) or " #" (Python) is never mangled.
849
+ * Returns null when no verified strip exists.
850
+ */
851
+ static TryVerifiedCommentStrip(input, fileLines) {
852
+ if (!fileLines || fileLines.length == 0)
853
+ return null;
854
+ if (fileLines.some((l) => l === input))
855
+ return null;
856
+ for (const marker of _DiffContentHeuristics.InlineCommentMarkers) {
857
+ for (let idx = input.indexOf(marker); idx >= 0; idx = input.indexOf(marker, idx + 1)) {
858
+ const candidate = input.substring(0, idx).trimEnd();
859
+ if (fileLines.some((l) => l === candidate))
860
+ return candidate;
861
+ }
862
+ }
863
+ return null;
864
+ }
865
+ /**
866
+ * Strips an inline comment the LLM appended to an inferred deletion line
867
+ * (e.g., "Line2 // Missing - marker" → "Line2").
868
+ * Prefers a content-verified strip; without file content (or when
869
+ * verification is inconclusive), falls back to stripping C-style " //" only.
870
+ */
871
+ static StripInlineComment(input, fileLines) {
872
+ if (fileLines?.some((l) => l === input))
873
+ return input;
874
+ const verified = _DiffContentHeuristics.TryVerifiedCommentStrip(input, fileLines);
875
+ if (verified !== null)
876
+ return verified;
877
+ const idx = input.indexOf(" //");
878
+ return idx >= 0 ? input.substring(0, idx) : input;
879
+ }
880
+ /**
881
+ * Checks if there's an insertion (line starting with '+') ahead of the current position,
882
+ * before encountering a context line, deletion line, or blank line (boundaries).
883
+ *
884
+ * Used to infer whether a raw line should be treated as a deletion.
885
+ */
886
+ static HasInsertionAheadBeforeContextOrDeletion(rawLines, currentIndex) {
887
+ for (let i = currentIndex + 1; i < rawLines.length; i++) {
888
+ const l = rawLines[i];
889
+ if (l.length == 0)
890
+ break;
891
+ const c = l[0];
892
+ if (c == " ")
893
+ break;
894
+ if (c == "-" && !_DiffContentHeuristics.IsDoubleMarkerContent(l))
895
+ break;
896
+ if (c == "+")
897
+ return true;
898
+ }
899
+ return false;
900
+ }
901
+ /**
902
+ * Checks if a line starting with "-" or "+" is likely content with a missing context prefix.
903
+ * Uses file content to disambiguate: if file has the line but NOT the would-be-modified content.
904
+ *
905
+ * Examples:
906
+ * - "- list item" might be markdown bullet, not deletion of " list item"
907
+ * - "+ Add feature" might be changelog entry, not insertion of " Add feature"
908
+ *
909
+ * @returns true if line should be treated as context, false if it should be treated as diff operation
910
+ */
911
+ static IsMissingContextPrefix(line, fileLines) {
912
+ if (line.length < 2)
913
+ return false;
914
+ const marker = line[0];
915
+ if (marker !== "-" && marker !== "+")
916
+ return false;
917
+ if (marker === "+" && line[1] !== " ")
918
+ return false;
919
+ const hasContentMatch = fileLines.some((l) => l === line);
920
+ const hasOperationMatch = fileLines.some((l) => l === line.substring(1));
921
+ return hasContentMatch && !hasOperationMatch;
922
+ }
923
+ /**
924
+ * Resolves ambiguity when stripping indentation reveals "- " (hyphen + space).
925
+ *
926
+ * This pattern is ambiguous:
927
+ * - Could be an indented DELETE line (deleting content starting with a space)
928
+ * - Could be a properly formatted CONTEXT line containing a markdown bullet
929
+ *
930
+ * Example: " - Label them..." could mean:
931
+ * - DELETE: Remove line " Label them..." (space + content) from file
932
+ * - CONTEXT: Keep line " - Label them..." (3 spaces + bullet) in file
933
+ *
934
+ * Resolution: Check which interpretation matches the actual file content.
935
+ * If only one matches, use that. If both or neither, prefer CONTEXT (conservative).
936
+ *
937
+ * @param original - The original line before stripping (e.g., " - Label them...")
938
+ * @param stripped - The line after stripping indentation (e.g., "- Label them...")
939
+ * @param fileLines - Lines from the actual file being patched
940
+ * @returns The line to use (original if CONTEXT, stripped if DELETE)
941
+ */
942
+ static ResolveHyphenSpaceAmbiguity(original, stripped, fileLines) {
943
+ const contextContent = original.length > 0 && original[0] === " " ? original.substring(1) : original;
944
+ const deleteContent = stripped.substring(1);
945
+ const hasContextMatch = fileLines.some((line) => line === contextContent);
946
+ const hasDeleteMatch = fileLines.some((line) => line === deleteContent);
947
+ if (hasContextMatch && !hasDeleteMatch)
948
+ return original;
949
+ if (hasDeleteMatch && !hasContextMatch)
950
+ return stripped;
951
+ return original;
952
+ }
953
+ };
954
+
955
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/hunkBuilder.js
956
+ var Header = class _Header {
957
+ Path;
958
+ OldStart;
959
+ OldCount;
960
+ NewStart;
961
+ NewCount;
962
+ DiffBodyStartLine;
963
+ constructor(Path = null, OldStart = -1, OldCount = 0, NewStart = -1, NewCount = 0, DiffBodyStartLine = -1) {
964
+ this.Path = Path;
965
+ this.OldStart = OldStart;
966
+ this.OldCount = OldCount;
967
+ this.NewStart = NewStart;
968
+ this.NewCount = NewCount;
969
+ this.DiffBodyStartLine = DiffBodyStartLine;
970
+ }
971
+ static WithParsed(h, parsedHdr, diffBodyStartLine) {
972
+ return new _Header(h.Path, parsedHdr.OldStart, parsedHdr.OldCount, parsedHdr.NewStart, parsedHdr.NewCount, diffBodyStartLine);
973
+ }
974
+ static ParseSide(header, part, prefix, isOld) {
975
+ if (!part.startsWith(prefix))
976
+ return header;
977
+ const split = part.substring(1).split(",");
978
+ const start = split.length > 0 && !isNaN(parseInt(split[0])) ? parseInt(split[0]) : -1;
979
+ const count = split.length > 1 && !isNaN(parseInt(split[1])) ? parseInt(split[1]) : 1;
980
+ return isOld ? new _Header(header.Path, start, count, header.NewStart, header.NewCount, header.DiffBodyStartLine) : new _Header(header.Path, header.OldStart, header.OldCount, start, count, header.DiffBodyStartLine);
981
+ }
982
+ };
983
+ var HunkBuilder = class _HunkBuilder {
984
+ hunks = [];
985
+ bodySeen = /* @__PURE__ */ new Map();
986
+ get Hunks() {
987
+ return this.hunks;
988
+ }
989
+ // Only the diff's FINAL hunk can be torn by a cutoff, so the parser reads this
990
+ // once, after the last commit; mid-diff the same signature is just sloppy counting.
991
+ LastHunkTruncated = false;
992
+ // A closing ``` fence is completion evidence a token cutoff cannot emit — the
993
+ // tear signature on a fence-closed hunk is miscount slop, not truncation.
994
+ // (Trailing prose already clears the flag via the pure-context commit path.)
995
+ ClearTruncation() {
996
+ this.LastHunkTruncated = false;
997
+ }
998
+ CommitIfAny(hdr, body, fileLines) {
999
+ if (body.length == 0)
1000
+ return;
1001
+ const blankContextDeletion = _HunkBuilder.IsBlankContextDeletion(hdr.OldCount, hdr.NewCount, body);
1002
+ if (!blankContextDeletion && !body.some((l) => l.length > 0 && (l[0] == "-" || l[0] == "+"))) {
1003
+ this.LastHunkTruncated = false;
1004
+ body.length = 0;
1005
+ return;
1006
+ }
1007
+ const diffLines = _HunkBuilder.BuildLines(body, blankContextDeletion, fileLines);
1008
+ this.LastHunkTruncated = _HunkBuilder.IsTruncated(hdr, diffLines);
1009
+ const oldSb = [];
1010
+ const newSb = [];
1011
+ let oldLines = 0, newLines = 0;
1012
+ for (const dl of diffLines) {
1013
+ switch (dl.Type) {
1014
+ case LineType.Insert:
1015
+ newSb.push(dl.Text, "\n");
1016
+ newLines++;
1017
+ break;
1018
+ case LineType.Delete:
1019
+ oldSb.push(dl.Text, "\n");
1020
+ oldLines++;
1021
+ break;
1022
+ case LineType.Context:
1023
+ oldSb.push(dl.Text, "\n");
1024
+ oldLines++;
1025
+ newSb.push(dl.Text, "\n");
1026
+ newLines++;
1027
+ break;
1028
+ }
1029
+ }
1030
+ const oldTxt = oldLines > 0 ? oldSb.join("") : "";
1031
+ const newTxt = newLines > 0 ? newSb.join("") : "";
1032
+ if (oldTxt == newTxt) {
1033
+ body.length = 0;
1034
+ return;
1035
+ }
1036
+ const hunk = new Hunk(hdr.Path ?? "", oldTxt, newTxt, hdr.OldStart);
1037
+ hunk.NewStart = hdr.NewStart;
1038
+ hunk.Lines = diffLines;
1039
+ hunk.DiffBodyStartLine = hdr.DiffBodyStartLine;
1040
+ this.AddHunk(hunk);
1041
+ body.length = 0;
1042
+ }
1043
+ // Token-limit cutoff: body ends mid-change-run and the header declares more lines
1044
+ // than delivered, asymmetrically. Symmetric deficits are stripped context (the
1045
+ // anchorer recovers) and never count. An insert-ending body is torn only when the
1046
+ // old side is ALSO short (promised trailing context never arrived): an old side
1047
+ // fully delivered is complete by its own header, and a new-side deficit alone is
1048
+ // overcount slop that must still apply — a complete append with a miscounted
1049
+ // header is indistinguishable from a mid-run cut, and counts are tie-breakers,
1050
+ // never sole grounds for rejection. A delete-ending body is the mirror: torn only
1051
+ // when the NEW side is also short (newDeficit > 0) AND the old side is shorter
1052
+ // still (the delete run was cut), so an over-counted-but-complete delete hunk
1053
+ // (newDeficit == 0) applies as overcount slop. Cost (both branches): a cut with no
1054
+ // trailing context owed on the completed side goes undetected.
1055
+ static IsTruncated(hdr, lines) {
1056
+ if (hdr.OldStart < 0 || hdr.NewStart < 0 || lines.length == 0)
1057
+ return false;
1058
+ const last = lines[lines.length - 1].Type;
1059
+ if (last == LineType.Context)
1060
+ return false;
1061
+ const actualOld = lines.filter((l) => l.Type != LineType.Insert).length;
1062
+ const actualNew = lines.filter((l) => l.Type != LineType.Delete).length;
1063
+ const oldDeficit = hdr.OldCount - actualOld, newDeficit = hdr.NewCount - actualNew;
1064
+ return last == LineType.Insert ? oldDeficit > 0 && newDeficit > oldDeficit : newDeficit > 0 && oldDeficit > newDeficit;
1065
+ }
1066
+ // Identical bodies collapse when the repeat adds no placement info (bare @@ or a
1067
+ // start line already seen) — LLM-slop duplication. But git legitimately emits
1068
+ // byte-identical hunks at distinct line numbers when a file repeats a pattern;
1069
+ // those are separate edits, and the anchorer's line-number tiebreak places each
1070
+ // at its own site. Distinct bodies always survive.
1071
+ AddHunk(h) {
1072
+ const bodyKey = `${h.OldText}
1073
+ \u241E
1074
+ ${h.NewText}`;
1075
+ const starts = this.bodySeen.get(bodyKey);
1076
+ if (starts) {
1077
+ if (h.OldStart < 0 || starts.has(-1) || starts.has(h.OldStart))
1078
+ return;
1079
+ starts.add(h.OldStart);
1080
+ } else {
1081
+ this.bodySeen.set(bodyKey, /* @__PURE__ */ new Set([h.OldStart]));
1082
+ }
1083
+ this.hunks.push(h);
1084
+ }
1085
+ static BuildLines(rawLines, blankContextDeletion, fileLines) {
1086
+ const result = [];
1087
+ let seenExplicitContext = false;
1088
+ const diffShaped = DiffContentHeuristics.IsDiffShapedRegion(rawLines, fileLines);
1089
+ for (let i = 0; i < rawLines.length; i++) {
1090
+ const raw = rawLines[i];
1091
+ if (raw.length == 0) {
1092
+ if (i == rawLines.length - 1)
1093
+ continue;
1094
+ result.push(new DiffLine(blankContextDeletion ? LineType.Delete : LineType.Context, ""));
1095
+ continue;
1096
+ }
1097
+ if (blankContextDeletion) {
1098
+ const c = raw[0];
1099
+ result.push(new DiffLine(LineType.Delete, c == " " || c == "-" ? _HunkBuilder.RemoveDiffPrefix(raw) : raw));
1100
+ continue;
1101
+ }
1102
+ switch (raw[0]) {
1103
+ case "+":
1104
+ case "-":
1105
+ if (fileLines && DiffContentHeuristics.IsMissingContextPrefix(raw, fileLines)) {
1106
+ result.push(new DiffLine(LineType.Context, raw));
1107
+ seenExplicitContext = true;
1108
+ break;
1109
+ }
1110
+ if (raw[0] === "+") {
1111
+ if (DiffContentHeuristics.IsSloppyDoublePlus(raw)) {
1112
+ result.push(diffShaped ? new DiffLine(LineType.Insert, raw.substring(1)) : new DiffLine(LineType.Insert, _HunkBuilder.RemoveDiffPrefix(raw), raw));
1113
+ break;
1114
+ }
1115
+ result.push(new DiffLine(LineType.Insert, _HunkBuilder.RemoveDiffPrefix(raw)));
1116
+ break;
1117
+ }
1118
+ if (DiffContentHeuristics.IsDoubleMarkerContent(raw)) {
1119
+ if (fileLines && fileLines.some((l) => l == raw.substring(1)) && !fileLines.some((l) => l == raw)) {
1120
+ result.push(new DiffLine(LineType.Delete, raw.substring(1)));
1121
+ break;
1122
+ }
1123
+ if (seenExplicitContext && raw.length > 2 && raw[2] === " ") {
1124
+ result.push(new DiffLine(LineType.Delete, raw.substring(1)));
1125
+ } else {
1126
+ result.push(DiffContentHeuristics.ApplyRawLineHeuristic(raw, rawLines, i, seenExplicitContext, fileLines));
1127
+ }
1128
+ } else {
1129
+ result.push(new DiffLine(LineType.Delete, _HunkBuilder.RemoveDiffPrefix(raw)));
1130
+ }
1131
+ break;
1132
+ case " ":
1133
+ result.push(new DiffLine(LineType.Context, _HunkBuilder.RemoveDiffPrefix(raw)));
1134
+ seenExplicitContext = true;
1135
+ break;
1136
+ default:
1137
+ result.push(DiffContentHeuristics.ApplyRawLineHeuristic(raw, rawLines, i, seenExplicitContext, fileLines));
1138
+ break;
1139
+ }
1140
+ }
1141
+ return result;
1142
+ }
1143
+ static IsBlankContextDeletion(oldCount, newCount, rawLines) {
1144
+ return oldCount > 0 && newCount == 0 && rawLines.every((l) => l.length == 0 || l[0] == " ");
1145
+ }
1146
+ // Strip diff prefix: ` foo` → `foo`, `-bar` → `bar`, `+baz` → `baz`
1147
+ // Sloppy doubled markers: `++code` → `code`, `++ code` → `code`
1148
+ // But NOT tripled: `---comment` → `--comment` (proper syntax for deleting `--comment`)
1149
+ // And NOT `-- text` which is deletion of `- text` (markdown list), not sloppy
1150
+ static RemoveDiffPrefix(s) {
1151
+ if (!s || s.length <= 1)
1152
+ return "";
1153
+ let result = s.substring(1);
1154
+ if (DiffContentHeuristics.IsSloppyDoublePlus(s)) {
1155
+ result = result.substring(1);
1156
+ if (result[0] === " " || result[0] === " ")
1157
+ result = result.substring(1);
1158
+ }
1159
+ return result;
1160
+ }
1161
+ };
1162
+
1163
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/unifiedDiffParser.js
1164
+ var UnifiedDiffParser = class _UnifiedDiffParser {
1165
+ /**
1166
+ * Parse a unified diff into structured hunks.
1167
+ * @param diff - The diff text to parse
1168
+ * @param files - Map of filename to content (content used for disambiguation of ambiguous lines)
1169
+ * @param truncation - Verdict for a surviving tear signature on the final hunk
1170
+ * @param controlChars - Verdict for raw control characters in insert lines
1171
+ */
1172
+ static Parse(diff, files, truncation = "warn", controlChars = "error") {
1173
+ const hunks = diff == null || diff.trim().length == 0 ? [] : _UnifiedDiffParser.ParseHunks(diff, files, truncation, controlChars);
1174
+ const fileKeySet = new Set(files.keys());
1175
+ const headerless = hunks.filter((h) => h.Key == null || h.Key == "");
1176
+ const explicitKnown = hunks.filter((h) => h.Key != null && h.Key != "" && fileKeySet.has(h.Key));
1177
+ const explicitFallback = hunks.filter((h) => h.Key != null && h.Key != "" && !fileKeySet.has(h.Key));
1178
+ const groups = [];
1179
+ if (headerless.length > 0)
1180
+ groups.push(new FileHunkGroup("", headerless));
1181
+ if (explicitKnown.length > 0)
1182
+ ArrayUtils.GroupBy(explicitKnown, (h) => h.Key).map((g) => new FileHunkGroup(g.key, g.values)).forEach((x) => groups.push(x));
1183
+ const treatAsSingleFile = fileKeySet.size == 1 && fileKeySet.has("") && explicitKnown.length == 0 && headerless.length == 0 && Array.from(new Set(explicitFallback.map((h) => h.Key))).length == 1;
1184
+ if (treatAsSingleFile && explicitFallback.length > 0)
1185
+ groups.push(new FileHunkGroup("", explicitFallback));
1186
+ else if (explicitFallback.length > 0)
1187
+ ArrayUtils.GroupBy(explicitFallback, (h) => h.Key).map((g) => new FileHunkGroup(g.key, g.values)).forEach((x) => groups.push(x));
1188
+ return groups;
1189
+ }
1190
+ static ParseHunks(diff, fileContents, truncation = "warn", controlChars = "error") {
1191
+ const hunkBuilder = new HunkBuilder();
1192
+ const body = [];
1193
+ let header = new Header();
1194
+ let inFence = false;
1195
+ let inHunkBody = false;
1196
+ let lineNo = -1;
1197
+ const defaultFileLines = fileContents?.size == 1 && fileContents.has("") ? _UnifiedDiffParser.ToFileLines(fileContents.get("")) : void 0;
1198
+ let currentFileLines = defaultFileLines;
1199
+ let fileLineSet = _UnifiedDiffParser.ToLineSet(currentFileLines);
1200
+ for (const raw of _UnifiedDiffParser.StripCommonIndent(diff.replace(/\r\n/g, "\n")).split("\n")) {
1201
+ lineNo++;
1202
+ if (raw.length > 1 && raw[0] == " " && fileLineSet != null && fileLineSet.has(raw.slice(1).replace(/\s+$/, ""))) {
1203
+ body.push(raw);
1204
+ continue;
1205
+ }
1206
+ const line = _UnifiedDiffParser.StripIndent(raw, currentFileLines);
1207
+ if (line.startsWith("```diff")) {
1208
+ inFence = true;
1209
+ continue;
1210
+ }
1211
+ if (!inFence && line.startsWith("```")) {
1212
+ continue;
1213
+ }
1214
+ if (inFence && line.startsWith("```")) {
1215
+ hunkBuilder.CommitIfAny(header, body, currentFileLines);
1216
+ hunkBuilder.ClearTruncation();
1217
+ inFence = false;
1218
+ inHunkBody = false;
1219
+ continue;
1220
+ }
1221
+ if (_UnifiedDiffParser.IsMeta(line))
1222
+ continue;
1223
+ const path = _UnifiedDiffParser.TryParseFileHeader(line);
1224
+ if (path && _UnifiedDiffParser.IsFileHeader(line, path, inHunkBody, fileContents, fileLineSet, body, currentFileLines)) {
1225
+ hunkBuilder.CommitIfAny(header, body, currentFileLines);
1226
+ header = new Header(path);
1227
+ currentFileLines = _UnifiedDiffParser.ToFileLines(fileContents?.get(path)) ?? defaultFileLines;
1228
+ fileLineSet = _UnifiedDiffParser.ToLineSet(currentFileLines);
1229
+ inHunkBody = false;
1230
+ continue;
1231
+ }
1232
+ const parsedHdr = _UnifiedDiffParser.TryParseHunkHeader(line);
1233
+ if (parsedHdr) {
1234
+ hunkBuilder.CommitIfAny(header, body, currentFileLines);
1235
+ header = Header.WithParsed(header, parsedHdr, lineNo + 1);
1236
+ inHunkBody = true;
1237
+ continue;
1238
+ }
1239
+ body.push(line);
1240
+ }
1241
+ hunkBuilder.CommitIfAny(header, body, currentFileLines);
1242
+ const hunks = hunkBuilder.Hunks;
1243
+ if (hunkBuilder.LastHunkTruncated && truncation != "ignore") {
1244
+ if (truncation == "error")
1245
+ throw new PatchParserException("The diff appears truncated mid-hunk: the final hunk's header declares more lines than its body delivers. Nothing was applied; re-send the complete diff.");
1246
+ if (hunks.length > 0)
1247
+ hunks[hunks.length - 1].TruncationSuspected = true;
1248
+ }
1249
+ if (controlChars != "ignore") {
1250
+ for (const hunk of hunks) {
1251
+ const suspect = _UnifiedDiffParser.DescribeControlChars(hunk.Lines.filter((l) => l.Type == LineType.Insert).map((l) => l.Text));
1252
+ if (suspect == null)
1253
+ continue;
1254
+ if (controlChars == "error")
1255
+ throw new PatchParserException(`Insert lines contain raw control characters (${suspect}) \u2014 almost certainly transport damage, not intended content. Nothing was applied; re-send the diff with the intended text (or opt into the 'warn'/'ignore' policy).`);
1256
+ hunk.ControlCharsSuspected = true;
1257
+ }
1258
+ }
1259
+ return hunks;
1260
+ }
1261
+ static IsSuspectControlChar(c) {
1262
+ const cp = c.charCodeAt(0);
1263
+ return cp < 32 && c != " " && c != "\n" && c != "\r" && c != "\f" || cp == 127;
1264
+ }
1265
+ // "U+0000 x2, U+001B x1" — names the invisible damage so the caller can see it.
1266
+ static DescribeControlChars(lines) {
1267
+ const counts = /* @__PURE__ */ new Map();
1268
+ for (const line of lines)
1269
+ for (const c of line)
1270
+ if (_UnifiedDiffParser.IsSuspectControlChar(c))
1271
+ counts.set(c, (counts.get(c) ?? 0) + 1);
1272
+ if (counts.size == 0)
1273
+ return null;
1274
+ return [...counts.entries()].sort((a, b) => a[0].charCodeAt(0) - b[0].charCodeAt(0)).map(([c, n]) => `U+${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")} x${n}`).join(", ");
1275
+ }
1276
+ static MetaPrefixes = [
1277
+ "index ",
1278
+ "new file mode ",
1279
+ "deleted file mode ",
1280
+ "similarity index ",
1281
+ "rename from ",
1282
+ "rename to ",
1283
+ "Binary files ",
1284
+ "GIT binary patch",
1285
+ "mode change "
1286
+ ];
1287
+ static IsMeta(line) {
1288
+ return _UnifiedDiffParser.MetaPrefixes.some((p) => line.startsWith(p));
1289
+ }
1290
+ static ToFileLines(content) {
1291
+ return content ? content.replace(/\r\n/g, "\n").split("\n") : void 0;
1292
+ }
1293
+ static ToLineSet(lines) {
1294
+ return lines ? new Set(lines.map((l) => l.replace(/\s+$/, ""))) : void 0;
1295
+ }
1296
+ // Strips the longest common whitespace prefix from all non-empty diff body lines.
1297
+ // This normalizes diffs that are uniformly indented (e.g. pasted inside a markdown block
1298
+ // or another indented structure), while preserving the single-space context markers on
1299
+ // lines whose content happens to start with '+' or '-'.
1300
+ //
1301
+ // @@ hunk headers are excluded from the prefix calculation because an LLM may indent
1302
+ // body lines but not the header (or vice versa). The strip is applied only to lines
1303
+ // that actually start with the computed prefix, so unindented headers are left intact.
1304
+ //
1305
+ // Runs before the per-line StripIndent pass: this handles uniform indentation
1306
+ // (including a single-space indent that per-line stripping must not touch), while
1307
+ // StripIndent handles ragged 2+ whitespace indentation line by line.
1308
+ static StripCommonIndent(diff) {
1309
+ const lines = diff.split("\n");
1310
+ let common = null;
1311
+ for (const line of lines) {
1312
+ if (line.length == 0)
1313
+ continue;
1314
+ const i = TextUtils.CountLeadingWhitespace(line);
1315
+ if (line.startsWith("@@", i))
1316
+ continue;
1317
+ const prefix = line.substring(0, i);
1318
+ if (common == null) {
1319
+ common = prefix;
1320
+ continue;
1321
+ }
1322
+ const minLen = Math.min(common.length, prefix.length);
1323
+ let j = 0;
1324
+ while (j < minLen && common[j] == prefix[j])
1325
+ j++;
1326
+ common = common.substring(0, j);
1327
+ if (common.length == 0)
1328
+ return diff;
1329
+ }
1330
+ if (common == null || common.length == 0)
1331
+ return diff;
1332
+ const c = common;
1333
+ if (!lines.some((l) => l.startsWith(c) && l.length > c.length && (l[c.length] == "+" || l[c.length] == "-")))
1334
+ return diff;
1335
+ return lines.map((l) => l.startsWith(c) ? l.substring(c.length) : l).join("\n");
1336
+ }
1337
+ static StripIndent(s, fileLines) {
1338
+ if (s == null || s.length == 0)
1339
+ return s;
1340
+ const stripped = s.replace(/^[ \t]{2,}/, "");
1341
+ if (fileLines && stripped.length > 1 && stripped[0] === "-" && stripped[1] === " ") {
1342
+ return DiffContentHeuristics.ResolveHyphenSpaceAmbiguity(s, stripped, fileLines);
1343
+ }
1344
+ if (fileLines && stripped.length > 0 && stripped[0] === "+" && fileLines.some((f) => f.trim() == stripped.trimEnd()))
1345
+ return s;
1346
+ return DiffContentHeuristics.IsValidDiffLineStart(stripped) ? stripped : s;
1347
+ }
1348
+ static TryParseFileHeader(line) {
1349
+ if (!(line.startsWith("diff --git ") || line.startsWith("--- ") || line.startsWith("+++ "))) {
1350
+ return null;
1351
+ }
1352
+ const path = line.startsWith("diff --git ") ? _UnifiedDiffParser.Canon(line.split(" ").filter((s) => s.length > 0)[2]) : _UnifiedDiffParser.Canon(line.length >= 4 ? line.substring(4) : "");
1353
+ return path;
1354
+ }
1355
+ // Inside a hunk body, `--- foo` could be deletion of `-- foo` (SQL comment,
1356
+ // prose) and `+++ foo` insertion of `++ foo` — or the next file's header.
1357
+ // Evidence ladder: a transition to a file we HOLD
1358
+ // is a real header; otherwise content decides — the delete reading wins when
1359
+ // the current file contains `-- foo` verbatim, the insert reading when the
1360
+ // body so far is a verified diff-shaped region (real header pairs never hit
1361
+ // this: their `+++` follows a header-read `---`, which ends the body). Only
1362
+ // in an evidence vacuum does the structure heuristic decide: real paths have
1363
+ // `.` (extension) or `/` (directory); prose doesn't.
1364
+ static IsFileHeader(line, path, inHunkBody, files, fileLineSet, body, fileLines) {
1365
+ if (!inHunkBody)
1366
+ return true;
1367
+ if (line.startsWith("diff --git "))
1368
+ return true;
1369
+ if (path != "" && files?.has(path))
1370
+ return true;
1371
+ if (line.startsWith("--- ") && fileLineSet != null && fileLineSet.has(line.slice(1).replace(/\s+$/, "")))
1372
+ return false;
1373
+ if (line.startsWith("+++ ") && body != null && DiffContentHeuristics.IsDiffShapedRegion(body, fileLines))
1374
+ return false;
1375
+ return _UnifiedDiffParser.LooksLikePath(path);
1376
+ }
1377
+ static LooksLikePath(path) {
1378
+ return path.includes(".") || path.includes("/") || path.includes("\\") || path === "/dev/null";
1379
+ }
1380
+ static Canon(p) {
1381
+ if (p == null || p.length == 0)
1382
+ return p;
1383
+ p = p.trim();
1384
+ if (p.startsWith("a/") || p.startsWith("b/"))
1385
+ p = p.substring(2);
1386
+ return p;
1387
+ }
1388
+ static TryParseHunkHeader(line) {
1389
+ const t = line.trim();
1390
+ if (!t.startsWith("@@"))
1391
+ return null;
1392
+ let header = new Header();
1393
+ const parts = t.replace(/^[@ ]+|[@ ]+$/g, "").split(" ").filter((s) => s.length > 0);
1394
+ if (parts.length > 0)
1395
+ header = Header.ParseSide(header, parts[0], "-", true);
1396
+ if (parts.length > 1)
1397
+ header = Header.ParseSide(header, parts[1], "+", false);
1398
+ return header;
1399
+ }
1400
+ };
1401
+
1402
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/anchorer/hunkSlicer.js
1403
+ var Block = class {
1404
+ StartIndex;
1405
+ EndIndex;
1406
+ DeleteLines;
1407
+ InsertLines;
1408
+ constructor(StartIndex, EndIndex, DeleteLines, InsertLines) {
1409
+ this.StartIndex = StartIndex;
1410
+ this.EndIndex = EndIndex;
1411
+ this.DeleteLines = DeleteLines;
1412
+ this.InsertLines = InsertLines;
1413
+ }
1414
+ };
1415
+ var SlicedHunk = class {
1416
+ Hunk;
1417
+ Blocks;
1418
+ constructor(Hunk2, Blocks) {
1419
+ this.Hunk = Hunk2;
1420
+ this.Blocks = Blocks;
1421
+ }
1422
+ };
1423
+ var HunkSlicer = class _HunkSlicer {
1424
+ static Slice(hunks) {
1425
+ return Array.from(hunks).map((h) => new SlicedHunk(h, Array.from(_HunkSlicer.SliceHunk(h))));
1426
+ }
1427
+ static *SliceHunk(hunk) {
1428
+ const lines = hunk.Lines;
1429
+ if (lines == null || lines.length == 0) {
1430
+ return;
1431
+ }
1432
+ let i = 0;
1433
+ while (i < lines.length) {
1434
+ if (lines[i].Type == LineType.Context) {
1435
+ i++;
1436
+ continue;
1437
+ }
1438
+ const start = i;
1439
+ const del = [];
1440
+ const ins = [];
1441
+ while (i < lines.length && lines[i].Type != LineType.Context) {
1442
+ const l = lines[i++];
1443
+ if (l.Type == LineType.Delete)
1444
+ del.push(l.Text);
1445
+ else if (l.Type == LineType.Insert)
1446
+ ins.push(l.Text);
1447
+ }
1448
+ const end = i - 1;
1449
+ if (del.length > 0 || ins.length > 0)
1450
+ yield new Block(start, end, del, ins);
1451
+ }
1452
+ }
1453
+ };
1454
+
1455
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/anchorer/search.js
1456
+ var Match = class {
1457
+ LineIndex;
1458
+ Fuzz;
1459
+ constructor(LineIndex, Fuzz) {
1460
+ this.LineIndex = LineIndex;
1461
+ this.Fuzz = Fuzz;
1462
+ }
1463
+ };
1464
+ var Search = class _Search {
1465
+ // Whitespace matching modes with their per-line fuzz
1466
+ static StrictnessPasses = [
1467
+ { Transform: (s) => s, Fuzz: 0 },
1468
+ // exact
1469
+ { Transform: (s) => s.trimEnd(), Fuzz: 1 },
1470
+ // rstrip
1471
+ { Transform: (s) => s.trim(), Fuzz: 100 },
1472
+ // strip
1473
+ { Transform: (s) => TextUtils.NormalizeHomoglyphs(s).trim(), Fuzz: 200 },
1474
+ // homoglyphs + strip
1475
+ { Transform: (s) => TextUtils.NormalizeHomoglyphs(TextUtils.StripInvisibles(s)).trim(), Fuzz: 300 },
1476
+ // invisibles stripped + homoglyphs + strip
1477
+ { Transform: (s) => TextUtils.NormalizeHomoglyphs(TextUtils.FoldCanonicalAndFullwidth(TextUtils.StripInvisibles(s))).trim(), Fuzz: 400 }
1478
+ // + NFC and fullwidth folds
1479
+ ];
1480
+ /// <summary>
1481
+ /// Attempts to locate <paramref name="needle"/> inside <paramref name="haystack"/>,
1482
+ /// applying the strictness passes in order, strictest first.
1483
+ /// Returns a FindResult with Index == -1 if no match.
1484
+ /// Passes whose per-line fuzz exceeds <paramref name="maxPassFuzz"/> are skipped:
1485
+ /// callers that treat a match as evidence the file already contains specific text
1486
+ /// (rather than as an anchor to edit) pass 100 to exclude the homoglyph and
1487
+ /// invisible-stripping passes.
1488
+ /// </summary>
1489
+ static Find(haystack, needle, startIndex = 0, maxPassFuzz = Number.MAX_SAFE_INTEGER) {
1490
+ if (needle.length == 0)
1491
+ return new Match(Math.min(Math.max(startIndex, 0), haystack.length), 0);
1492
+ if (haystack.length < needle.length)
1493
+ return new Match(-1, 0);
1494
+ for (const { Transform: transform, Fuzz: fuzz } of _Search.StrictnessPasses.filter((p) => p.Fuzz <= maxPassFuzz)) {
1495
+ const transformedNeedle = Array.from(needle).map(transform);
1496
+ for (let i = startIndex; i <= haystack.length - needle.length; i++) {
1497
+ let match = true;
1498
+ for (let j = 0; j < needle.length; j++) {
1499
+ if (transform(haystack[i + j]) != transformedNeedle[j]) {
1500
+ match = false;
1501
+ break;
1502
+ }
1503
+ }
1504
+ if (!match)
1505
+ continue;
1506
+ let affected = 0;
1507
+ for (let k = 0; k < needle.length; k++) {
1508
+ if (transform(haystack[i + k]) != haystack[i + k] || transformedNeedle[k] != needle[k])
1509
+ affected++;
1510
+ }
1511
+ return new Match(i, fuzz * affected);
1512
+ }
1513
+ }
1514
+ return new Match(-1, 0);
1515
+ }
1516
+ /// <summary>
1517
+ /// True when two lines are equal under any of the strictness passes the anchor
1518
+ /// search itself uses. Used to revalidate context at a line-number-tiebroken slot:
1519
+ /// the revalidation must not be stricter than the search that produced the
1520
+ /// candidates, or slots the search considered equivalent (indent slop, homoglyphs)
1521
+ /// get rejected loudly instead of applied.
1522
+ /// </summary>
1523
+ static LinesEquivalent(a, b) {
1524
+ return _Search.StrictnessPasses.some((p) => p.Transform(a) == p.Transform(b));
1525
+ }
1526
+ /// <summary>
1527
+ /// Attempts to find a unique match and reports ambiguity without requiring a separate call.
1528
+ /// Success=true when exactly one match; Ambiguous=true when more than one match exists; both false when none.
1529
+ /// </summary>
1530
+ static FindUnique(haystack, needle) {
1531
+ const first = _Search.Find(haystack, needle);
1532
+ if (first.LineIndex < 0)
1533
+ return UniqueMatch.NotFound;
1534
+ const second = _Search.Find(haystack, needle, first.LineIndex + 1);
1535
+ if (second.LineIndex >= 0)
1536
+ return UniqueMatch.Ambiguous;
1537
+ return new UniqueMatch(MatchState.Success, first.LineIndex, first.Fuzz);
1538
+ }
1539
+ /// <summary>
1540
+ /// Finds the best insertion anchor for a pure-insert hunk using provided context lines.
1541
+ /// Attempts, in order: full pattern (pre+post), pre-only (insert after pre), post-only (insert before post).
1542
+ /// If none uniquely match, returns a non-success result with Ambiguous flag when any side matches somewhere.
1543
+ /// </summary>
1544
+ static FindContextAnchor(haystack, contextBefore, contextAfter) {
1545
+ const preCount = contextBefore.length;
1546
+ let sawAmbiguity = false;
1547
+ const searchPatterns = [
1548
+ { pattern: contextBefore.concat(contextAfter), insertIndex: preCount, condition: preCount + contextAfter.length > 0 },
1549
+ { pattern: contextBefore, insertIndex: preCount, condition: preCount > 0 },
1550
+ { pattern: contextAfter, insertIndex: 0, condition: contextAfter.length > 0 }
1551
+ ];
1552
+ for (const { pattern, insertIndex, condition } of searchPatterns) {
1553
+ if (!condition)
1554
+ continue;
1555
+ const match = _Search.FindUnique(haystack, pattern);
1556
+ if (match.IsSuccess)
1557
+ return new UniqueMatch(MatchState.Success, match.LineIndex + insertIndex, match.Fuzz);
1558
+ if (match.IsAmbiguous)
1559
+ sawAmbiguity = true;
1560
+ }
1561
+ return new UniqueMatch(sawAmbiguity ? MatchState.Ambiguous : MatchState.NotFound);
1562
+ }
1563
+ /// <summary>
1564
+ /// Returns every position in <paramref name="haystack"/> where <paramref name="needle"/>
1565
+ /// matches exactly (no whitespace fuzz). Used for line-number tiebreaking among
1566
+ /// ambiguous candidates — we deliberately exclude fuzzy matches so the candidate set
1567
+ /// reflects only positions the LLM could plausibly have meant.
1568
+ /// </summary>
1569
+ static FindAllExact(haystack, needle) {
1570
+ const results = [];
1571
+ if (needle.length == 0)
1572
+ return results;
1573
+ for (let i = 0; i + needle.length <= haystack.length; i++)
1574
+ if (needle.every((v, j) => haystack[i + j] === v))
1575
+ results.push(i);
1576
+ return results;
1577
+ }
1578
+ static TiebreakByLineNumber(candidates, expectedLine) {
1579
+ if (candidates.length == 0)
1580
+ return null;
1581
+ if (candidates.length == 1)
1582
+ return candidates[0];
1583
+ const sorted = ArrayUtils.OrderBy(candidates.map((c) => ({ line: c, dist: Math.abs(c - expectedLine) })), (x) => x.dist);
1584
+ if (sorted[0].dist == 0)
1585
+ return sorted[0].line;
1586
+ const gapToNext = sorted[1].dist - sorted[0].dist;
1587
+ if (gapToNext > sorted[0].dist * 10)
1588
+ return sorted[0].line;
1589
+ return null;
1590
+ }
1591
+ };
1592
+
1593
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/anchorer/blockAnchorer.js
1594
+ var BlockAnchorer = class _BlockAnchorer {
1595
+ static AnchorBlocks(hunk, blocks, lines, options) {
1596
+ return blocks.map((focus, index) => {
1597
+ let aboveEdge = focus.StartIndex;
1598
+ let belowEdge = focus.EndIndex;
1599
+ let aboveBlockIdx = index - 1;
1600
+ let belowBlockIdx = index + 1;
1601
+ let chunk = null;
1602
+ while (true) {
1603
+ const aboveBound = aboveBlockIdx >= 0 ? blocks[aboveBlockIdx].EndIndex + 1 : 0;
1604
+ const belowBound = belowBlockIdx < blocks.length ? blocks[belowBlockIdx].StartIndex - 1 : hunk.Lines.length - 1;
1605
+ for (let k = 1; k <= options.ContextWindowMax; k++) {
1606
+ const ctxAbove = _BlockAnchorer.ContextLines(hunk, aboveEdge - 1, aboveBound, k, true);
1607
+ const ctxBelow = _BlockAnchorer.ContextLines(hunk, belowEdge + 1, belowBound, k, false);
1608
+ const needle = _BlockAnchorer.BuildNeedle(hunk, aboveEdge, belowEdge);
1609
+ const match = _BlockAnchorer.Anchor(lines, ctxAbove, needle, ctxBelow);
1610
+ chunk = new Chunk(aboveEdge == focus.StartIndex ? ctxAbove : _BlockAnchorer.ContextLines(hunk, focus.StartIndex - 1, 0, k, true), focus.DeleteLines, focus.InsertLines, belowEdge == focus.EndIndex ? ctxBelow : _BlockAnchorer.ContextLines(hunk, focus.EndIndex + 1, hunk.Lines.length - 1, k, false), _BlockAnchorer.AlignMatchToFocus(match, hunk, aboveEdge, focus.StartIndex), new DiffLocation(hunk, focus.StartIndex, focus.EndIndex));
1611
+ if (!chunk.Match.IsAmbiguous)
1612
+ return chunk;
1613
+ if (ctxAbove.length < k && ctxBelow.length < k)
1614
+ break;
1615
+ }
1616
+ if (aboveBlockIdx < 0 && belowBlockIdx >= blocks.length)
1617
+ break;
1618
+ const ctxAboveCount = aboveBlockIdx >= 0 ? aboveEdge - (blocks[aboveBlockIdx].EndIndex + 1) : Number.MAX_SAFE_INTEGER;
1619
+ const ctxBelowCount = belowBlockIdx < blocks.length ? blocks[belowBlockIdx].StartIndex - (belowEdge + 1) : Number.MAX_SAFE_INTEGER;
1620
+ if (ctxBelowCount <= ctxAboveCount && belowBlockIdx < blocks.length)
1621
+ belowEdge = blocks[belowBlockIdx++].EndIndex;
1622
+ else
1623
+ aboveEdge = blocks[aboveBlockIdx--].StartIndex;
1624
+ }
1625
+ if (chunk.Match.IsAmbiguous && hunk.OldStart >= 0) {
1626
+ const tiebroken = _BlockAnchorer.TryLineNumberTiebreak(hunk, focus, lines);
1627
+ if (tiebroken != null)
1628
+ chunk = chunk.with({ Match: tiebroken });
1629
+ }
1630
+ return chunk;
1631
+ });
1632
+ }
1633
+ static TryLineNumberTiebreak(hunk, focus, lines) {
1634
+ let expectedLine = hunk.OldStart - 1;
1635
+ for (let i = 0; i < focus.StartIndex; i++)
1636
+ if (hunk.Lines[i].Type != LineType.Insert)
1637
+ expectedLine++;
1638
+ if (focus.DeleteLines.length == 0) {
1639
+ if (expectedLine < 0 || expectedLine > lines.length)
1640
+ return null;
1641
+ if (!_BlockAnchorer.CtxMatchesAtSlot(hunk, focus, lines, expectedLine))
1642
+ return null;
1643
+ return new UniqueMatch(MatchState.Success, expectedLine);
1644
+ }
1645
+ const candidates = Search.FindAllExact(lines, focus.DeleteLines);
1646
+ const resolved = Search.TiebreakByLineNumber(candidates, expectedLine);
1647
+ return resolved != null && _BlockAnchorer.CtxMatchesAtSlot(hunk, focus, lines, resolved) ? new UniqueMatch(MatchState.Success, resolved) : null;
1648
+ }
1649
+ // Validates the hunk's immediate context lines against a resolved slot's
1650
+ // neighbours so a stale or shifted header errs loudly instead of silently
1651
+ // mis-placing the edit. Content-bearing context compares under the search's own
1652
+ // strictness ladder — the revalidation must not be stricter than the search that
1653
+ // produced the candidates, or indent-slopped slots the search matched get
1654
+ // rejected loudly. But a line that trims to near-nothing (brace/paren-only)
1655
+ // carries its signal IN the indentation, which every ladder pass beyond rstrip
1656
+ // erases — such context stays whitespace-right-trimmed strict, or a shifted
1657
+ // header walks the edit to any same-shaped brace. Both tolerances corpus-measured
1658
+ // 2026-07-06 (ladder-everywhere: 3 silent mis-applies; this gate: 0, +14 recall).
1659
+ // Serves both callers: a pure insert (DeleteLines empty, after-context sits at
1660
+ // `slot`) and a delete-block tiebreak (deletes occupy [slot, slot + DeleteLines.length)).
1661
+ static CtxMatchesAtSlot(hunk, focus, lines, slot) {
1662
+ const MinLadderContent = 3;
1663
+ const matches = (hunkIdx, lineIdx) => {
1664
+ if (hunkIdx < 0 || hunkIdx >= hunk.Lines.length || hunk.Lines[hunkIdx].Type != LineType.Context)
1665
+ return true;
1666
+ if (lineIdx < 0 || lineIdx >= lines.length)
1667
+ return false;
1668
+ const text = hunk.Lines[hunkIdx].Text;
1669
+ return text.trim().length >= MinLadderContent ? Search.LinesEquivalent(lines[lineIdx], text) : lines[lineIdx].trimEnd() == text.trimEnd();
1670
+ };
1671
+ return matches(focus.StartIndex - 1, slot - 1) && matches(focus.EndIndex + 1, slot + focus.DeleteLines.length);
1672
+ }
1673
+ static AlignMatchToFocus(match, hunk, needleStartIndex, focusStartIndex) {
1674
+ if (!match.IsSuccess)
1675
+ return match;
1676
+ return new UniqueMatch(match.State, match.LineIndex + hunk.Lines.slice(needleStartIndex, focusStartIndex).filter((l) => l.Type != LineType.Insert).length, match.Fuzz);
1677
+ }
1678
+ static ContextLines(hunk, startLine, bound, maxTake, isAbove) {
1679
+ if (maxTake <= 0)
1680
+ return [];
1681
+ const lines = [];
1682
+ for (let i = 0; i < maxTake; i++) {
1683
+ const index = startLine + (isAbove ? -i : i);
1684
+ if (!(index >= 0 && index < hunk.Lines.length))
1685
+ break;
1686
+ if (!(isAbove ? index >= bound : index <= bound))
1687
+ break;
1688
+ const line = hunk.Lines[index];
1689
+ if (line.Type != LineType.Context)
1690
+ break;
1691
+ lines.push(line.Text);
1692
+ }
1693
+ return isAbove ? [...lines].reverse() : lines;
1694
+ }
1695
+ static BuildNeedle(h, startIndex, endIndex) {
1696
+ return h.Lines.slice(startIndex, endIndex + 1).filter((line) => line.Type != LineType.Insert).map((line) => line.Text);
1697
+ }
1698
+ static Anchor(lines, contextAbove, deleteLines, contextBelow) {
1699
+ if (deleteLines.length == 0)
1700
+ return Search.FindContextAnchor(lines, contextAbove, contextBelow);
1701
+ const pattern = contextAbove.concat(deleteLines).concat(contextBelow);
1702
+ const match = Search.FindUnique(lines, pattern);
1703
+ if (match.IsAmbiguous)
1704
+ return UniqueMatch.Ambiguous;
1705
+ if (match.IsSuccess)
1706
+ return new UniqueMatch(match.State, match.LineIndex + contextAbove.length, match.Fuzz);
1707
+ return Search.FindUnique(lines, deleteLines);
1708
+ }
1709
+ };
1710
+
1711
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/anchorer/hunkAnchorer.js
1712
+ var AnchorOutcome = class {
1713
+ Chunks;
1714
+ AlreadyAppliedCount;
1715
+ constructor(Chunks, AlreadyAppliedCount) {
1716
+ this.Chunks = Chunks;
1717
+ this.AlreadyAppliedCount = AlreadyAppliedCount;
1718
+ }
1719
+ };
1720
+ var HunkAnchorer = class _HunkAnchorer {
1721
+ static Anchor(originalText, hunks, options) {
1722
+ const lines = TextUtils.ToLines(originalText);
1723
+ const slicedHunks = HunkSlicer.Slice(hunks);
1724
+ const chunks = [];
1725
+ for (const sliced of slicedHunks)
1726
+ for (const chunk of BlockAnchorer.AnchorBlocks(sliced.Hunk, sliced.Blocks, lines, options))
1727
+ chunks.push(_HunkAnchorer.UseFallbackAnchorIfNecessary(sliced.Hunk, chunk, lines.length));
1728
+ const deleteCounts = /* @__PURE__ */ new Map();
1729
+ for (const l of chunks.flatMap((c) => c.DeleteLines))
1730
+ if (l.trim().length > 0)
1731
+ deleteCounts.set(l, (deleteCounts.get(l) ?? 0) + 1);
1732
+ const movedFromElsewhere = (c, line) => (deleteCounts.get(line) ?? 0) > c.DeleteLines.filter((d) => d === line).length;
1733
+ const kept = chunks.filter((c) => !_HunkAnchorer.AppliedAtAnchor(lines, c) && (c.InsertLines.some((l) => movedFromElsewhere(c, l)) || !_HunkAnchorer.AlreadyApplied(lines, c))).map((c) => _HunkAnchorer.TrySubLineSplice(c, lines));
1734
+ return new AnchorOutcome(kept, chunks.length - kept.length);
1735
+ }
1736
+ static UseFallbackAnchorIfNecessary(hunk, chunk, lineCount) {
1737
+ if (chunk.Match.IsNotFound && hunk.Lines.length == chunk.InsertLines.length) {
1738
+ const anchorLine = hunk.OldStart <= 1 ? 0 : hunk.OldStart;
1739
+ if (anchorLine > lineCount)
1740
+ return chunk;
1741
+ return new Chunk(chunk.ContextBefore, chunk.DeleteLines, chunk.InsertLines, chunk.ContextAfter, new UniqueMatch(MatchState.Success, anchorLine), chunk.DiffLocation);
1742
+ }
1743
+ return chunk;
1744
+ }
1745
+ // Sub-line splice fallback: a delete line that matches no file line but occurs
1746
+ // as a substring of exactly one line is an author-quoted fragment — an author
1747
+ // cannot intend to delete text they never mentioned, so the only coherent
1748
+ // reading replaces the fragment within the line and preserves the rest. Every
1749
+ // guard failure means fragment-vs-mangled-whole-line is undecidable: the chunk
1750
+ // stays NotFound and errors loudly. The insert-side guards catch damaged
1751
+ // whole-line edits: an insert that echoes text from outside the fragment, or
1752
+ // dwarfs it, was authored by someone who saw the whole line.
1753
+ // MUST run only on chunks that survived the already-applied filter: an applied
1754
+ // edit that wrapped the old line (comment-out, prefix insertion) leaves the
1755
+ // delete line as a substring of the new line, and splicing it again would
1756
+ // stack the wrapper (corpus wave-1 case 177: "#x" became "##x").
1757
+ static TrySubLineSplice(chunk, lines) {
1758
+ if (!chunk.Match.IsNotFound || chunk.DeleteLines.length != 1 || chunk.InsertLines.length > 1)
1759
+ return chunk;
1760
+ const frag = chunk.DeleteLines[0];
1761
+ if (frag.trim().length < _HunkAnchorer.MinSpliceFragmentContent)
1762
+ return chunk;
1763
+ let lineIndex = -1;
1764
+ for (let i = 0; i < lines.length; i++) {
1765
+ const first = lines[i].indexOf(frag);
1766
+ if (first < 0)
1767
+ continue;
1768
+ const again = lines[i].indexOf(frag, first + 1) >= 0;
1769
+ if (lineIndex >= 0 || again)
1770
+ return chunk;
1771
+ lineIndex = i;
1772
+ }
1773
+ if (lineIndex < 0)
1774
+ return chunk;
1775
+ const target = lines[lineIndex];
1776
+ const at = target.indexOf(frag);
1777
+ const prefix = target.slice(0, at), suffix = target.slice(at + frag.length);
1778
+ const insert = chunk.InsertLines.length == 1 ? chunk.InsertLines[0] : "";
1779
+ if (prefix.length == 0 && suffix.length > 0)
1780
+ return chunk;
1781
+ if (insert.length == 0 && prefix.length > 0 && suffix.length > 0 && /\s/.test(prefix[prefix.length - 1]) && /\s/.test(suffix[0]))
1782
+ return chunk;
1783
+ const remnantOk = (r) => r.length == 0 || r.trim().length >= _HunkAnchorer.MinSpliceRemnantContent;
1784
+ const echoes = (r) => r.length > 0 && insert.includes(r.trim());
1785
+ if (!remnantOk(prefix) || !remnantOk(suffix) || echoes(prefix) || echoes(suffix) || insert.length > frag.length * 2 + 8 && _HunkAnchorer.SharedStem(frag, insert) < _HunkAnchorer.MinSpliceStemContent)
1786
+ return chunk;
1787
+ return new Chunk([], [target], [prefix + insert + suffix], [], new UniqueMatch(MatchState.Success, lineIndex, _HunkAnchorer.SubLineSpliceFuzz), chunk.DiffLocation);
1788
+ }
1789
+ // Trimmed length a fragment must reach before substring evidence counts.
1790
+ static MinSpliceFragmentContent = 8;
1791
+ // Trimmed length a non-empty out-of-fragment remnant must carry.
1792
+ static MinSpliceRemnantContent = 4;
1793
+ // Trimmed length the fragment/insert shared stem must reach to waive the dwarf cap.
1794
+ static MinSpliceStemContent = 8;
1795
+ // The longest run the insert shares with the fragment's start or end, in trimmed
1796
+ // chars — the fragment-edit evidence that waives the dwarf cap.
1797
+ static SharedStem(frag, insert) {
1798
+ let p = 0;
1799
+ while (p < frag.length && p < insert.length && frag[p] == insert[p])
1800
+ p++;
1801
+ let s = 0;
1802
+ while (s < frag.length && s < insert.length && frag[frag.length - 1 - s] == insert[insert.length - 1 - s])
1803
+ s++;
1804
+ return Math.max(frag.slice(0, p).trim().length, s == 0 ? 0 : frag.slice(-s).trim().length);
1805
+ }
1806
+ // Reported fuzz for a spliced edit: looser than every line-match pass.
1807
+ static SubLineSpliceFuzz = 500;
1808
+ // A sanity check to avoid errors for a chunk that is already applied.
1809
+ // Asymmetric strictness: insert-side evidence is capped at whitespace-level
1810
+ // passes (a homoglyph variant of the post-image is not proof the edit landed,
1811
+ // and would turn a loud MatchNotFound into a silent no-op), while the delete
1812
+ // side stays uncapped (finding the pre-image even loosely proves the edit is
1813
+ // still pending, blocking a false already-applied verdict).
1814
+ static AlreadyApplied(lines, c) {
1815
+ if (!c.HasContextLines() && (c.IsPureDelete || c.IsPureInsert))
1816
+ return false;
1817
+ const insertImage = c.HasContextLines() ? c.InsertLinesWithContext() : c.InsertLines;
1818
+ const deleteImage = c.HasContextLines() ? c.DeleteLinesWithContext() : c.DeleteLines;
1819
+ const insertAt = Search.Find(lines, insertImage, 0, _HunkAnchorer.MaxAppliedEvidenceFuzz);
1820
+ return insertAt.LineIndex != -1 && Search.Find(lines, deleteImage).LineIndex == -1 && !_HunkAnchorer.OldImagePresentElsewhere(lines, c.DeleteLines, c.IsPureDelete ? null : insertImage, insertAt.LineIndex);
1821
+ }
1822
+ // Veto on the already-applied verdict: "old image absent" cannot
1823
+ // distinguish an applied edit from a delete block damaged in transit — one bad
1824
+ // char defeats every search pass — so a window nearly matching the delete block
1825
+ // proves the old image is still in the file and the edit pending: keep the chunk
1826
+ // and let it error loudly. Refinements, each forced by a corpus counter-example:
1827
+ // - Windows overlapping an insert-image occurrence never veto: after a genuine
1828
+ // apply the region shares most lines with the old image (rewrites re-state
1829
+ // unchanged lines as -/+), and vetoing there would break idempotent re-apply.
1830
+ // - Pure deletes skip that exclusion (exclusionImage = null): their post-image
1831
+ // is thin context that occurs everywhere — even inside the pending block
1832
+ // itself — so its occurrences prove nothing.
1833
+ // - The window is the delete block WITHOUT context: context sits in both images
1834
+ // and would glue the pending old image to a context match, masking it.
1835
+ // - Only content-bearing lines vote, with an absolute floor: blank/brace lines
1836
+ // near-match everywhere and tiny blocks carry too little signal either way.
1837
+ static OldImagePresentElsewhere(lines, dels, exclusionImage, insertIndex) {
1838
+ const contentIdx = [...dels.keys()].filter((i) => dels[i].trim().length > 0);
1839
+ if (contentIdx.length < _HunkAnchorer.MinNearImageMatches)
1840
+ return false;
1841
+ const insertStarts = exclusionImage == null ? [] : [...Search.FindAllExact(lines, exclusionImage), insertIndex];
1842
+ const overlapsInsert = (start) => insertStarts.some((s) => start < s + exclusionImage.length && s < start + dels.length);
1843
+ for (let start = 0; start + dels.length <= lines.length; start++) {
1844
+ if (overlapsInsert(start))
1845
+ continue;
1846
+ const matched = contentIdx.filter((j) => lines[start + j] === dels[j]).length;
1847
+ if (matched >= _HunkAnchorer.MinNearImageMatches && matched * 2 > contentIdx.length)
1848
+ return true;
1849
+ }
1850
+ return false;
1851
+ }
1852
+ // Exact-matching content lines required before a near-old-image window counts.
1853
+ static MinNearImageMatches = 3;
1854
+ // Highest per-line fuzz (strip pass) that still counts as already-applied evidence.
1855
+ static MaxAppliedEvidenceFuzz = 100;
1856
+ // A successfully anchored pure-insert chunk whose full post-image (context +
1857
+ // inserted lines + context) already sits at the resolved slot — or immediately
1858
+ // before it — would duplicate itself if applied: the file already reflects this
1859
+ // chunk. Including the context lines in the comparison distinguishes "already
1860
+ // applied" from a diff that intentionally duplicates adjacent lines (there the
1861
+ // old-side context sits where the copy would be, so the post-image cannot match).
1862
+ static AppliedAtAnchor(lines, c) {
1863
+ if (!c.IsPureInsert || !c.Match.IsSuccess)
1864
+ return false;
1865
+ const post = c.InsertLinesWithContext();
1866
+ const slot = c.Match.LineIndex;
1867
+ return _HunkAnchorer.RegionEquals(lines, slot - c.ContextBefore.length, post) || _HunkAnchorer.RegionEquals(lines, slot - c.ContextBefore.length - c.InsertLines.length, post);
1868
+ }
1869
+ static RegionEquals(lines, start, region) {
1870
+ if (start < 0 || start + region.length > lines.length)
1871
+ return false;
1872
+ for (let i = 0; i < region.length; i++)
1873
+ if (lines[start + i] !== region[i])
1874
+ return false;
1875
+ return true;
1876
+ }
1877
+ };
1878
+
1879
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/selectionTarget.js
1880
+ var SelectionTarget = class {
1881
+ FullText;
1882
+ SelectedText;
1883
+ UseSelection = false;
1884
+ LineOffset = 0;
1885
+ get TargetText() {
1886
+ return this.UseSelection ? this.SelectedText : this.FullText;
1887
+ }
1888
+ constructor(fullText, selection = "") {
1889
+ this.FullText = fullText;
1890
+ this.SelectedText = selection;
1891
+ if (!(this.SelectedText == null || this.SelectedText.length == 0)) {
1892
+ const hay = TextUtils.ToLines(this.FullText);
1893
+ const needle = TextUtils.ToLines(this.SelectedText);
1894
+ const result = Search.Find(hay, needle);
1895
+ this.UseSelection = result.LineIndex >= 0;
1896
+ this.LineOffset = this.UseSelection ? Math.max(0, result.LineIndex) : 0;
1897
+ }
1898
+ }
1899
+ Replace(replace) {
1900
+ if (!this.UseSelection)
1901
+ return replace;
1902
+ const fullLines = TextUtils.ToLines(this.FullText);
1903
+ const selectionLines = TextUtils.ToLines(this.SelectedText);
1904
+ const replaceLines = TextUtils.ToLines(replace);
1905
+ const edit = new Edit(this.LineOffset, selectionLines, replaceLines);
1906
+ edit.ApplyTo(fullLines);
1907
+ return TextUtils.RoundTripWhitespace(this.FullText, fullLines);
1908
+ }
1909
+ };
1910
+
1911
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/sanitizers/shared.js
1912
+ function isDiffMarker(line) {
1913
+ return line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("@@") || line.startsWith("diff --git");
1914
+ }
1915
+ function isHunkBoundary(line, trimmed) {
1916
+ return isDiffMarker(line) || trimmed.startsWith("```");
1917
+ }
1918
+ function isValidPrefix(char) {
1919
+ return char === "+" || char === "-" || char === " ";
1920
+ }
1921
+ function transformOutsideHunkBodies(text, transform) {
1922
+ let remainingOld = 0, remainingNew = 0;
1923
+ let inBareBody = false;
1924
+ const kept = [];
1925
+ for (const line of text.split("\n")) {
1926
+ const trimmed = line.trim();
1927
+ const m = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(trimmed);
1928
+ if (m) {
1929
+ remainingOld = m[1] !== void 0 ? parseInt(m[1]) : 1;
1930
+ remainingNew = m[2] !== void 0 ? parseInt(m[2]) : 1;
1931
+ inBareBody = false;
1932
+ kept.push(line);
1933
+ continue;
1934
+ }
1935
+ if (remainingOld > 0 || remainingNew > 0) {
1936
+ const c = line.length > 0 ? line[0] : " ";
1937
+ if (c === "-")
1938
+ remainingOld--;
1939
+ else if (c === "+")
1940
+ remainingNew--;
1941
+ else {
1942
+ remainingOld--;
1943
+ remainingNew--;
1944
+ }
1945
+ kept.push(line);
1946
+ continue;
1947
+ }
1948
+ if (trimmed.startsWith("@@")) {
1949
+ inBareBody = true;
1950
+ kept.push(line);
1951
+ continue;
1952
+ }
1953
+ if (inBareBody && line.length > 0 && isValidPrefix(line[0]) && !isLikelyFileHeader(trimmed)) {
1954
+ kept.push(line);
1955
+ continue;
1956
+ }
1957
+ inBareBody = false;
1958
+ const t = transform(line);
1959
+ if (t !== null)
1960
+ kept.push(t);
1961
+ }
1962
+ return kept.join("\n");
1963
+ }
1964
+ function isLikelyFileHeader(trimmed) {
1965
+ return trimmed.startsWith("diff --git") || (trimmed.startsWith("--- ") || trimmed.startsWith("+++ ")) && (trimmed.includes(".") || trimmed.includes("/") || trimmed.includes("\\"));
1966
+ }
1967
+
1968
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/sanitizers/customFormatSanitizer.js
1969
+ var CustomFormatSanitizer = class _CustomFormatSanitizer {
1970
+ static OPERATIONS = [
1971
+ { header: /^\s*\*\*\* Update File: (.+)$/i, fromPath: (p) => `a/${p}`, toPath: (p) => `b/${p}` },
1972
+ { header: /^\s*\*\*\* Add File: (.+)$/i, fromPath: (_p) => "/dev/null", toPath: (p) => `b/${p}` },
1973
+ { header: /^\s*\*\*\* Delete File: (.+)$/i, fromPath: (p) => `a/${p}`, toPath: (_p) => "/dev/null" }
1974
+ ];
1975
+ static process(text) {
1976
+ return transformOutsideHunkBodies(text, (line) => {
1977
+ const hadCr = line.endsWith("\r");
1978
+ const core = hadCr ? line.slice(0, -1) : line;
1979
+ for (const op of _CustomFormatSanitizer.OPERATIONS) {
1980
+ const m = core.match(op.header);
1981
+ if (!m)
1982
+ continue;
1983
+ const path = m[1].trim();
1984
+ const tail = hadCr ? "\r" : "";
1985
+ return `--- ${op.fromPath(path)}${tail}
1986
+ +++ ${op.toPath(path)}${tail}`;
1987
+ }
1988
+ return line;
1989
+ });
1990
+ }
1991
+ };
1992
+
1993
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/sanitizers/decoratedHeaderSanitizer.js
1994
+ var DecoratedHeaderSanitizer = class _DecoratedHeaderSanitizer {
1995
+ static DECORATED_HEADER_REGEX = /^(---|\+\+\+)\s+(.+?)\s+(---|\+\+\+)+\s*$/;
1996
+ static process(text) {
1997
+ return transformOutsideHunkBodies(text, (line) => {
1998
+ const hadCr = line.endsWith("\r");
1999
+ const core = hadCr ? line.slice(0, -1) : line;
2000
+ const replaced = core.replace(_DecoratedHeaderSanitizer.DECORATED_HEADER_REGEX, "$1 $2");
2001
+ return replaced === core ? line : hadCr ? replaced + "\r" : replaced;
2002
+ });
2003
+ }
2004
+ };
2005
+
2006
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/sanitizers/decorativeMarkerSanitizer.js
2007
+ var DecorativeMarkerSanitizer = class _DecorativeMarkerSanitizer {
2008
+ static process(text) {
2009
+ return transformOutsideHunkBodies(text, (line) => _DecorativeMarkerSanitizer.isDecorativeMarker(line.trim()) ? null : line);
2010
+ }
2011
+ static isDecorativeMarker(line) {
2012
+ if (!line)
2013
+ return false;
2014
+ if (/^diff --git\s+/.test(line))
2015
+ return false;
2016
+ if (/^[*#_]{3,}$/.test(line))
2017
+ return true;
2018
+ if (/^[=\-]{5,}$/.test(line))
2019
+ return true;
2020
+ const wholeLineKeyword = /^[*=\-#_\s]*(BEGIN|END|START|STOP)\s+(PATCH|DIFF|HUNK|SECTION|BLOCK)[*=\-#_\s]*$/i.test(line);
2021
+ if (wholeLineKeyword && /[*=\-#_]{2,}/.test(line))
2022
+ return true;
2023
+ return false;
2024
+ }
2025
+ };
2026
+
2027
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/sanitizers/backtickSanitizer.js
2028
+ var BacktickSanitizer = class {
2029
+ static process(text) {
2030
+ return text.replace(/^```+\s*diff\b/gmi, "```diff").replace(/^```+$/gm, "```");
2031
+ }
2032
+ };
2033
+
2034
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/sanitizers/diffBlockReunifier.js
2035
+ var DiffBlockReunifier = class {
2036
+ static process(text) {
2037
+ const lines = text.split("\n");
2038
+ const result = [];
2039
+ let inDiff = false;
2040
+ for (let i = 0; i < lines.length; i++) {
2041
+ const line = lines[i];
2042
+ const trimmed = line.trim();
2043
+ if (trimmed.startsWith("```diff")) {
2044
+ inDiff = true;
2045
+ result.push(line);
2046
+ } else if (trimmed === "```" && line.startsWith("```")) {
2047
+ if (inDiff && this.hasDiffContentBeforeNextFence(lines, i)) {
2048
+ continue;
2049
+ }
2050
+ inDiff = false;
2051
+ result.push(line);
2052
+ } else {
2053
+ result.push(line);
2054
+ }
2055
+ }
2056
+ return result.join("\n");
2057
+ }
2058
+ static hasDiffContentBeforeNextFence(lines, currentIndex) {
2059
+ for (let j = currentIndex + 1; j < lines.length; j++) {
2060
+ const ahead = lines[j].trim();
2061
+ if (lines[j].startsWith("```"))
2062
+ return false;
2063
+ if (isDiffMarker(ahead))
2064
+ return true;
2065
+ }
2066
+ return false;
2067
+ }
2068
+ };
2069
+
2070
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/sanitizers/devNullSanitizer.js
2071
+ var DevNullSanitizer = class {
2072
+ static process(text, fileKeys) {
2073
+ if (!fileKeys?.length)
2074
+ return text;
2075
+ const existingFiles = new Set(fileKeys);
2076
+ const lines = text.split("\n");
2077
+ const result = [];
2078
+ for (let i = 0; i < lines.length; i++) {
2079
+ const line = lines[i];
2080
+ const trimmed = line.trim();
2081
+ if (this.isNewFileModeForExistingFile(trimmed, lines, i, existingFiles)) {
2082
+ continue;
2083
+ }
2084
+ if (trimmed.startsWith("--- /dev/null")) {
2085
+ const targetFile = this.getTargetFileFromNextLine(lines[i + 1]);
2086
+ if (targetFile && existingFiles.has(targetFile)) {
2087
+ result.push(line.replace("/dev/null", `a/${targetFile}`));
2088
+ continue;
2089
+ }
2090
+ }
2091
+ result.push(line);
2092
+ }
2093
+ return result.join("\n");
2094
+ }
2095
+ static isNewFileModeForExistingFile(line, allLines, currentIndex, existingFiles) {
2096
+ if (!line.startsWith("new file mode"))
2097
+ return false;
2098
+ const prevLine = allLines[currentIndex - 1]?.trim() || "";
2099
+ if (!prevLine.startsWith("diff --git"))
2100
+ return false;
2101
+ const match = prevLine.match(/diff --git\s+[ab]\/(\S+)\s+[ab]\/\S+/);
2102
+ const filePath = match?.[1];
2103
+ return filePath ? existingFiles.has(filePath) : false;
2104
+ }
2105
+ static getTargetFileFromNextLine(nextLine) {
2106
+ if (!nextLine?.trim().startsWith("+++ "))
2107
+ return null;
2108
+ const match = nextLine.match(/^\+\+\+\s+(?:[ab]\/)?(\S+)/);
2109
+ return match?.[1] ?? null;
2110
+ }
2111
+ };
2112
+
2113
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/sanitizers/prefixInjector.js
2114
+ var PrefixInjector = class {
2115
+ static process(text) {
2116
+ const lines = text.split("\n");
2117
+ let inHunk = false;
2118
+ let deletes = { remaining: 0, consumed: 0 };
2119
+ let adds = { remaining: 0, consumed: 0 };
2120
+ for (let i = 0; i < lines.length; i++) {
2121
+ const line = lines[i];
2122
+ if (line.length === 0 && i === lines.length - 1)
2123
+ continue;
2124
+ const trimmed = line.trim();
2125
+ if (trimmed.startsWith("@@")) {
2126
+ const { deleteCount, addCount } = this.parseHunkHeader(trimmed);
2127
+ inHunk = true;
2128
+ deletes = { remaining: deleteCount, consumed: 0 };
2129
+ adds = { remaining: addCount, consumed: 0 };
2130
+ continue;
2131
+ }
2132
+ if (isHunkBoundary(line, trimmed)) {
2133
+ inHunk = false;
2134
+ continue;
2135
+ }
2136
+ if (inHunk) {
2137
+ const first = line[0] ?? "";
2138
+ const hasValidPrefix = isValidPrefix(first);
2139
+ const prefix = hasValidPrefix ? first : this.inferPrefix(deletes.remaining - deletes.consumed, adds.remaining - adds.consumed);
2140
+ if (!hasValidPrefix && prefix !== null) {
2141
+ lines[i] = prefix + line;
2142
+ }
2143
+ if (prefix === "+")
2144
+ adds.consumed++;
2145
+ else if (prefix === "-")
2146
+ deletes.consumed++;
2147
+ else {
2148
+ adds.consumed++;
2149
+ deletes.consumed++;
2150
+ }
2151
+ }
2152
+ }
2153
+ return lines.join("\n");
2154
+ }
2155
+ static parseHunkHeader(header) {
2156
+ const match = header.match(/@@\s*-(\d+)(?:,(\d+))?\s*\+(\d+)(?:,(\d+))?\s*@@/);
2157
+ return match ? { deleteCount: parseInt(match[2] ?? "1"), addCount: parseInt(match[4] ?? "1") } : { deleteCount: 0, addCount: 0 };
2158
+ }
2159
+ // Only inject when the header arithmetic is unambiguous. When both deletes and
2160
+ // adds remain (or the header had no usable counts), leave the line untouched —
2161
+ // the parser's raw-line heuristics use content evidence and insertion-lookahead
2162
+ // to classify it better than count-based guessing can.
2163
+ static inferPrefix(remainingDeletes, remainingAdds) {
2164
+ if (remainingDeletes > 0 && remainingAdds <= 0)
2165
+ return "-";
2166
+ if (remainingAdds > 0 && remainingDeletes <= 0)
2167
+ return "+";
2168
+ return null;
2169
+ }
2170
+ };
2171
+
2172
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/parser/diffSanitizer.js
2173
+ var DiffSanitizer = class {
2174
+ static Process(diff, fileKeys) {
2175
+ if (!diff?.trim())
2176
+ return diff;
2177
+ let result = CustomFormatSanitizer.process(diff);
2178
+ result = DecoratedHeaderSanitizer.process(result);
2179
+ result = DecorativeMarkerSanitizer.process(result);
2180
+ result = BacktickSanitizer.process(result);
2181
+ result = DiffBlockReunifier.process(result);
2182
+ result = DevNullSanitizer.process(result, fileKeys);
2183
+ result = PrefixInjector.process(result);
2184
+ return result;
2185
+ }
2186
+ };
2187
+
2188
+ // ../node_modules/.pnpm/matchu-patchu@0.2.0/node_modules/matchu-patchu/dist/patcher.js
2189
+ var Patcher = class _Patcher {
2190
+ static Apply(diff, files, options) {
2191
+ options = options ?? new PatchOptions();
2192
+ const fileKeys = files.map((f) => f.Key);
2193
+ const fileContents = new Map(files.map((f) => [f.Key, f.InputFullText]));
2194
+ diff = options.SanitizeDiff ? DiffSanitizer.Process(diff, fileKeys) : diff;
2195
+ const fileHunks = UnifiedDiffParser.Parse(diff, fileContents, options.Truncation, options.ControlChars);
2196
+ const foreignErrors = fileHunks.filter((g) => g.Key != "" && !fileContents.has(g.Key)).map((g) => new PatchError("FileMismatch", _Patcher.ForeignChunk(g.Hunks[0]), g.Key));
2197
+ if (foreignErrors.length > 0 && !options.ContinueOnError)
2198
+ throw new PatchException(foreignErrors[0]);
2199
+ const patchOutputFiles = files.map((f) => {
2200
+ const targetSelection = new SelectionTarget(f.InputFullText, f.InputSelectedText);
2201
+ const targetText = targetSelection.TargetText;
2202
+ const hunks = fileHunks.find((h) => h.Key == f.Key)?.Hunks ?? [];
2203
+ const anchored = HunkAnchorer.Anchor(targetText, hunks, options);
2204
+ const outputEdits = ChunkApplier.Apply(targetText, anchored.Chunks, options);
2205
+ return new PatchOutputFile(f.Key, outputEdits.Edits.map((h) => h.Fuzz).reduce((a, b) => a + b, 0), outputEdits.Edits.map((e) => e.Shift(targetSelection.LineOffset)), f.InputSelectedText, f.InputFullText, targetSelection.Replace(outputEdits.OutputText), [...outputEdits.Errors], anchored.AlreadyAppliedCount, hunks.flatMap((h) => h.Lines).filter((l) => l.CollapsedFrom != null).map((l) => l.CollapsedFrom), hunks.some((h) => h.TruncationSuspected), hunks.some((h) => h.ControlCharsSuspected));
2206
+ });
2207
+ const output = new PatchOutput(patchOutputFiles);
2208
+ output.Errors = foreignErrors;
2209
+ return output;
2210
+ }
2211
+ // Error-reporting stand-in for a hunk that was never anchored (it names a file
2212
+ // that isn't being patched): its lines and diff location, no match.
2213
+ static ForeignChunk(h) {
2214
+ return new Chunk([], h.Lines.filter((l) => l.Type == LineType.Delete).map((l) => l.Text), h.Lines.filter((l) => l.Type == LineType.Insert).map((l) => l.Text), [], UniqueMatch.NotFound, new DiffLocation(h));
2215
+ }
2216
+ };
2217
+
2218
+ // cli/agents/pi/server/piPatcherExtension.ts
2219
+ var DESCRIPTION = true ? "Apply unified diffs to files \u2014 tolerant of form, strict about intent. Repairs sloppy AI-generated diffs (mangled headers, whitespace drift, missing prefixes; line numbers can be wrong or absent \u2014 hunks anchor by fuzzy-matched context lines) and applies all hunks atomically when the intent is unambiguous; fails with a precise, typed error when it isn't. Use for multi-hunk edits in one step, or when exact string-replacement editing fails on whitespace or invisible characters." : "Apply unified diffs to files.";
2220
+ var BUILD_TAG = true ? "matchu-patchu-pi 0.2.0, built 2026-07-07 19:23:07" : "matchu-patchu-pi dev";
2221
+ var errorBlocks = (errors) => {
2222
+ const parts = [`Patch failed with ${errors.length} error(s):`];
2223
+ for (const e of errors) parts.push("", e.toString());
2224
+ return parts.join("\n");
2225
+ };
2226
+ var noOpMessage = (target, alreadyApplied) => alreadyApplied > 0 ? `Applied 0 edit(s) to ${target}: the file already contains this change (${alreadyApplied} chunk(s) detected as already applied). File left unmodified.` : `Applied 0 edit(s) to ${target}: the patch parsed but produced no changes for this file. File left unmodified.`;
2227
+ var fuzzNote = (fuzz) => fuzz > 0 ? ` (applied with fuzz factor ${fuzz})` : "";
2228
+ function discoverDiffKeys(diff) {
2229
+ const keys = [];
2230
+ for (const e of Patcher.Apply(diff, []).Errors) {
2231
+ if (e.FileKey && !keys.includes(e.FileKey)) keys.push(e.FileKey);
2232
+ }
2233
+ return keys;
2234
+ }
2235
+ function applySingle(cwd, filePath, diff, dryRun) {
2236
+ const abs = resolve(cwd, filePath);
2237
+ if (!existsSync(abs)) return { ok: false, message: `Error: file not found: ${filePath}` };
2238
+ const out = Patcher.Apply(diff, [new PatchInputFile("", readFileSync(abs, "utf-8"))]).Files[0];
2239
+ if (out.Errors.length > 0) return { ok: false, message: errorBlocks(out.Errors) };
2240
+ if (out.Edits.length === 0) return { ok: true, message: noOpMessage(filePath, out.AlreadyAppliedCount) };
2241
+ if (!dryRun) writeFileSync(abs, out.OutputFullText);
2242
+ return {
2243
+ ok: true,
2244
+ message: `Successfully applied ${out.Edits.length} edit(s) to ${filePath}${fuzzNote(out.Fuzz)}.` + (dryRun ? `
2245
+
2246
+ --- patched output ---
2247
+ ${out.OutputFullText}` : "")
2248
+ };
2249
+ }
2250
+ function applyMulti(cwd, diff, dryRun) {
2251
+ const keys = discoverDiffKeys(diff);
2252
+ if (keys.length === 0)
2253
+ return { ok: false, message: "Error: no file headers (---/+++) found in the diff \u2014 pass filePath to patch a headerless diff into one file" };
2254
+ const missing = keys.filter((k) => !existsSync(resolve(cwd, k)));
2255
+ if (missing.length > 0)
2256
+ return { ok: false, message: missing.map((k) => `Error: file not found: ${k}`).join("\n") };
2257
+ const inputs = keys.map((k) => new PatchInputFile(k, readFileSync(resolve(cwd, k), "utf-8")));
2258
+ const result = Patcher.Apply(diff, inputs);
2259
+ const errors = [...result.Errors, ...result.Files.flatMap((f) => f.Errors)];
2260
+ if (errors.length > 0) return { ok: false, message: errorBlocks(errors) };
2261
+ const lines = [];
2262
+ const dryOutputs = [];
2263
+ for (const out of result.Files) {
2264
+ if (out.Edits.length === 0) {
2265
+ lines.push(noOpMessage(out.Key, out.AlreadyAppliedCount));
2266
+ continue;
2267
+ }
2268
+ if (!dryRun) writeFileSync(resolve(cwd, out.Key), out.OutputFullText);
2269
+ else dryOutputs.push(`--- patched output: ${out.Key} ---
2270
+ ${out.OutputFullText}`);
2271
+ lines.push(`Successfully applied ${out.Edits.length} edit(s) to ${out.Key}${fuzzNote(out.Fuzz)}.`);
2272
+ }
2273
+ return { ok: true, message: [lines.join("\n"), ...dryOutputs].join("\n\n") };
2274
+ }
2275
+ function applyPatch(cwd, args) {
2276
+ if (!args.diff || !args.diff.trim()) return { ok: false, message: "Error: patch is empty" };
2277
+ try {
2278
+ const out = args.filePath ? applySingle(cwd, args.filePath, args.diff, args.dryRun === true) : applyMulti(cwd, args.diff, args.dryRun === true);
2279
+ return out.ok ? { ok: true, message: `${out.message} [${BUILD_TAG}]` } : out;
2280
+ } catch (ex) {
2281
+ if (ex instanceof PatchParserException)
2282
+ return { ok: false, message: `Error: failed to parse patch \u2014 ${ex.message}` };
2283
+ return { ok: false, message: `Error: ${ex instanceof Error ? ex.message : String(ex)}` };
2284
+ }
2285
+ }
2286
+ function logErr(...a) {
2287
+ try {
2288
+ console.error("[matchu-patchu]", ...a);
2289
+ } catch {
2290
+ }
2291
+ }
2292
+ function piPatcherExtension_default(pi) {
2293
+ try {
2294
+ pi.registerTool({
2295
+ name: "patch",
2296
+ label: "Patch",
2297
+ description: DESCRIPTION,
2298
+ promptSnippet: "Modify one or more existing files by applying a unified diff",
2299
+ promptGuidelines: [
2300
+ "Use patch for every edit to an existing file; write is only for creating new files or full rewrites.",
2301
+ "patch accepts lenient unified diffs: line numbers are optional and hunks anchor by context lines. One diff may target several files via ---/+++ headers; pass filePath instead for a headerless single-file diff."
2302
+ ],
2303
+ parameters: Type.Object({
2304
+ diff: Type.String({ description: "Unified diff content to apply" }),
2305
+ filePath: Type.Optional(Type.String({
2306
+ description: "Path of the single file to patch (absolute or relative to the project root); the diff's headers are then ignored. Omit to target the file(s) named by the diff's ---/+++ headers."
2307
+ })),
2308
+ dryRun: Type.Optional(Type.Boolean({ description: "If true, return the patched content without writing to disk" }))
2309
+ }),
2310
+ async execute(_id, params, _signal, _onUpdate, ctx) {
2311
+ const { ok, message } = applyPatch(ctx.cwd, params);
2312
+ if (!ok) throw new Error(message);
2313
+ return { content: [{ type: "text", text: message }], details: {} };
2314
+ }
2315
+ });
2316
+ pi.on("session_start", () => {
2317
+ try {
2318
+ const active = pi.getActiveTools();
2319
+ if (active.includes("edit")) pi.setActiveTools(active.filter((n) => n !== "edit"));
2320
+ } catch (e) {
2321
+ logErr("remove edit", e);
2322
+ }
2323
+ });
2324
+ } catch (e) {
2325
+ logErr("load", e);
2326
+ }
2327
+ }
2328
+ export {
2329
+ applyPatch,
2330
+ piPatcherExtension_default as default,
2331
+ discoverDiffKeys
2332
+ };