pi-hashline-edit-pro 2.8.3 → 2.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "2.8.3",
3
+ "version": "2.8.4",
4
4
  "type": "module",
5
5
  "description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 3-char hash (A-Za-z0-9) that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
6
6
  "main": "index.ts",
@@ -1,2 +1,2 @@
1
1
  - `undo_last_change`: only the last `replace`/`insert` per file can be undone; a successful `write` clears it, so call immediately after a bad diff (look for `-anchor│` lines you wanted to keep).
2
- - `undo_last_change`: if the file changed after the edit you get `[E_UNDO_STALE]` — the record is kept, so undo again after the file matches the edited state; if the file was deleted it is restored.
2
+ - `undo_last_change`: if the file changed after the edit you get `[E_UNDO_STALE]` — nothing is reverted and the record is kept; do not modify the file to force an undo, verify with `read` and stop; if the file was deleted it is restored.
@@ -1 +1 @@
1
- Undo the last `replace` or `insert` on a file, restoring previous content, BOM and line endings. Use after a bad edit when the diff showed wrong lines removed. If the file was deleted, it is restored; if changed elsewhere, you get `[E_UNDO_STALE]` and the record is kept so you can retry after restoring the edited state. If output says truncated, use `read` to see full file.
1
+ Undo the last `replace` or `insert` on a file, restoring previous content, BOM and line endings. Use after a bad edit when the diff showed wrong lines removed. If the file was deleted, it is restored; if changed elsewhere, you get `[E_UNDO_STALE]`, nothing is reverted, and the record is kept do not modify the file to make an undo possible; call `read` to verify the current state and stop. If output says truncated, use `read` to see full file.
package/src/commit.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PipelineResult } from "./replace";
2
- import { abortIf } from "./utils";
2
+ import { abortIf, clipLine } from "./utils";
3
3
  import { buildChanged, buildNoop, type RMeta, type TResult } from "./replace-response";
4
4
  import { saveUndo } from "./replace-undo";
5
5
  import { safeSnapId } from "./file-reader";
@@ -22,6 +22,12 @@ export interface CommitMeta {
22
22
  onNoopDedup?: () => void;
23
23
  }
24
24
 
25
+ function boundaryDedupWarning(lineTexts: string[]): string {
26
+ const quoted = lineTexts.map((line) => `"${clipLine(line, 80)}"`).join(", ");
27
+ const plural = lineTexts.length > 1;
28
+ return `Boundary dedup: ${quoted} already ${plural ? "exist" : "exists"} next to the edited range, so ${plural ? "they were" : "it was"} not added again. Use insert only if you truly want ${plural ? "duplicates" : "a duplicate"}.`;
29
+ }
30
+
25
31
  export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promise<TResult> {
26
32
  const { path, absolutePath, mutationTargetPath, signal } = meta;
27
33
  const warnings = [...(meta.prefixWarnings ?? []), ...pipe.warnings];
@@ -54,6 +60,9 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
54
60
  "Non-UTF-8 bytes were shown as U+FFFD; this edit rewrote the file as UTF-8.",
55
61
  );
56
62
  }
63
+ if (pipe.boundaryRemovedLineTexts.length > 0) {
64
+ warnings.push(boundaryDedupWarning(pipe.boundaryRemovedLineTexts));
65
+ }
57
66
 
58
67
  abortIf(signal);
59
68
  const undo = await saveUndo(mutationTargetPath, {
@@ -53,6 +53,23 @@ function parseRef(ref: string): Anchor {
53
53
 
54
54
  export const parseHashRef = parseRef;
55
55
 
56
+ const JSON_ENVELOPE_RE = /^\s*\["(.*)"\]\.\s*$/;
57
+
58
+ function unwrapJsonEnvelope(line: string, warnings?: string[]): string {
59
+ const match = line.match(JSON_ENVELOPE_RE);
60
+ if (!match) return line;
61
+ const withoutDot = line.trim().slice(0, -1);
62
+ try {
63
+ JSON.parse(withoutDot);
64
+ return line;
65
+ } catch {
66
+ warnings?.push(
67
+ '[E_BAD_SHAPE] Unwrapped JSON array syntax from a replacement_lines element.',
68
+ );
69
+ return match[1]!;
70
+ }
71
+ }
72
+
56
73
  export function parseText(edit: string[], warnings?: string[]): string[] {
57
74
  if (!Array.isArray(edit) || edit.some((line) => typeof line !== "string")) {
58
75
  throw new Error(NEW_CONTENT_NOT_ARRAY_MSG);
@@ -60,13 +77,14 @@ export function parseText(edit: string[], warnings?: string[]): string[] {
60
77
  const out: string[] = [];
61
78
  let split = false;
62
79
  for (const line of edit) {
63
- const normalized = line.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
64
- if (normalized !== line) split = true;
80
+ const unwrapped = unwrapJsonEnvelope(line, warnings);
81
+ const normalized = unwrapped.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
82
+ if (normalized !== unwrapped) split = true;
65
83
  out.push(...normalized.split("\n"));
66
84
  }
67
85
  if (split) {
68
86
  warnings?.push(
69
- "[E_BAD_SHAPE] Autocorrected: split embedded newlines in replacement_lines into separate lines.",
87
+ "[E_BAD_SHAPE] replacement_lines contained embedded newlines; split into one line each.",
70
88
  );
71
89
  }
72
90
  return out;
@@ -195,7 +195,7 @@ export function stripAnchorRow(
195
195
  : match[1] === "-"
196
196
  ? 'leading "-" marker'
197
197
  : '"anchor│" prefix';
198
- warnings?.push(`[E_BAD_REF] Stripped ${marker} from ${entryLabel} "${trimmed}".`);
198
+ warnings?.push(`[E_BAD_REF] Stripped ${marker} from ${entryLabel} "${clipLine(trimmed, 48)}".`);
199
199
  return match[2]!;
200
200
  }
201
201
 
@@ -240,13 +240,8 @@ export function stripBarePrefixes(
240
240
  const locations = stripped
241
241
  .map((s) => `replacement_lines line ${s.lineIndex + 1}`)
242
242
  .join(", ");
243
- const matchedCount = stripped.filter((s) => s.matched).length;
244
- const guidance =
245
- matchedCount === 0
246
- ? " Verify it was pasted from read output."
247
- : "";
248
243
  warnings.push(
249
- `[E_BARE_HASH_PREFIX] Stripped "anchor│" prefix from ${locations}.${guidance}`
244
+ `[E_BARE_HASH_PREFIX] Stripped "anchor│" prefix from ${locations}.`
250
245
  );
251
246
  return { ...edit, content_lines: contentLines };
252
247
  }
@@ -290,7 +285,7 @@ export function swapReversedRanges(
290
285
  return edit;
291
286
  }
292
287
  warnings.push(
293
- `[E_BAD_OP] Autocorrected: remove_from/remove_to were reversed; swapped them.`
288
+ `[E_BAD_OP] Swapped reversed remove_from/remove_to.`
294
289
  );
295
290
  return { ...edit, hash_bounds: [endRef, startRef] as [Anchor, Anchor] };
296
291
  }
@@ -100,7 +100,7 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
100
100
  : "The edit produced identical content.";
101
101
  const dedupNote =
102
102
  boundaryRemovedLines !== undefined && boundaryRemovedLines > 0
103
- ? `\nBoundary dedup removed ${boundaryRemovedLines} line(s) from the replacement. Send the same edit again to apply it literally.`
103
+ ? `\nBoundary dedup removed ${boundaryRemovedLines} line(s); resend the same edit to apply it literally.`
104
104
  : "";
105
105
 
106
106
  const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}${dedupNote}${warnBlock(warnings)}`;
@@ -146,7 +146,7 @@ export function regUndo(pi: ExtensionAPI): void {
146
146
  content: [
147
147
  {
148
148
  type: "text",
149
- text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file changed after the edit. The undo record is kept; once the file matches the edited state again, undo_last_change will succeed. Call read() to inspect the current state.`
149
+ text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file was modified after the edit, so nothing was reverted. The current content already contains your applied edit plus that external change and is most likely the correct state. Do not modify the file to make an undo possible and do not revert your own edit. The undo record is kept. Call read() to verify the current state, then stop.`
150
150
  },
151
151
  ],
152
152
  isError: true,
package/src/replace.ts CHANGED
@@ -63,6 +63,7 @@ export interface PipelineResult {
63
63
  totalRemovedLines: number;
64
64
  hadBoundaryDedup: boolean;
65
65
  boundaryRemovedLines: number;
66
+ boundaryRemovedLineTexts: string[];
66
67
  identity: FileIdentity;
67
68
  }
68
69
 
@@ -91,7 +92,7 @@ async function resolveMissingPath(
91
92
  if (matches.length === 1) {
92
93
  return {
93
94
  path: matches[0]!,
94
- warning: `[E_BAD_SHAPE] Autocorrected: missing "path" resolved to ${matches[0]}.`,
95
+ warning: `[E_BAD_SHAPE] Missing "path" resolved to ${matches[0]}.`,
95
96
  };
96
97
  }
97
98
  if (matches.length > 1) {
@@ -226,6 +227,7 @@ export async function execPipeline(
226
227
  totalRemovedLines,
227
228
  hadBoundaryDedup: (anchorResult.autoFixes?.length ?? 0) > 0,
228
229
  boundaryRemovedLines: anchorResult.autoFixes?.length ?? 0,
230
+ boundaryRemovedLineTexts: anchorResult.autoFixes?.map((fix) => fix.removedLine) ?? [],
229
231
  identity,
230
232
  };
231
233
  }