pi-hashline-edit-pro 2.8.3 → 3.0.0
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 +24 -24
- package/package.json +2 -2
- package/prompts/grep-guidelines.md +3 -3
- package/prompts/grep.md +1 -1
- package/prompts/insert-guidelines.md +1 -3
- package/prompts/insert-snippet.md +1 -1
- package/prompts/insert.md +1 -1
- package/prompts/read-guidelines.md +2 -2
- package/prompts/read.md +1 -1
- package/prompts/replace-guidelines.md +4 -5
- package/prompts/replace-snippet.md +1 -1
- package/prompts/replace.md +1 -1
- package/prompts/undo-last-change-guidelines.md +1 -2
- package/prompts/undo-last-change.md +1 -1
- package/src/commit.ts +10 -1
- package/src/config.ts +1 -1
- package/src/constants.ts +1 -1
- package/src/grep.ts +5 -5
- package/src/hashline/alphabet.ts +30 -7
- package/src/hashline/anchor-table.json +1 -0
- package/src/hashline/hash.ts +6 -28
- package/src/hashline/parse.ts +29 -11
- package/src/hashline/resolve.ts +6 -11
- package/src/insert.ts +6 -6
- package/src/payload-contract.ts +5 -5
- package/src/replace-response.ts +1 -1
- package/src/replace-undo.ts +1 -1
- package/src/replace.ts +3 -1
- package/src/utils.ts +1 -1
package/src/hashline/hash.ts
CHANGED
|
@@ -7,36 +7,20 @@ import {
|
|
|
7
7
|
persistSnapshot,
|
|
8
8
|
} from "../hash-store";
|
|
9
9
|
import { xxh32, initHasher } from "./hasher";
|
|
10
|
-
import { HASH_LEN,
|
|
11
|
-
export { initHasher, HASH_LEN,
|
|
10
|
+
import { HASH_LEN, ANCHOR_COUNT, anchorAt, anchorIndex, HASH_CLASS, HASH_RUN } from "./alphabet";
|
|
11
|
+
export { initHasher, HASH_LEN, HASH_CLASS, HASH_RUN };
|
|
12
12
|
|
|
13
13
|
export const ANCHOR_LEN = HASH_LEN;
|
|
14
14
|
|
|
15
15
|
export const HASH_SEP = "│";
|
|
16
16
|
|
|
17
|
-
export const HASH_SPACE =
|
|
17
|
+
export const HASH_SPACE = ANCHOR_COUNT;
|
|
18
18
|
export const MAX_HASH_LINES = HASH_SPACE;
|
|
19
19
|
|
|
20
|
-
export const HASH_PROBE_STRIDE =
|
|
21
|
-
|
|
22
|
-
function idxToHash(idx: number): string {
|
|
23
|
-
let out = "";
|
|
24
|
-
for (let j = 0; j < HASH_LEN; j++) {
|
|
25
|
-
out = ALPH[idx % ALPH.length]! + out;
|
|
26
|
-
idx = Math.floor(idx / ALPH.length);
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const hashCache = new Map<number, string>();
|
|
20
|
+
export const HASH_PROBE_STRIDE = 571;
|
|
32
21
|
|
|
33
22
|
function hashAt(idx: number): string {
|
|
34
|
-
|
|
35
|
-
if (hash === undefined) {
|
|
36
|
-
hash = idxToHash(idx);
|
|
37
|
-
hashCache.set(idx, hash);
|
|
38
|
-
}
|
|
39
|
-
return hash;
|
|
23
|
+
return anchorAt(idx);
|
|
40
24
|
}
|
|
41
25
|
|
|
42
26
|
export const HL_PREFIX_PLUS_RE = new RegExp(
|
|
@@ -182,13 +166,7 @@ export async function lineHashes(
|
|
|
182
166
|
}
|
|
183
167
|
|
|
184
168
|
function hashToIndex(hash: string): number {
|
|
185
|
-
|
|
186
|
-
for (let j = 0; j < HASH_LEN; j++) {
|
|
187
|
-
const charIdx = ALPH.indexOf(hash[j]!);
|
|
188
|
-
if (charIdx < 0) return -1;
|
|
189
|
-
idx = idx * ALPH.length + charIdx;
|
|
190
|
-
}
|
|
191
|
-
return idx;
|
|
169
|
+
return anchorIndex(hash);
|
|
192
170
|
}
|
|
193
171
|
|
|
194
172
|
function nearestNew(
|
package/src/hashline/parse.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ANCHOR_LEN,
|
|
3
|
-
ALPH_RE,
|
|
4
3
|
HASH_CLASS,
|
|
5
4
|
} from "./hash";
|
|
5
|
+
import { ALPH_RE } from "./alphabet";
|
|
6
6
|
import { NEW_CONTENT_NOT_ARRAY_MSG } from "../constants";
|
|
7
7
|
|
|
8
8
|
const HASH_EXTRACT_RE = new RegExp(HASH_CLASS);
|
|
@@ -13,11 +13,11 @@ function diagRef(ref: string): string {
|
|
|
13
13
|
const trimmed = ref.trim();
|
|
14
14
|
|
|
15
15
|
if (!trimmed.length) {
|
|
16
|
-
return `[E_BAD_REF] Invalid anchor. Expected a
|
|
16
|
+
return `[E_BAD_REF] Invalid anchor. Expected a 4-char alphanumeric anchor (e.g. "Hasu").`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
if (/^\d+/.test(trimmed)) {
|
|
20
|
-
return `[E_BAD_REF] Invalid anchor. Use the anchor alone (e.g. "
|
|
20
|
+
return `[E_BAD_REF] Invalid anchor. Use the anchor alone (e.g. "Hasu"): no line numbers or trailing content.`;
|
|
21
21
|
}
|
|
22
22
|
if (trimmed.includes("│") && trimmed.includes("\n")) {
|
|
23
23
|
const lines = trimmed.split(/\r?\n/);
|
|
@@ -26,16 +26,16 @@ function diagRef(ref: string): string {
|
|
|
26
26
|
const hashRe = HASH_EXTRACT_RE;
|
|
27
27
|
const firstMatch = first.match(hashRe);
|
|
28
28
|
const lastMatch = last.match(hashRe);
|
|
29
|
-
const firstHash = firstMatch?.[0] ?? "
|
|
30
|
-
const lastHash = lastMatch?.[0] ?? "
|
|
29
|
+
const firstHash = firstMatch?.[0] ?? "Hasu";
|
|
30
|
+
const lastHash = lastMatch?.[0] ?? "Hasu";
|
|
31
31
|
const preview = first.slice(0, 60);
|
|
32
|
-
return `[E_BAD_REF] Invalid anchor — remove_from and remove_to must each be a single bare
|
|
32
|
+
return `[E_BAD_REF] Invalid anchor — remove_from and remove_to must each be a single bare 4-char hash (e.g. "Hasu"), not a block with HASH│content. Received ${lines.length} lines starting "${preview}…" — use only the first hash "${firstHash}" as remove_from and "${lastHash}" as remove_to, and put the new content (without HASH│) in replacement_lines.`;
|
|
33
33
|
}
|
|
34
34
|
if (trimmed.includes("│")) {
|
|
35
|
-
return `[E_BAD_REF] Invalid anchor "${trimmed}": use only the
|
|
35
|
+
return `[E_BAD_REF] Invalid anchor "${trimmed}": use only the 4-char anchor, drop everything from "│" onward.`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a
|
|
38
|
+
return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a 4-char alphanumeric anchor (e.g. "Hasu").`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function parseRef(ref: string): Anchor {
|
|
@@ -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
|
|
64
|
-
|
|
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]
|
|
87
|
+
"[E_BAD_SHAPE] replacement_lines contained embedded newlines; split into one line each.",
|
|
70
88
|
);
|
|
71
89
|
}
|
|
72
90
|
return out;
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -159,12 +159,12 @@ function assertItem(edit: Record<string, unknown>): void {
|
|
|
159
159
|
|
|
160
160
|
if ("remove_from" in edit && typeof edit.remove_from !== "string") {
|
|
161
161
|
throw new Error(
|
|
162
|
-
`[E_BAD_SHAPE] Field "remove_from" must be an anchor string (
|
|
162
|
+
`[E_BAD_SHAPE] Field "remove_from" must be an anchor string (4-char anchor).`,
|
|
163
163
|
);
|
|
164
164
|
}
|
|
165
165
|
if ("remove_to" in edit && typeof edit.remove_to !== "string") {
|
|
166
166
|
throw new Error(
|
|
167
|
-
`[E_BAD_SHAPE] Field "remove_to" must be an anchor string (
|
|
167
|
+
`[E_BAD_SHAPE] Field "remove_to" must be an anchor string (4-char anchor).`,
|
|
168
168
|
);
|
|
169
169
|
}
|
|
170
170
|
if (!("replacement_lines" in edit)) {
|
|
@@ -175,7 +175,7 @@ function assertItem(edit: Record<string, unknown>): void {
|
|
|
175
175
|
}
|
|
176
176
|
if (typeof edit.remove_from !== "string" || typeof edit.remove_to !== "string") {
|
|
177
177
|
throw new Error(
|
|
178
|
-
`[E_BAD_SHAPE] The edit requires "remove_from" and "remove_to" anchor strings (
|
|
178
|
+
`[E_BAD_SHAPE] The edit requires "remove_from" and "remove_to" anchor strings (4-char anchors from read output).`,
|
|
179
179
|
);
|
|
180
180
|
}
|
|
181
181
|
}
|
|
@@ -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}
|
|
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]
|
|
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
|
}
|
package/src/insert.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function assertInsertReq(request: unknown): asserts request is InsertReq
|
|
|
31
31
|
throw new Error('[E_BAD_SHAPE] Insert request requires a non-empty "path" string.');
|
|
32
32
|
}
|
|
33
33
|
if (typeof request.anchor !== "string" || request.anchor.length === 0) {
|
|
34
|
-
throw new Error('[E_BAD_SHAPE] Insert request requires an "anchor" string (
|
|
34
|
+
throw new Error('[E_BAD_SHAPE] Insert request requires an "anchor" string (4-char anchor from read output).');
|
|
35
35
|
}
|
|
36
36
|
if (request.direction !== "before" && request.direction !== "after") {
|
|
37
37
|
throw new Error('[E_BAD_SHAPE] Insert request "direction" must be "before" or "after".');
|
|
@@ -49,23 +49,23 @@ const insertToolSchema = Type.Object(
|
|
|
49
49
|
}),
|
|
50
50
|
anchor: Type.String({
|
|
51
51
|
description:
|
|
52
|
-
'Bare
|
|
52
|
+
'Bare 4-char anchor from a read row like `Hasu│content`, never the content. A pasted `+Hasu│x` diff row or `anchor│` prefix is stripped with a warning. The anchor line is preserved; lines go after or before it.',
|
|
53
53
|
}),
|
|
54
54
|
direction: Type.Union(
|
|
55
55
|
[
|
|
56
|
-
Type.Literal("after"
|
|
57
|
-
Type.Literal("before"
|
|
56
|
+
Type.Literal("after"),
|
|
57
|
+
Type.Literal("before"),
|
|
58
58
|
],
|
|
59
59
|
{ description: '"after" or "before"' },
|
|
60
60
|
),
|
|
61
61
|
lines: Type.Array(
|
|
62
62
|
Type.String({
|
|
63
63
|
description:
|
|
64
|
-
"One line to insert
|
|
64
|
+
"One line to insert; never embed \\n inside an element.",
|
|
65
65
|
}),
|
|
66
66
|
{
|
|
67
67
|
description:
|
|
68
|
-
'
|
|
68
|
+
'One string per line; [""] is a blank line; never include the anchor line.',
|
|
69
69
|
}
|
|
70
70
|
),
|
|
71
71
|
},
|
package/src/payload-contract.ts
CHANGED
|
@@ -4,22 +4,22 @@ import { isRec, normalizeFilePath, rejectUnknownFields } from "./utils";
|
|
|
4
4
|
const replacementLinesSchema = Type.Array(
|
|
5
5
|
Type.String({
|
|
6
6
|
description:
|
|
7
|
-
"One replacement line
|
|
7
|
+
"One replacement line; never embed \\n inside an element.",
|
|
8
8
|
}),
|
|
9
9
|
{
|
|
10
10
|
description:
|
|
11
|
-
"
|
|
11
|
+
"One string per line. Use [] to delete the range.",
|
|
12
12
|
},
|
|
13
13
|
);
|
|
14
14
|
|
|
15
15
|
const removeFromSchema = Type.String({
|
|
16
16
|
description:
|
|
17
|
-
"Bare
|
|
17
|
+
"Bare 4-char anchor from a read row like `Hasu│content` (the leftmost column), never the row content. Marks the FIRST line to remove (inclusive)",
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const removeToSchema = Type.String({
|
|
21
21
|
description:
|
|
22
|
-
"Bare
|
|
22
|
+
"Bare 4-char anchor from a read row like `Hasu│content` (the leftmost column), never the row content. Marks the LAST line to remove (inclusive)",
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
export const editToolSchema = Type.Object(
|
|
@@ -27,7 +27,7 @@ export const editToolSchema = Type.Object(
|
|
|
27
27
|
path: Type.Optional(
|
|
28
28
|
Type.String({
|
|
29
29
|
description:
|
|
30
|
-
"Path to edit
|
|
30
|
+
"Path to edit; always provide it explicitly — it is only auto-resolved from the anchors as a fallback.",
|
|
31
31
|
}),
|
|
32
32
|
),
|
|
33
33
|
remove_from: removeFromSchema,
|
package/src/replace-response.ts
CHANGED
|
@@ -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)
|
|
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)}`;
|
package/src/replace-undo.ts
CHANGED
|
@@ -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
|
|
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]
|
|
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
|
}
|
package/src/utils.ts
CHANGED
|
@@ -114,7 +114,7 @@ export function getCached<K, V>(map: Map<K, V>, key: K, compute: (key: K) => V):
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
export function isHashRow(line: string): boolean {
|
|
117
|
-
return /^[A-Za-z0-9]{
|
|
117
|
+
return /^[A-Za-z0-9]{4}│/.test(line);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
function gutterWidth(max: number, fallback: number): number {
|