pi-hashline-edit-pro 2.6.2 → 2.6.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 +3 -2
- package/package.json +1 -1
- package/src/boundary-bypass.ts +26 -0
- package/src/hashline/alphabet.ts +2 -0
- package/src/hashline/apply.ts +3 -1
- package/src/hashline/hash.ts +5 -5
- package/src/hashline/index.ts +1 -0
- package/src/hashline/resolve.ts +2 -2
- package/src/replace.ts +16 -1
package/README.md
CHANGED
|
@@ -96,8 +96,8 @@ One edit per call, with `remove_from`, `remove_to`, and `replacement_lines` at t
|
|
|
96
96
|
Notes:
|
|
97
97
|
|
|
98
98
|
- The request is checked before any file I/O, so a bad request never touches the file.
|
|
99
|
-
- Common copy-paste slips are fixed automatically and reported: a leftover `HASH│` prefix in `replacement_lines` or `remove_from`/`remove_to`, diff-preview rows pasted into the replacement, a reversed range, or a boundary line pasted twice. New lines that re-include a block adjacent to the range are stripped automatically when that block is unique in the file — the whole run is stripped as one unit (including repeated structural lines like `}`), so re-including an unchanged block next to the range never duplicates it. A missing `path` is resolved from the anchors when they uniquely identify a file in the hash store (reported as a warning); when the anchors match multiple known files the request is rejected with the candidate paths named. `file_path` works as an alias for `path` in all three tools.
|
|
100
|
-
- An edit that produces identical content reports `No changes made` and leaves the anchors alone.
|
|
99
|
+
- Common copy-paste slips are fixed automatically and reported: a leftover `HASH│` prefix (including a truncated or expanded prefix of up to 6 characters, e.g. `L3│` or `ab12│`) in `replacement_lines` or `remove_from`/`remove_to`, diff-preview rows pasted into the replacement, a reversed range, or a boundary line pasted twice. New lines that re-include a block adjacent to the range are stripped automatically when that block is unique in the file — the whole run is stripped as one unit (including repeated structural lines like `}`), so re-including an unchanged block next to the range never duplicates it. A missing `path` is resolved from the anchors when they uniquely identify a file in the hash store (reported as a warning); when the anchors match multiple known files the request is rejected with the candidate paths named. `file_path` works as an alias for `path` in all three tools.
|
|
100
|
+
- An edit that produces identical content reports `No changes made` and leaves the anchors alone. When such a noop happened because a boundary anti-duplication cut removed lines from the replacement (the cut blocked a line that duplicates the block next to the range from being added), the exact same replacement sent once more runs with the edge anti-duplication turned off for that single call and is applied literally — the duplicated lines are kept, and the result carries a `[E_BOUNDARY_BYPASS]` notice. The pending bypass is per file and keyed to that exact payload; any applied edit clears it.
|
|
101
101
|
- Every line in the removed range must match what was last shown to you. The extension records the `HASH│content` rows it serves — `read` output, the auto-read block after `write`, the `+HASH│`/` HASH│` rows of post-edit diffs (replace and undo), the current-range rows of `[E_RANGE_STALE]` feedback, and the context rows of stale/ambiguous-anchor feedback — and verifies the whole range against that record before writing. If an interior line changed on disk since it was shown (external editor, formatter-on-save, code generation) or was never shown, the edit is refused with `[E_RANGE_STALE]` and the current range is returned with fresh anchors, so the retry needs no `read`. Edits outside the served record are only possible for files that were never read (for example right after a `write` with auto-read disabled); once the file has been served, every replaced line must have been shown.
|
|
102
102
|
- After a successful edit you get the post-edit diff with fresh anchors, so you can keep editing without re-reading.
|
|
103
103
|
- Do not issue multiple replace calls on the same file in one message; parallel edits split attention across the post-edit diffs and removed lines are easy to miss. Verify each diff before the next edit on that file.
|
|
@@ -181,6 +181,7 @@ A no-op replace never changes the file, so anchors remain valid. On first run af
|
|
|
181
181
|
| `[E_UNDO_STALE]` | `undo_last_replace` refused: the file was modified or deleted after the last replace. |
|
|
182
182
|
| `[E_UNDO_UNAVAILABLE]` | Undo history could not be persisted to the hash store; the `replace` was refused and the file was left unchanged. |
|
|
183
183
|
| `[E_RANGE_STALE]` | A line in the replaced range no longer matches what was last shown (the file changed on disk, or the line was never shown). The edit was refused; the current range is returned with fresh anchors. |
|
|
184
|
+
| `[E_BOUNDARY_BYPASS]` | The boundary anti-duplication was turned off for one replace call (an identical replacement had previously been cut to a noop); the duplicate lines were applied literally. The dedup is restored for the next call. |
|
|
184
185
|
| `[E_FILE_TOO_LARGE]` | The file exceeds the 238,328-line hashline limit or the 100MB size limit. |
|
|
185
186
|
|
|
186
187
|
## Troubleshooting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/undo 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",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const boundaryBypassTracker = new Map<string, string>();
|
|
2
|
+
|
|
3
|
+
export function noopPayloadKey(
|
|
4
|
+
absolutePath: string,
|
|
5
|
+
removeFrom: string,
|
|
6
|
+
removeTo: string,
|
|
7
|
+
replacementLines: string[],
|
|
8
|
+
): string {
|
|
9
|
+
return JSON.stringify([absolutePath, removeFrom, removeTo, replacementLines]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function markBoundaryNoop(absolutePath: string, payload: string): void {
|
|
13
|
+
boundaryBypassTracker.set(absolutePath, payload);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function consumeBoundaryBypass(absolutePath: string, payload: string): boolean {
|
|
17
|
+
if (boundaryBypassTracker.get(absolutePath) === payload) {
|
|
18
|
+
boundaryBypassTracker.delete(absolutePath);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function clearBoundaryBypass(absolutePath: string): void {
|
|
25
|
+
boundaryBypassTracker.delete(absolutePath);
|
|
26
|
+
}
|
package/src/hashline/alphabet.ts
CHANGED
package/src/hashline/apply.ts
CHANGED
|
@@ -147,6 +147,7 @@ export function applyEdit(
|
|
|
147
147
|
precomputedHashes?: string[],
|
|
148
148
|
filePath?: string,
|
|
149
149
|
servedHashes?: ReadonlySet<string>,
|
|
150
|
+
skipBoundaryDedup?: boolean,
|
|
150
151
|
): {
|
|
151
152
|
content: string;
|
|
152
153
|
firstChangedLine: number | undefined;
|
|
@@ -188,7 +189,7 @@ export function applyEdit(
|
|
|
188
189
|
|
|
189
190
|
let resolved = initialResolved;
|
|
190
191
|
let autoFixes: AutoFix[] | undefined;
|
|
191
|
-
if (boundaryDups.length > 0) {
|
|
192
|
+
if (boundaryDups.length > 0 && !skipBoundaryDedup) {
|
|
192
193
|
autoFixes = [];
|
|
193
194
|
const correctedEdit: HEdit = {
|
|
194
195
|
...prefixFixed,
|
|
@@ -241,6 +242,7 @@ export function applyEdit(
|
|
|
241
242
|
firstChangedLine: undefined,
|
|
242
243
|
lastChangedLine: undefined,
|
|
243
244
|
...(warnings.length ? { warnings } : {}),
|
|
245
|
+
...(autoFixes ? { autoFixes } : {}),
|
|
244
246
|
noopEdit: { loc: spanResult.loc, currentContent: spanResult.currentContent },
|
|
245
247
|
};
|
|
246
248
|
}
|
package/src/hashline/hash.ts
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
upsertSnapshot,
|
|
7
7
|
} from "../hash-store";
|
|
8
8
|
import { xxh32, contentChecksum, initHasher } from "./hasher";
|
|
9
|
-
import { HASH_LEN, ALPH, ALPH_RE, HASH_CLASS } from "./alphabet";
|
|
10
|
-
export { initHasher, HASH_LEN, ALPH_RE, HASH_CLASS };
|
|
9
|
+
import { HASH_LEN, ALPH, ALPH_RE, HASH_CLASS, HASH_RUN } from "./alphabet";
|
|
10
|
+
export { initHasher, HASH_LEN, ALPH_RE, HASH_CLASS, HASH_RUN };
|
|
11
11
|
|
|
12
12
|
export const ANCHOR_LEN = HASH_LEN;
|
|
13
13
|
|
|
@@ -39,13 +39,13 @@ function hashAt(idx: number): string {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export const HL_PREFIX_PLUS_RE = new RegExp(
|
|
42
|
-
`^\\+${
|
|
42
|
+
`^\\+${HASH_RUN}│`,
|
|
43
43
|
);
|
|
44
44
|
export const HL_PREFIX_MINUS_RE = new RegExp(
|
|
45
|
-
`^-(?:${
|
|
45
|
+
`^-(?:${HASH_RUN}│| {${ANCHOR_LEN}}│)`,
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
-
export const HL_BARE_PREFIX_RE = new RegExp(`^\\s*(${
|
|
48
|
+
export const HL_BARE_PREFIX_RE = new RegExp(`^\\s*(${HASH_RUN})│`);
|
|
49
49
|
|
|
50
50
|
export function canon(line: string): string {
|
|
51
51
|
return line.replace(/\r/g, "").trimEnd();
|
package/src/hashline/index.ts
CHANGED
package/src/hashline/resolve.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { abortIf, rejectUnknownFields, firstNonEmptyIndex, lastNonEmptyIndex, clipLine } from "../utils";
|
|
2
|
-
import {
|
|
2
|
+
import { HASH_SEP, HASH_RUN, HL_BARE_PREFIX_RE, HL_PREFIX_PLUS_RE, HL_PREFIX_MINUS_RE, canon } from "./hash";
|
|
3
3
|
import { parseHashRef, parseText, type Anchor } from "./parse";
|
|
4
4
|
import { NEW_CONTENT_NOT_ARRAY_MSG, MAX_RANGE_STALE_LINES } from "../constants";
|
|
5
5
|
|
|
@@ -162,7 +162,7 @@ function assertItem(edit: Record<string, unknown>): void {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
const ANCHOR_ROW_RE = new RegExp(`^([+-]?)(${
|
|
165
|
+
const ANCHOR_ROW_RE = new RegExp(`^([+-]?)(${HASH_RUN})│`);
|
|
166
166
|
|
|
167
167
|
export function resEdit(edit: HTEdit, warnings?: string[]): HEdit {
|
|
168
168
|
assertItem(edit as Record<string, unknown>);
|
package/src/replace.ts
CHANGED
|
@@ -47,6 +47,7 @@ import { loadP, loadGuide } from "./prompts";
|
|
|
47
47
|
import { saveUndo } from "./replace-undo";
|
|
48
48
|
import { loadHashStore, findSnapshotPaths, type HashStore } from "./hash-store";
|
|
49
49
|
import { getServed, recordServedSafe, recordServedDiffSafe } from "./served";
|
|
50
|
+
import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
|
|
50
51
|
|
|
51
52
|
const replacementLinesSchema = Type.Array(
|
|
52
53
|
Type.String({
|
|
@@ -107,6 +108,7 @@ interface PipelineResult {
|
|
|
107
108
|
resultHashes: string[];
|
|
108
109
|
totalAddedLines: number;
|
|
109
110
|
totalRemovedLines: number;
|
|
111
|
+
hadBoundaryDedup: boolean;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
const PREVIEW_DEBOUNCE_MS = 150;
|
|
@@ -179,6 +181,7 @@ export interface ExecPipelineOptions {
|
|
|
179
181
|
signal?: AbortSignal;
|
|
180
182
|
store?: HashStore;
|
|
181
183
|
noPersist?: boolean;
|
|
184
|
+
skipBoundaryDedup?: boolean;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
function collectRemovedHashes(
|
|
@@ -252,6 +255,7 @@ export async function execPipeline(
|
|
|
252
255
|
originalHashes,
|
|
253
256
|
path,
|
|
254
257
|
served,
|
|
258
|
+
options?.skipBoundaryDedup,
|
|
255
259
|
);
|
|
256
260
|
} catch (error) {
|
|
257
261
|
if (options?.noPersist !== true) {
|
|
@@ -298,6 +302,7 @@ export async function execPipeline(
|
|
|
298
302
|
originalHashes,
|
|
299
303
|
totalAddedLines,
|
|
300
304
|
totalRemovedLines,
|
|
305
|
+
hadBoundaryDedup: (anchorResult.autoFixes?.length ?? 0) > 0,
|
|
301
306
|
};
|
|
302
307
|
}
|
|
303
308
|
|
|
@@ -477,6 +482,8 @@ export function buildToolDef(): ToolDef {
|
|
|
477
482
|
const path = normalizedParams.path;
|
|
478
483
|
const absolutePath = toCwd(path, ctx.cwd);
|
|
479
484
|
const mutationTargetPath = await resolveTarget(absolutePath);
|
|
485
|
+
const noopPayload = noopPayloadKey(mutationTargetPath, normalizedParams.remove_from, normalizedParams.remove_to, normalizedParams.replacement_lines);
|
|
486
|
+
const boundaryBypass = consumeBoundaryBypass(mutationTargetPath, noopPayload);
|
|
480
487
|
return withFileMutationQueue(mutationTargetPath, async () => {
|
|
481
488
|
abortIf(signal);
|
|
482
489
|
|
|
@@ -492,21 +499,28 @@ export function buildToolDef(): ToolDef {
|
|
|
492
499
|
firstChangedLine,
|
|
493
500
|
lastChangedLine,
|
|
494
501
|
resultHashes,
|
|
502
|
+
hadBoundaryDedup,
|
|
495
503
|
totalAddedLines,
|
|
496
504
|
totalRemovedLines,
|
|
497
505
|
} = await execPipeline(
|
|
498
506
|
normalizedParams,
|
|
499
507
|
ctx.cwd,
|
|
500
|
-
{ accessMode: constants.R_OK | constants.W_OK, signal },
|
|
508
|
+
{ accessMode: constants.R_OK | constants.W_OK, signal, skipBoundaryDedup: boundaryBypass },
|
|
501
509
|
);
|
|
502
510
|
|
|
503
511
|
if (resolution) {
|
|
504
512
|
warnings.unshift(resolution.warning);
|
|
505
513
|
}
|
|
514
|
+
if (boundaryBypass && originalNormalized !== result) {
|
|
515
|
+
warnings.push("[E_BOUNDARY_BYPASS] Boundary dedup was off for this call. Boundary dedup is now restored.");
|
|
516
|
+
}
|
|
506
517
|
|
|
507
518
|
const editsAttempted = 1;
|
|
508
519
|
if (originalNormalized === result) {
|
|
509
520
|
const noopSnapshotId = await safeSnapId(absolutePath, "noop edit");
|
|
521
|
+
if (hadBoundaryDedup) {
|
|
522
|
+
markBoundaryNoop(mutationTargetPath, noopPayload);
|
|
523
|
+
}
|
|
510
524
|
return buildNoop({
|
|
511
525
|
path,
|
|
512
526
|
noopEdit,
|
|
@@ -550,6 +564,7 @@ export function buildToolDef(): ToolDef {
|
|
|
550
564
|
await undo.restore();
|
|
551
565
|
throw error;
|
|
552
566
|
}
|
|
567
|
+
clearBoundaryBypass(mutationTargetPath);
|
|
553
568
|
const updatedSnapshotId = await safeSnapId(absolutePath, "post-edit");
|
|
554
569
|
|
|
555
570
|
const editMeta: RMeta = {
|