pi-hashline-edit-pro 0.12.2 → 0.12.3

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/README.md CHANGED
@@ -106,7 +106,7 @@ The post-edit diff (with `+`/`-` markers) is exposed to the host UI via `details
106
106
  - **Strict patch content.** If `content_lines` contains `+HASH│` display prefixes (or `-N ` numbered deletion rows), the edit is rejected with `[E_INVALID_PATCH]`. This narrowly guards against pasting the tool's own diff-preview rows back as content; standard unified-diff lines (`+x`, `-x`, ` x`, `@@ … @@`) are **not** rejected — they are written literally, since literal content must never be silently altered. Bare `HASH│` content (the first 4 chars of a `content_lines` entry looking like 3 base64 chars + `│`) is rejected with `[E_BARE_HASH_PREFIX]`. When the suspect's prefix happens to match a real file-line anchor, the error message flags that as strong evidence the model copied an anchor from the read output.
107
107
  - **Atomic writes.** Files are written via temp-file-then-rename to avoid corruption from interrupted writes. Symlink chains are resolved so the target file is updated without replacing the symlink. Hard-linked files are updated in place to preserve the shared inode. File permissions are preserved across atomic renames.
108
108
  - **Per-file mutation queue.** Edits queue by the canonical write target, so concurrent edits through different symlink paths still serialize onto the same underlying file.
109
- - **Boundary duplication warnings.** When the last line of a replacement matches the next surviving line (or the first line matches the preceding one), a warning is emitted. This catches a common LLM pattern where closing delimiters like `}`, `});`, or `} else {` are accidentally duplicated. The warning includes the surviving line's post-edit hash so the model can reference it in a follow-up edit. No autocorrection is applied — the duplicate stays in the file and the model decides whether to remove it. Raw line comparison (not trimmed) avoids false positives when indentation differs.
109
+ - **Boundary duplication warnings.** When the last line of a replacement matches the next surviving line (or the first line matches the preceding one), a warning is emitted. This catches a common LLM pattern where closing delimiters like `}`, `});`, or `} else {` are accidentally duplicated. The warning is a short header followed by a hashline-anchored window: the duplicated pair plus 2 lines of context before and after, shown as plain `HASH│content` rows with post-edit hashes. Seeing the duplication in context (rather than just being told a hash) is what prompts the model to act on it — and since the hashes are current and staleness is per-line, the model can remove the duplicate in one follow-up `replace` without re-reading. No autocorrection is applied — the duplicate stays in the file and the model decides whether to remove it. Raw line comparison (not trimmed) avoids false positives when indentation differs.
110
110
 
111
111
  ## Hashing
112
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 18-bit, perfect hashing)",
5
5
  "main": "index.ts",
6
6
  "repository": {
package/prompts/read.md CHANGED
@@ -7,6 +7,7 @@ HASH format:
7
7
 
8
8
  Pagination:
9
9
  - Large files return a truncated preview with a pagination hint (e.g. `[Showing lines 1-100 of 500. Use offset=101 to continue.]`). Call `read` again with `offset=N` to continue.
10
+ - Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}; output exceeding either is truncated. Pass `limit` to read fewer lines.
10
11
 
11
12
  File kinds:
12
13
  - Text files are returned as `HASH│content` lines.
@@ -211,6 +211,56 @@ function assemble(
211
211
  return result;
212
212
  }
213
213
 
214
+ /**
215
+ * Builds the boundary-duplication warning shown after an edit. A minimal header
216
+ * is followed by a hashline-anchored window: 2 lines of context before the
217
+ * duplicated pair, the pair itself, and 2 lines after. The window carries the
218
+ * post-edit hashes the model needs to remove the duplicate in a follow-up
219
+ * `replace` (no `read` round-trip required, since the hashes are current and
220
+ * staleness is per-line). Rows are plain `HASH│content` — no annotations.
221
+ */
222
+ function fmtBoundaryWarning(params: {
223
+ kind: "trailing" | "leading";
224
+ survivingContent: string;
225
+ matchIndex: number;
226
+ resultLines: string[];
227
+ resultHashes: string[];
228
+ }): string {
229
+ const header =
230
+ params.kind === "trailing"
231
+ ? "Boundary duplication (trailing): the last replacement line duplicated the next line. Delete the duplicate."
232
+ : "Boundary duplication (leading): the first replacement line duplicated the previous line. Delete the duplicate.";
233
+
234
+ // Locate the adjacent duplicated pair (two identical neighboring lines). The
235
+ // occurrence-based matchIndex usually lands inside the pair; when a file has
236
+ // several identical lines we pick the pair nearest matchIndex so the window
237
+ // frames the duplication this edit introduced, not an unrelated one.
238
+ let pairStart = -1;
239
+ let bestDist = Infinity;
240
+ for (let i = 0; i < params.resultLines.length - 1; i++) {
241
+ if (
242
+ params.resultLines[i] === params.survivingContent &&
243
+ params.resultLines[i + 1] === params.survivingContent
244
+ ) {
245
+ const dist = Math.abs(i - params.matchIndex);
246
+ if (dist < bestDist) {
247
+ bestDist = dist;
248
+ pairStart = i;
249
+ }
250
+ }
251
+ }
252
+ if (pairStart < 0) pairStart = params.matchIndex;
253
+
254
+ const winStart = Math.max(0, pairStart - 2);
255
+ const winEnd = Math.min(params.resultLines.length - 1, pairStart + 3);
256
+
257
+ const rows: string[] = [];
258
+ for (let i = winStart; i <= winEnd; i++) {
259
+ rows.push(`${params.resultHashes[i]}${HASH_SEP}${params.resultLines[i]}`);
260
+ }
261
+ return `${header}\n\n${rows.join("\n")}`;
262
+ }
263
+
214
264
  export function applyEdits(
215
265
  content: string,
216
266
  edits: import("./resolve").HEdit[],
@@ -284,16 +334,15 @@ export function applyEdits(
284
334
  }
285
335
  }
286
336
  if (matchIndex >= 0) {
287
- const hash = resultHashes[matchIndex];
288
- if (bw.kind === "trailing") {
289
- warnings.push(
290
- `Potential boundary duplication: the last line of the replacement (${JSON.stringify(bw.replacementLineContent)}) matches the next surviving line. Surviving line hash: ${hash}`
291
- );
292
- } else {
293
- warnings.push(
294
- `Potential boundary duplication: the first line of the replacement (${JSON.stringify(bw.replacementLineContent)}) matches the preceding surviving line. Surviving line hash: ${hash}`
295
- );
296
- }
337
+ warnings.push(
338
+ fmtBoundaryWarning({
339
+ kind: bw.kind,
340
+ survivingContent: bw.survivingLineContent,
341
+ matchIndex,
342
+ resultLines,
343
+ resultHashes,
344
+ }),
345
+ );
297
346
  }
298
347
  }
299
348