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 +1 -1
- package/package.json +1 -1
- package/prompts/read.md +1 -0
- package/src/hashline/apply.ts +59 -10
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
|
|
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
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.
|
package/src/hashline/apply.ts
CHANGED
|
@@ -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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|