decorated-pi 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/extensions/patch.ts +328 -104
  2. package/package.json +1 -1
@@ -176,6 +176,103 @@ function applyOverwrite(
176
176
  // Edits (exact string replacement)
177
177
  // ═══════════════════════════════════════════════════════════════════════════
178
178
 
179
+ type LocateEditResult =
180
+ | {
181
+ found: true;
182
+ oldNorm: string;
183
+ newNorm: string;
184
+ matchIdx: number;
185
+ displayAnchor?: string;
186
+ anchorMissing: boolean;
187
+ }
188
+ | {
189
+ found: false;
190
+ oldNorm: string;
191
+ anchorState: "ok" | "missing" | "not_unique";
192
+ anchorMessage?: string;
193
+ };
194
+
195
+ /** Shared edit location logic used by both one-shot and sequential paths.
196
+ * Returns structured failure details when old_str is not found so callers can
197
+ * preserve precise diagnostics instead of guessing why matching failed.
198
+ * Throws ApplyError on duplicate global matches or non-unique old_str. */
199
+ function locateEdit(
200
+ edit: { old_str: string; new_str: string; anchor?: string },
201
+ content: string,
202
+ displayPath: string,
203
+ ): LocateEditResult {
204
+ let oldNorm = normalizeLineEndings(edit.old_str);
205
+ let newNorm = normalizeLineEndings(edit.new_str);
206
+
207
+ let searchFrom = 0;
208
+ let displayAnchor: string | undefined;
209
+ let anchorMissing = false;
210
+ let anchorState: "ok" | "missing" | "not_unique" = "ok";
211
+ let anchorMessage: string | undefined;
212
+
213
+ // ── Anchor parsing ──
214
+ if (edit.anchor) {
215
+ const anchorNorm = normalizeLineEndings(edit.anchor);
216
+ const anchorIdx = content.indexOf(anchorNorm);
217
+ if (anchorIdx === -1) {
218
+ anchorState = "missing";
219
+ anchorMessage = `Anchor not found in ${displayPath}: "${truncate(edit.anchor)}".`;
220
+ } else {
221
+ const secondAnchor = content.indexOf(anchorNorm, anchorIdx + 1);
222
+ if (secondAnchor !== -1) {
223
+ anchorState = "not_unique";
224
+ anchorMessage = `Anchor is not unique in ${displayPath}: "${truncate(edit.anchor)}".`;
225
+ } else {
226
+ searchFrom = Math.max(0, anchorIdx - (oldNorm.length - 1));
227
+ displayAnchor = edit.anchor;
228
+ }
229
+ }
230
+ }
231
+
232
+ // ── Exact match in search range ──
233
+ let matchIdx = anchorMessage ? -1 : content.indexOf(oldNorm, searchFrom);
234
+
235
+ // ── Global exact match fallback (when anchor was missing/unusable) ──
236
+ if (matchIdx === -1 && anchorMessage) {
237
+ displayAnchor = edit.anchor;
238
+ anchorMissing = true;
239
+ matchIdx = content.indexOf(oldNorm, 0);
240
+ if (matchIdx !== -1) {
241
+ const secondGlobalMatch = content.indexOf(oldNorm, matchIdx + 1);
242
+ if (secondGlobalMatch !== -1) {
243
+ const dupDiag = diagnoseOldStrNotUnique(oldNorm, content);
244
+ throw new ApplyError(`${anchorMessage}\n${dupDiag}`);
245
+ }
246
+ }
247
+ }
248
+
249
+ // ── Fuzzy match ──
250
+ if (matchIdx === -1) {
251
+ const searchLine = searchFrom === 0 ? 0 : content.substring(0, searchFrom).split("\n").length - 1;
252
+ const fuzzy = tryFuzzyLineMatch(oldNorm, content, searchLine);
253
+ if (fuzzy) {
254
+ oldNorm = fuzzy.matched;
255
+ matchIdx = fuzzy.idx;
256
+ newNorm = normalizeIndentForFuzzy(fuzzy.matched.split("\n")[0] ?? "", newNorm);
257
+ }
258
+ }
259
+
260
+ if (matchIdx === -1) {
261
+ return { found: false, oldNorm, anchorState, anchorMessage };
262
+ }
263
+
264
+ // ── Uniqueness check (skip when anchor was used as a fallback) ──
265
+ if (!anchorMessage) {
266
+ const secondMatch = content.indexOf(oldNorm, matchIdx + 1);
267
+ if (secondMatch !== -1) {
268
+ const dupDiag = diagnoseOldStrNotUnique(oldNorm, content);
269
+ throw new ApplyError(`${dupDiag}`);
270
+ }
271
+ }
272
+
273
+ return { found: true, oldNorm, newNorm, matchIdx, displayAnchor, anchorMissing };
274
+ }
275
+
179
276
  async function applyEdits(
180
277
  absPath: string,
181
278
  displayPath: string,
@@ -192,150 +289,205 @@ async function applyEdits(
192
289
 
193
290
  const rawContent = fs.readFileSync(absPath, "utf8");
194
291
  const lineEnding = detectLineEnding(rawContent);
195
- let content = normalizeLineEndings(rawContent);
292
+ const originalContent = normalizeLineEndings(rawContent);
196
293
 
197
- // Precompute line offsets for O(log n) line number lookups
198
- const lineOffsets = buildLineOffsets(rawContent);
294
+ // Precompute line offsets for the original file (used throughout)
295
+ const lineOffsets = buildLineOffsets(originalContent);
199
296
  const totalLines = lineOffsets.length - 1;
200
297
 
201
- // Track cumulative offset for mapping current positions back to original
202
- let cumulativeOffset = 0;
203
- const replacements: ReplacementInfo[] = [];
204
- const neededRanges: LineRange[] = [];
298
+ // ═══════════════════════════════════════════════════════════════════
299
+ // Phase 1: try matching every old_str against the ORIGINAL snapshot.
300
+ // If any edit requires content from a prior edit (chained dependency),
301
+ // fall back to sequential mode.
302
+ // ═══════════════════════════════════════════════════════════════════
303
+
304
+ const planned: Array<Extract<LocateEditResult, { found: true }>> = [];
305
+ let needsSequential = false;
205
306
 
206
307
  for (const edit of edits) {
207
308
  if (!edit.old_str) {
208
309
  throw new ApplyError(`old_str must not be empty in ${displayPath}.`);
209
310
  }
210
311
 
211
- let oldNorm = normalizeLineEndings(edit.old_str);
212
- let newNorm = normalizeLineEndings(edit.new_str);
312
+ const located = locateEdit(edit, originalContent, displayPath);
313
+ if (!located.found) {
314
+ // old_str not found in the original snapshot — likely chained edit.
315
+ // Fall back to sequential mode.
316
+ needsSequential = true;
317
+ break;
318
+ }
213
319
 
214
- // Determine search range
215
- let searchFrom = 0;
216
- let displayAnchor: string | undefined;
217
- let anchorMissing = false;
218
- let anchorNotFoundMessage: string | undefined;
320
+ planned.push(located);
321
+ }
219
322
 
220
- if (edit.anchor) {
221
- const anchorNorm = normalizeLineEndings(edit.anchor);
323
+ // ═══════════════════════════════════════════════════════════════════
324
+ // Sequential fallback — old behaviour for chained edits
325
+ // ═══════════════════════════════════════════════════════════════════
222
326
 
223
- // Find anchor — must be unique when present
224
- const anchorIdx = content.indexOf(anchorNorm);
225
- if (anchorIdx === -1) {
226
- anchorNotFoundMessage = `Anchor not found in ${displayPath}: "${truncate(edit.anchor)}".`;
227
- } else {
228
- const secondAnchor = content.indexOf(anchorNorm, anchorIdx + 1);
229
- if (secondAnchor !== -1) {
230
- anchorNotFoundMessage = `Anchor is not unique in ${displayPath}: "${truncate(edit.anchor)}".`;
231
- } else {
232
- searchFrom = Math.max(0, anchorIdx - (oldNorm.length - 1));
233
- displayAnchor = edit.anchor;
234
- anchorMissing = false;
235
- }
236
- }
237
- }
238
-
239
- // Find old_str in range — must be unique
240
- let matchIdx = anchorNotFoundMessage ? -1 : content.indexOf(oldNorm, searchFrom);
241
- if (matchIdx === -1 && anchorNotFoundMessage) {
242
- // Anchor was missing/unusable — try global exact match first
243
- displayAnchor = edit.anchor;
244
- anchorMissing = true;
245
- matchIdx = content.indexOf(oldNorm, 0);
246
- if (matchIdx !== -1) {
247
- const secondGlobalMatch = content.indexOf(oldNorm, matchIdx + 1);
248
- if (secondGlobalMatch !== -1) {
249
- const dupDiag = diagnoseOldStrNotUnique(oldNorm, content);
250
- throw new ApplyError(`${anchorNotFoundMessage}\n${dupDiag}`);
327
+ if (needsSequential) {
328
+ let content = originalContent;
329
+ let cumulativeOffset = 0;
330
+ const rawReplacements: ReplacementInfo[] = [];
331
+
332
+ for (const edit of edits) {
333
+ const located = locateEdit(edit, content, displayPath);
334
+ if (!located.found) {
335
+ const diag = diagnoseOldStrMismatch(located.oldNorm, content);
336
+ if (located.anchorState === "missing" || located.anchorState === "not_unique") {
337
+ throw new ApplyError(
338
+ `${located.anchorMessage}\nold_str not found in ${displayPath}: "${truncate(edit.old_str)}".\n${diag}`
339
+ );
251
340
  }
252
- }
253
- }
254
-
255
- if (matchIdx === -1) {
256
- // Fuzzy match fallback: normalize tab↔space + trailing whitespace
257
- const searchLine = searchFrom === 0 ? 0 : content.substring(0, searchFrom).split("\n").length - 1;
258
- const fuzzy = tryFuzzyLineMatch(oldNorm, content, searchLine);
259
- if (fuzzy) {
260
- oldNorm = fuzzy.matched;
261
- matchIdx = fuzzy.idx;
262
- newNorm = normalizeIndentForFuzzy(fuzzy.matched.split("\n")[0] ?? "", newNorm);
263
- } else if (anchorNotFoundMessage) {
264
- const diag = diagnoseOldStrMismatch(oldNorm, content);
265
- throw new ApplyError(
266
- `${anchorNotFoundMessage}\nold_str not found in ${displayPath}: "${truncate(edit.old_str)}".\n${diag}`
267
- );
268
- } else {
269
- const diag = diagnoseOldStrMismatch(oldNorm, content);
270
341
  throw new ApplyError(
271
342
  `old_str not found in ${displayPath}` +
272
343
  (edit.anchor ? ` after anchor "${truncate(edit.anchor)}"` : "") +
273
344
  `: "${truncate(edit.old_str)}".\n${diag}`
274
345
  );
275
346
  }
347
+
348
+ const { oldNorm, newNorm, matchIdx, displayAnchor, anchorMissing } = located;
349
+
350
+ const oldStartLine = lineAtOffset(lineOffsets, matchIdx - cumulativeOffset);
351
+ const oldEndLine = lineAtOffset(lineOffsets, matchIdx - cumulativeOffset + oldNorm.length - 1);
352
+
353
+ content =
354
+ content.substring(0, matchIdx) +
355
+ newNorm +
356
+ content.substring(matchIdx + oldNorm.length);
357
+
358
+ cumulativeOffset += newNorm.length - oldNorm.length;
359
+
360
+ rawReplacements.push({
361
+ oldStartLine,
362
+ oldEndLine,
363
+ newStartLine: 0, // placeholder — recalculated after collapse
364
+ newEndLine: 0,
365
+ oldLines: oldNorm.split("\n").filter((l, i, arr) => !(i === arr.length - 1 && l === "")),
366
+ newLines: newNorm.split("\n").filter((l, i, arr) => !(i === arr.length - 1 && l === "")),
367
+ anchor: displayAnchor ? displayAnchor.split("\n")[0] : undefined,
368
+ anchorMissing,
369
+ });
276
370
  }
277
371
 
278
- // Check uniqueness in anchor-narrowed / plain search path only when anchor was used normally
279
- if (!anchorNotFoundMessage) {
280
- const secondMatch = content.indexOf(oldNorm, matchIdx + 1);
281
- if (secondMatch !== -1) {
282
- const dupDiag = diagnoseOldStrNotUnique(oldNorm, content);
283
- throw new ApplyError(
284
- `${dupDiag}`
285
- );
372
+ // Collapse chained-edit replacements into net-change replacements,
373
+ // so the TUI diff shows only the net effect (original→final).
374
+ const cleanReplacements = collapseSequentialReplacements(rawReplacements);
375
+
376
+ const mergedRanges = mergeRanges(cleanReplacements.map(r => ({
377
+ startLine: Math.max(1, r.oldStartLine - CONTEXT_LINES),
378
+ endLine: Math.min(totalLines, r.oldEndLine + CONTEXT_LINES),
379
+ })));
380
+ const neededLines: Map<number, string> = new Map();
381
+ for (const range of mergedRanges) {
382
+ const lines = extractLineRange(originalContent, lineOffsets, range.startLine, range.endLine);
383
+ for (let i = 0; i < lines.length; i++) {
384
+ neededLines.set(range.startLine + i, lines[i]);
286
385
  }
287
386
  }
288
387
 
289
- // Compute line numbers in the original file for diff generation (O(log n) via binary search)
290
- // matchIdx is in the modified content; subtract cumulative offset to map back to original
291
- const origMatchIdx = matchIdx - cumulativeOffset;
292
- const oldStartLine = lineAtOffset(lineOffsets, origMatchIdx);
293
- const oldEndLine = lineAtOffset(lineOffsets, origMatchIdx + oldNorm.length - 1);
388
+ const fileDiff = generateLocalDiff(displayPath, cleanReplacements, neededLines, totalLines);
389
+ if (result.diff) {
390
+ result.diff += "\n" + fileDiff;
391
+ } else {
392
+ result.diff = fileDiff;
393
+ }
294
394
 
295
- // Apply replacement
296
- content =
297
- content.substring(0, matchIdx) +
298
- newNorm +
299
- content.substring(matchIdx + oldNorm.length);
395
+ const finalContent = restoreLineEndings(content, lineEnding);
396
+ if (lineEnding === "\r\n" && rawContent.includes("\r\n")) {
397
+ result.warnings.push(`${displayPath}: CRLF line endings were normalized to LF during editing.`);
398
+ }
399
+
400
+ fs.writeFileSync(absPath, finalContent, "utf8");
401
+ result.modified.push(displayPath);
402
+ result.replacements.set(displayPath, cleanReplacements);
403
+ return;
404
+ }
405
+
406
+ // ═══════════════════════════════════════════════════════════════════
407
+ // Phase 2: Conflict detection — sort by position, check for overlaps
408
+ // ═══════════════════════════════════════════════════════════════════
409
+
410
+ const sorted = [...planned].sort((a, b) => a.matchIdx - b.matchIdx);
411
+
412
+ for (let i = 0; i < sorted.length - 1; i++) {
413
+ const cur = sorted[i]!;
414
+ const next = sorted[i + 1]!;
415
+ const curEnd = cur.matchIdx + cur.oldNorm.length;
416
+ if (curEnd > next.matchIdx) {
417
+ const curStartLine = lineAtOffset(lineOffsets, cur.matchIdx);
418
+ const curEndLine = lineAtOffset(lineOffsets, curEnd - 1);
419
+ const nextStartLine = lineAtOffset(lineOffsets, next.matchIdx);
420
+ const overlapEnd = Math.min(curEnd, next.matchIdx + next.oldNorm.length);
421
+ const overlapEndLine = lineAtOffset(lineOffsets, overlapEnd - 1);
422
+ throw new ApplyError(
423
+ `Edits target overlapping regions in ${displayPath}: ` +
424
+ `edit targeting lines ${curStartLine}-${curEndLine} overlaps with ` +
425
+ `edit targeting lines ${nextStartLine}-${overlapEndLine}. ` +
426
+ `Split overlapping edits into separate patch calls.`
427
+ );
428
+ }
429
+ }
300
430
 
301
- // Track the offset shift for subsequent edits
302
- cumulativeOffset += newNorm.length - oldNorm.length;
431
+ // ═══════════════════════════════════════════════════════════════════
432
+ // Phase 3: One-shot assembly — splice replacements into final content
433
+ // ═══════════════════════════════════════════════════════════════════
303
434
 
304
- // Compute new_str line numbers in the result
305
- const newStartLine = charOffsetToLine(content, matchIdx);
306
- const newEndLine = charOffsetToLine(content, matchIdx + newNorm.length - 1);
435
+ let content = "";
436
+ let cursor = 0;
437
+ const replacements: ReplacementInfo[] = [];
438
+ const neededRanges: LineRange[] = [];
439
+
440
+ for (const p of sorted) {
441
+ // Copy original content up to this edit
442
+ content += originalContent.substring(cursor, p.matchIdx);
443
+
444
+ // Record where new_str lands in the assembled content
445
+ const newStartIdx = content.length;
446
+ content += p.newNorm;
447
+ const newEndIdx = content.length - 1;
448
+
449
+ // Compute line numbers (original file coordinates for old, result for new)
450
+ const oldStartLine = lineAtOffset(lineOffsets, p.matchIdx);
451
+ const oldEndLine = lineAtOffset(lineOffsets, p.matchIdx + p.oldNorm.length - 1);
452
+ const newStartLine = charOffsetToLine(content, newStartIdx);
453
+ const newEndLine = charOffsetToLine(content, newEndIdx);
307
454
 
308
- // Record replacement info
309
455
  replacements.push({
310
456
  oldStartLine,
311
457
  oldEndLine,
312
458
  newStartLine,
313
459
  newEndLine,
314
- oldLines: oldNorm.split("\n").filter((l, i, arr) => !(i === arr.length - 1 && l === "")),
315
- newLines: newNorm.split("\n").filter((l, i, arr) => !(i === arr.length - 1 && l === "")),
316
- anchor: displayAnchor ? displayAnchor.split("\n")[0] : undefined,
317
- anchorMissing,
460
+ oldLines: p.oldNorm.split("\n").filter((l, i, arr) => !(i === arr.length - 1 && l === "")),
461
+ newLines: p.newNorm.split("\n").filter((l, i, arr) => !(i === arr.length - 1 && l === "")),
462
+ anchor: p.displayAnchor ? p.displayAnchor.split("\n")[0] : undefined,
463
+ anchorMissing: p.anchorMissing,
318
464
  });
319
465
 
320
- // Collect context range for this edit
321
466
  neededRanges.push({
322
467
  startLine: Math.max(1, oldStartLine - CONTEXT_LINES),
323
468
  endLine: Math.min(totalLines, oldEndLine + CONTEXT_LINES),
324
469
  });
470
+
471
+ cursor = p.matchIdx + p.oldNorm.length;
325
472
  }
326
473
 
327
- // Generate diff using only needed context lines (no full-file split)
474
+ // Copy trailing original content
475
+ content += originalContent.substring(cursor);
476
+
477
+ // ═══════════════════════════════════════════════════════════════════
478
+ // Diff generation
479
+ // ═══════════════════════════════════════════════════════════════════
480
+
328
481
  const mergedRanges = mergeRanges(neededRanges);
329
- const currentLineOffsets = buildLineOffsets(content);
482
+ const originalLineOffsets = buildLineOffsets(originalContent);
330
483
  const neededLines: Map<number, string> = new Map();
331
484
  for (const range of mergedRanges) {
332
- const lines = extractLineRange(content, currentLineOffsets, range.startLine, range.endLine);
485
+ const lines = extractLineRange(originalContent, originalLineOffsets, range.startLine, range.endLine);
333
486
  for (let i = 0; i < lines.length; i++) {
334
487
  neededLines.set(range.startLine + i, lines[i]);
335
488
  }
336
489
  }
337
490
 
338
- // Build diff for this file and append to result
339
491
  const fileDiff = generateLocalDiff(displayPath, replacements, neededLines, totalLines);
340
492
  if (result.diff) {
341
493
  result.diff += "\n" + fileDiff;
@@ -346,7 +498,6 @@ async function applyEdits(
346
498
  // Restore line endings
347
499
  const finalContent = restoreLineEndings(content, lineEnding);
348
500
 
349
- // Warn if line endings were normalized (CRLF → LF)
350
501
  if (lineEnding === "\r\n" && rawContent.includes("\r\n")) {
351
502
  result.warnings.push(`${displayPath}: CRLF line endings were normalized to LF during editing.`);
352
503
  }
@@ -560,14 +711,17 @@ interface ChunkAnchor {
560
711
  function getChunkAnchors(chunk: ReplacementChunk): ChunkAnchor[] {
561
712
  const byText = new Map<string, ChunkAnchor>();
562
713
  for (const rep of chunk.reps) {
563
- const text = rep.anchor?.trim();
564
- if (!text) continue;
565
- const existing = byText.get(text);
566
- if (!existing) {
567
- byText.set(text, { text, missing: Boolean(rep.anchorMissing) });
568
- } else if (!rep.anchorMissing) {
569
- // If any replacement successfully used this anchor, do not mark it missing.
570
- existing.missing = false;
714
+ const raw = rep.anchor?.trim();
715
+ if (!raw) continue;
716
+ // Support \n-separated anchors from collapsed sequential replacements
717
+ const texts = raw.includes("\n") ? raw.split("\n").map(s => s.trim()).filter(Boolean) : [raw];
718
+ for (const text of texts) {
719
+ const existing = byText.get(text);
720
+ if (!existing) {
721
+ byText.set(text, { text, missing: Boolean(rep.anchorMissing) });
722
+ } else if (!rep.anchorMissing) {
723
+ existing.missing = false;
724
+ }
571
725
  }
572
726
  }
573
727
  return [...byText.values()];
@@ -795,6 +949,76 @@ function charOffsetToLine(content: string, offset: number): number {
795
949
  /**
796
950
  * Generate diff using only the needed lines (partial file context).
797
951
  */
952
+ /** Collapse chained-edit replacements (where out[i] === in[i+1]) into
953
+ * net-change replacements showing only the net effect (original→final). */
954
+ function collapseSequentialReplacements(
955
+ reps: ReplacementInfo[],
956
+ ): ReplacementInfo[] {
957
+ const collapsed: ReplacementInfo[] = [];
958
+ let i = 0;
959
+ while (i < reps.length) {
960
+ const start = reps[i]!;
961
+ let merged: ReplacementInfo = {
962
+ ...start,
963
+ newStartLine: start.oldStartLine,
964
+ newEndLine: start.oldStartLine + start.newLines.length - 1,
965
+ };
966
+
967
+ const anchors: string[] = [];
968
+ const seenAnchors = new Set<string>();
969
+ const addAnchor = (raw?: string) => {
970
+ if (!raw) return;
971
+ for (const text of raw.split("\n").map(s => s.trim()).filter(Boolean)) {
972
+ if (seenAnchors.has(text)) continue;
973
+ seenAnchors.add(text);
974
+ anchors.push(text);
975
+ }
976
+ };
977
+ addAnchor(start.anchor);
978
+
979
+ let j = i + 1;
980
+ while (j < reps.length) {
981
+ const next = reps[j]!;
982
+ // Merge chained edits when next edit's input matches merged output.
983
+ // We allow slightly shifted line numbers because sequential edits can
984
+ // change string lengths before we compute displayed line ranges.
985
+ if (!(linesEqual(merged.newLines, next.oldLines) && next.oldStartLine <= merged.oldEndLine + 1)) {
986
+ break;
987
+ }
988
+ addAnchor(next.anchor);
989
+ merged = {
990
+ // Keep the ORIGINAL region from the first replacement in the chain.
991
+ // Later chained replacements may have shifted line numbers, but the
992
+ // net diff should still point at the original file region.
993
+ oldStartLine: merged.oldStartLine,
994
+ oldEndLine: merged.oldEndLine,
995
+ newStartLine: merged.oldStartLine,
996
+ newEndLine: merged.oldStartLine + next.newLines.length - 1,
997
+ oldLines: merged.oldLines,
998
+ newLines: next.newLines,
999
+ anchor: undefined,
1000
+ anchorMissing: merged.anchorMissing || next.anchorMissing,
1001
+ };
1002
+ j++;
1003
+ }
1004
+
1005
+ collapsed.push({
1006
+ ...merged,
1007
+ anchor: anchors.length > 0 ? anchors.join("\n") : undefined,
1008
+ });
1009
+ i = j;
1010
+ }
1011
+ return collapsed;
1012
+ }
1013
+
1014
+ function linesEqual(a: string[], b: string[]): boolean {
1015
+ if (a.length !== b.length) return false;
1016
+ for (let i = 0; i < a.length; i++) {
1017
+ if (a[i] !== b[i]) return false;
1018
+ }
1019
+ return true;
1020
+ }
1021
+
798
1022
  function generateLocalDiff(
799
1023
  filePath: string,
800
1024
  reps: ReplacementInfo[],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decorated-pi",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "decorated-pi is a practical enhancement pack for pi — smarter tools that are token efficient and cache friendly.",
5
5
  "keywords": [
6
6
  "pi",