pi-hashline-edit-pro 0.6.0 → 0.7.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 -16
- package/index.ts +19 -4
- package/package.json +2 -2
- package/prompts/read.md +1 -1
- package/prompts/replace.md +11 -14
- package/src/hashline/apply.ts +1 -2
- package/src/hashline/hash.ts +10 -2
- package/src/hashline/parse.ts +3 -3
- package/src/hashline/resolve.ts +6 -23
- package/src/replace-response.ts +2 -2
- package/src/replace.ts +0 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pi-hashline-edit-pro
|
|
2
2
|
|
|
3
|
-
A [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that replaces the built-in `read` and `edit` tools with a hash-anchored line-replacing workflow. Strict semantics, no silent relocation, no autocorrection, no fuzzy fallback.
|
|
3
|
+
A [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that replaces the built-in `read` and `edit` tools with a hash-anchored line-replacing workflow. Strict semantics, no silent relocation, no autocorrection, no fuzzy fallback. 3-character content hashes over a 64-character URL-safe base64 alphabet give 18 bits of entropy per anchor, with perfect hashing (collision resolution) ensuring every line gets a unique anchor.
|
|
4
4
|
|
|
5
|
-
Fork of [pi-hashline-edit](https://github.com/RimuruW/pi-hashline-edit) by RimuruW. The strict-semantics policy is unchanged. This fork extends the upstream design in
|
|
5
|
+
Fork of [pi-hashline-edit](https://github.com/RimuruW/pi-hashline-edit) by RimuruW. The strict-semantics policy is unchanged. This fork extends the upstream design in three ways: a 3-character hash length with perfect hashing, an occurrence-aware discriminator that makes identical content at different positions hash to different values, and collision resolution that ensures unique anchors for every line in a file.
|
|
6
6
|
|
|
7
7
|
Every line returned by `read` carries a short content hash. Edits reference those hashes instead of raw text, so the tool can detect stale context and reject outdated changes before they reach the file.
|
|
8
8
|
|
|
@@ -10,10 +10,12 @@ Every line returned by `read` carries a short content hash. Edits reference thos
|
|
|
10
10
|
|
|
11
11
|
The original uses 2-character hashes of a 16-character alphabet, with the hash being a pure function of line content. That's 8 bits / 256 buckets, and two byte-identical lines (e.g. repeated `import` statements, repeated `}`) always share a hash because the hash is `xxHash32(content)`.
|
|
12
12
|
|
|
13
|
-
This fork makes
|
|
13
|
+
This fork makes three changes that compound:
|
|
14
14
|
|
|
15
|
-
1. **Bump hash length to
|
|
15
|
+
1. **Bump hash length to 3 characters** of the 64-char URL-safe base64 alphabet. That gives 18 bits / 262,144 buckets. With perfect hashing (collision resolution), every line in a file gets a unique anchor.
|
|
16
16
|
2. **Make the hash occurrence-aware.** The hash for line N is `xxHash32("C{occurrence}:{content}")` where `occurrence` is the running count of that content string earlier in the file. Two `import {...}` statements at different positions now hash to different values, so the model can target a specific occurrence without resorting to `offset` + a small `limit` window. All lines — including symbol-only lines like `}` — use the same occurrence-based discrimination.
|
|
17
|
+
3. **Perfect hashing (collision resolution).** When computing hashes for a file, if a line's base hash collides with an already-assigned hash, the hash is incremented (using a retry counter in the discriminator: `C{occurrence}:R{retry}`) until a unique hash is found. This ensures every line in a file gets a unique anchor, even with the shorter 3-character hash space.
|
|
18
|
+
|
|
17
19
|
## Installation
|
|
18
20
|
|
|
19
21
|
From npm:
|
|
@@ -32,7 +34,7 @@ pi install /path/to/pi-hashline-edit-pro
|
|
|
32
34
|
|
|
33
35
|
### `read` -- tagged line output
|
|
34
36
|
|
|
35
|
-
Text files are returned with a `HASH│content` prefix on every line. The line number is not part of the wire format, only the
|
|
37
|
+
Text files are returned with a `HASH│content` prefix on every line. The line number is not part of the wire format, only the 3-character hash followed by the `│` separator and the line content. Example output for the source below:
|
|
36
38
|
|
|
37
39
|
```js
|
|
38
40
|
function hello() {
|
|
@@ -43,12 +45,12 @@ function hello() {
|
|
|
43
45
|
would be returned as:
|
|
44
46
|
|
|
45
47
|
```text
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
0qH│function hello() {
|
|
49
|
+
szJ│ console.log("world");
|
|
50
|
+
_zl│}
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
- `HASH` is a
|
|
53
|
+
- `HASH` is a 3-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3`).
|
|
52
54
|
|
|
53
55
|
Optional parameters:
|
|
54
56
|
|
|
@@ -65,7 +67,7 @@ Replaces using the `HASH│content` anchors from `read` output to target lines p
|
|
|
65
67
|
{
|
|
66
68
|
"path": "src/main.ts",
|
|
67
69
|
"edits": [
|
|
68
|
-
{ "start": "
|
|
70
|
+
{ "start": "ve7", "end": "ve7", "lines": [" console.log('hashline');"] }
|
|
69
71
|
]
|
|
70
72
|
}
|
|
71
73
|
```
|
|
@@ -87,11 +89,13 @@ After a successful replace, the result text contains an `--- Anchors ---` block
|
|
|
87
89
|
|
|
88
90
|
### Auto-read after write
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
Auto-read is **disabled by default**. When enabled, after a successful `write` the extension automatically reads the file and appends a `--- Auto-read (hashline anchors) ---` block to the result. This gives the model immediate `HASH│content` anchors for the newly written file without requiring a separate `read` call. The workflow becomes:
|
|
91
93
|
|
|
92
94
|
1. `write` a file, result includes hashline anchors
|
|
93
95
|
2. `replace` using those anchors directly
|
|
94
96
|
|
|
97
|
+
Toggle at runtime with the `/toggle-auto-read` command. The current state persists for the session.
|
|
98
|
+
|
|
95
99
|
For large files (>2000 lines), the auto-read output is truncated with a pagination hint. Use `read` with `offset` to see more.
|
|
96
100
|
|
|
97
101
|
### Diff for the host
|
|
@@ -100,16 +104,16 @@ The post-edit diff (with `+`/`-` markers and new `HASH│content` anchors) is ex
|
|
|
100
104
|
|
|
101
105
|
## Design Decisions
|
|
102
106
|
|
|
103
|
-
- **Stale anchors fail.** A hash mismatch means the file has changed since the last `read`. The error
|
|
107
|
+
- **Stale anchors fail.** A hash mismatch means the file has changed since the last `read`. The error tells the model to call `read()` to get fresh anchors, then copy the 3-character HASH from each line into the next replace call.
|
|
104
108
|
- **No fallback relocation.** Mismatched anchors are never silently relocated to a "close enough" line. This trades convenience for correctness.
|
|
105
|
-
- **Strict patch content.** If `lines` contains `+HASH│` display prefixes (or `-N ` diff rows), the edit is rejected with `[E_INVALID_PATCH]`. Bare `HASH│` content (the first
|
|
109
|
+
- **Strict patch content.** If `lines` contains `+HASH│` display prefixes (or `-N ` diff rows), the edit is rejected with `[E_INVALID_PATCH]`. Bare `HASH│` content (the first 4 chars of a `lines` entry looking like 3 base64 chars + `│`) is also 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.
|
|
106
110
|
- **Legacy dialect rejected.** The native top-level `oldText`/`newText` (and `old_text`/`new_text`) dialect is rejected with `[E_LEGACY_SHAPE]`. The error message tells the model to call `read` first and send `{start:"<HASH>", end:"<HASH>", lines:[...]}`.
|
|
107
111
|
- **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
112
|
- **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
113
|
|
|
110
114
|
## Hashing
|
|
111
115
|
|
|
112
|
-
Hashes are computed with [xxhash-wasm](https://github.com/jungomi/xxhash-wasm) (xxHash32 via WebAssembly), then mapped to a
|
|
116
|
+
Hashes are computed with [xxhash-wasm](https://github.com/jungomi/xxhash-wasm) (xxHash32 via WebAssembly), then mapped to a 3-character string from the URL-safe base64 alphabet `A-Za-z0-9-_`. That's 64 distinct characters, 6 bits per position, 18 bits of entropy per anchor.
|
|
113
117
|
|
|
114
118
|
The alphabet is sized for an LLM consumer. The model tokenizes, it doesn't squint at pixel glyphs, so the human-readability heuristics used by smaller hand-curated alphabets (no G/L/I/O because they look like digits, no vowels so the hash doesn't accidentally spell a word, no hex digits so it can't be confused with `0xFF`) don't apply. The full 64 chars give maximum entropy per character, with case and digits included.
|
|
115
119
|
|
|
@@ -118,13 +122,15 @@ Hashes are occurrence-aware: a discriminator prefix is mixed into the xxHash inp
|
|
|
118
122
|
- `}` on line 5 and `}` on line 17 hash differently (1st vs 2nd occurrence of `}`).
|
|
119
123
|
- `import { foo } from 'bar';` on line 3 and the same string on line 47 hash differently (1st vs 2nd occurrence).
|
|
120
124
|
|
|
125
|
+
**Perfect hashing (collision resolution):** When computing hashes for a file, if a line's base hash collides with an already-assigned hash, the hash is incremented (using a retry counter in the discriminator: `C{occurrence}:R{retry}`) until a unique hash is found. This ensures every line in a file gets a unique anchor, even with the shorter 3-character hash space.
|
|
126
|
+
|
|
121
127
|
The runtime always precomputes the full per-line hash array for a file via `computeLineHashes(content)`, then looks up by line number during validation and during `read` / `replace` response formatting. There is no per-line recomputation that could disagree with what the model saw in its last read.
|
|
122
128
|
|
|
123
|
-
`HASH_LENGTH` and `HASH_ALPHABET` are constants at the top of `src/hashline/hash.ts`; bump the length to
|
|
129
|
+
`HASH_LENGTH` and `HASH_ALPHABET` are constants at the top of `src/hashline/hash.ts`; bump the length to 4 if you need even more entropy without collision resolution.
|
|
124
130
|
|
|
125
131
|
### Bare-prefix detector
|
|
126
132
|
|
|
127
|
-
With the `│` delimiter format, the bare-prefix detector regex `^\s*[A-Za-z0-9_\-]{
|
|
133
|
+
With the `│` delimiter format, the bare-prefix detector regex `^\s*[A-Za-z0-9_\-]{3}│` is highly specific. It only matches lines starting with exactly 3 base64 chars and `│`. This eliminates false positives from common code patterns like `init:`, `data:`, `else:`, etc. The detector rejects edit lines matching this pattern with `[E_BARE_HASH_PREFIX]` to prevent the model from accidentally pasting hash anchors into file content.
|
|
128
134
|
|
|
129
135
|
## Development
|
|
130
136
|
|
|
@@ -137,6 +143,8 @@ npm test
|
|
|
137
143
|
|
|
138
144
|
Set `PI_HASHLINE_DEBUG=1` to show an "active" notification at session start.
|
|
139
145
|
|
|
146
|
+
Set `PI_HASHLINE_AUTO_READ=1` to enable auto-read after write by default (can still be toggled at runtime with `/toggle-auto-read`).
|
|
147
|
+
|
|
140
148
|
## Credits
|
|
141
149
|
|
|
142
150
|
- [RimuruW](https://github.com/RimuruW) -- original `pi-hashline-edit` and the strict-semantics policy
|
package/index.ts
CHANGED
|
@@ -17,9 +17,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
17
17
|
pi.setActiveTools(active.filter((t) => t !== "edit"));
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
// Auto-read after write
|
|
21
|
-
//
|
|
20
|
+
// Auto-read after write state - controlled by PI_HASHLINE_AUTO_READ env var (default: disabled).
|
|
21
|
+
// Can be toggled at runtime via /toggle-auto-read command.
|
|
22
|
+
const autoReadValue = process.env.PI_HASHLINE_AUTO_READ;
|
|
23
|
+
let autoReadEnabled = autoReadValue === "1" || autoReadValue === "true";
|
|
24
|
+
|
|
25
|
+
// Register toggle-auto-read command
|
|
26
|
+
pi.registerCommand("toggle-auto-read", {
|
|
27
|
+
description: "Toggle automatic hashline anchors after write operations",
|
|
28
|
+
handler: async (_args, ctx) => {
|
|
29
|
+
autoReadEnabled = !autoReadEnabled;
|
|
30
|
+
const state = autoReadEnabled ? "enabled" : "disabled";
|
|
31
|
+
ctx.ui.notify(`Auto-read after write: ${state}`, "info");
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Auto-read after write handler - always registered, but checks the flag
|
|
22
36
|
pi.on("tool_result", async (event, ctx) => {
|
|
37
|
+
if (!autoReadEnabled) return;
|
|
23
38
|
if (event.toolName !== "write" || event.isError) return;
|
|
24
39
|
|
|
25
40
|
const filePath = (event.input as Record<string, unknown>)?.path;
|
|
@@ -30,8 +45,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
30
45
|
const content = await readFile(absolutePath, "utf-8");
|
|
31
46
|
|
|
32
47
|
// Normalize and compute hashline output
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
const normalized = normalizeToLF(content);
|
|
49
|
+
const visibleLines = getVisibleLines(normalized);
|
|
35
50
|
|
|
36
51
|
if (visibleLines.length === 0) return;
|
|
37
52
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (
|
|
3
|
+
"version": "0.7.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|
package/prompts/read.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Read a text file. Each line is returned as `HASH│content`.
|
|
2
2
|
|
|
3
3
|
HASH format:
|
|
4
|
-
- The HASH is
|
|
4
|
+
- The HASH is 3 characters from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3`, `4yN`, `-qk`).
|
|
5
5
|
- The content after the `│` separator is the line verbatim.
|
|
6
6
|
- The line number is not part of the output. Use the HASH to reference lines.
|
|
7
7
|
|
package/prompts/replace.md
CHANGED
|
@@ -8,15 +8,15 @@ How to use:
|
|
|
8
8
|
```
|
|
9
9
|
read({ path: "src/main.ts" })
|
|
10
10
|
// Returns:
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// MQX│const x = 1;
|
|
12
|
+
// ZPM│const y = 2;
|
|
13
|
+
// VRW│const z = 3;
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
2. Copy the
|
|
16
|
+
2. Copy the 3-character HASH (before `│`) into `start`/`end`:
|
|
17
17
|
```json
|
|
18
18
|
{ "path": "src/main.ts", "edits": [
|
|
19
|
-
{ "start": "
|
|
19
|
+
{ "start": "MQX", "end": "MQX", "lines": ["const x = 99;"] }
|
|
20
20
|
] }
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -25,14 +25,14 @@ Examples:
|
|
|
25
25
|
1. Single line replace:
|
|
26
26
|
```json
|
|
27
27
|
{ "path": "src/main.ts", "edits": [
|
|
28
|
-
{ "start": "
|
|
28
|
+
{ "start": "MQX", "end": "MQX", "lines": ["const x = 1;"] }
|
|
29
29
|
] }
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
2. Range replace (3 lines → 3 new lines):
|
|
33
33
|
```json
|
|
34
34
|
{ "path": "src/main.ts", "edits": [
|
|
35
|
-
{ "start": "
|
|
35
|
+
{ "start": "ZPM", "end": "VRW", "lines": [
|
|
36
36
|
"function greet(name) {",
|
|
37
37
|
" return `Hello, ${name}`;",
|
|
38
38
|
"}"
|
|
@@ -43,27 +43,24 @@ Examples:
|
|
|
43
43
|
3. Multiple regions in one call (delete two non-adjacent ranges):
|
|
44
44
|
```json
|
|
45
45
|
{ "path": "src/server.ts", "edits": [
|
|
46
|
-
{ "start": "
|
|
47
|
-
{ "start": "
|
|
46
|
+
{ "start": "aB3", "end": "xY7", "lines": [] },
|
|
47
|
+
{ "start": "MQX", "end": "ZPM", "lines": [] }
|
|
48
48
|
] }
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
Rules:
|
|
52
52
|
- `start` and `end` are required. A single-line replace is `start=X, end=X`.
|
|
53
53
|
- To delete a range, use `lines: []`.
|
|
54
|
-
- `start`, `end` are HASH anchors only (e.g. `
|
|
54
|
+
- `start`, `end` are HASH anchors only (e.g. `aB3`). Do not include `│` or line content.
|
|
55
55
|
- `lines` is literal file content — each string becomes exactly one line in the file. No `HASH│` prefix, no `+`/`-` diff markers.
|
|
56
56
|
- Don't add `""` for spacing unless you actually want a new blank line.
|
|
57
57
|
- Copy anchors from the most recent `read` of the file. Do not guess or construct them.
|
|
58
58
|
- All edits in one call must be non-conflicting. The runtime rejects with `[E_EDIT_CONFLICT]` if two ranges overlap.
|
|
59
59
|
- If `lines` matches current content, the replace is classified as `noop` (file unchanged).
|
|
60
|
+
- The `start`/`end` range is inclusive — both anchors and every line between them are replaced. If your replacement content includes lines that already exist in the file (e.g. closing brackets), make sure those lines are within your range, otherwise they will appear twice.
|
|
60
61
|
|
|
61
62
|
On success, the response contains an `--- Anchors ---` block with fresh HASH anchors for the changed region. Use those for nearby follow-up replaces instead of re-reading.
|
|
62
63
|
|
|
63
|
-
Auto-read after write:
|
|
64
|
-
- After a successful `write`, the result includes a `--- Auto-read (hashline anchors) ---` block.
|
|
65
|
-
- Use those anchors directly for `replace` calls without a separate `read`.
|
|
66
|
-
|
|
67
64
|
Error recovery:
|
|
68
65
|
- `[E_STALE_ANCHOR]` — file changed since last read. The error includes fresh `>>> HASH│content` lines; copy the HASH and retry.
|
|
69
66
|
- `[E_BAD_REF]` — malformed HASH. Re-read and try again.
|
package/src/hashline/apply.ts
CHANGED
|
@@ -220,7 +220,6 @@ export function applyHashlineEdits(
|
|
|
220
220
|
edits: import("./resolve").HashlineEdit[],
|
|
221
221
|
signal?: AbortSignal,
|
|
222
222
|
precomputedHashes?: string[],
|
|
223
|
-
filePath?: string,
|
|
224
223
|
): {
|
|
225
224
|
content: string;
|
|
226
225
|
firstChangedLine: number | undefined;
|
|
@@ -262,7 +261,7 @@ export function applyHashlineEdits(
|
|
|
262
261
|
);
|
|
263
262
|
}
|
|
264
263
|
|
|
265
|
-
const barePrefixWarnings = assertNoBareHashPrefixLines(edits, lineIndex.fileLines, fileHashes
|
|
264
|
+
const barePrefixWarnings = assertNoBareHashPrefixLines(edits, lineIndex.fileLines, fileHashes);
|
|
266
265
|
warnings.push(...barePrefixWarnings);
|
|
267
266
|
maybeWarnSuspiciousUnicodeEscapePlaceholder(edits, warnings);
|
|
268
267
|
|
package/src/hashline/hash.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import xxhash from "xxhash-wasm";
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
export const HASH_LENGTH =
|
|
5
|
+
export const HASH_LENGTH = 3;
|
|
6
6
|
|
|
7
7
|
export const HASH_PREFIX = "";
|
|
8
8
|
|
|
@@ -81,11 +81,19 @@ export function computeLineHashes(content: string): string[] {
|
|
|
81
81
|
const lines = content.split("\n");
|
|
82
82
|
const hashes = new Array<string>(lines.length);
|
|
83
83
|
const counts = new Map<string, number>();
|
|
84
|
+
const assigned = new Set<string>();
|
|
84
85
|
for (let i = 0; i < lines.length; i++) {
|
|
85
86
|
const canonical = canonicalizeLine(lines[i]!);
|
|
86
87
|
const occurrence = (counts.get(canonical) ?? 0) + 1;
|
|
87
88
|
counts.set(canonical, occurrence);
|
|
88
|
-
|
|
89
|
+
let hash = hashToString(xxh32(`${DISCRIMINATOR(occurrence)}:${canonical}`));
|
|
90
|
+
let retry = 0;
|
|
91
|
+
while (assigned.has(hash)) {
|
|
92
|
+
retry++;
|
|
93
|
+
hash = hashToString(xxh32(`${DISCRIMINATOR(occurrence)}:${canonical}:R${retry}`));
|
|
94
|
+
}
|
|
95
|
+
assigned.add(hash);
|
|
96
|
+
hashes[i] = hash;
|
|
89
97
|
}
|
|
90
98
|
return hashes;
|
|
91
99
|
}
|
package/src/hashline/parse.ts
CHANGED
|
@@ -15,14 +15,14 @@ function diagnoseHashRef(ref: string): string {
|
|
|
15
15
|
const trimmed = ref.trim();
|
|
16
16
|
|
|
17
17
|
if (!trimmed.length) {
|
|
18
|
-
return `[E_BAD_REF] Invalid anchor. Expected a
|
|
18
|
+
return `[E_BAD_REF] Invalid anchor. Expected a 3-character base64 anchor (e.g. \"aB3\").`;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
if (/^\d+/.test(trimmed)) {
|
|
22
|
-
return `[E_BAD_REF] Invalid anchor. Use the hash alone (e.g. \"
|
|
22
|
+
return `[E_BAD_REF] Invalid anchor. Use the hash alone (e.g. \"aB3\") — no line numbers or trailing content.`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
return `[E_BAD_REF] Invalid anchor \"${trimmed}\". Expected a
|
|
25
|
+
return `[E_BAD_REF] Invalid anchor \"${trimmed}\". Expected a 3-character base64 anchor (e.g. \"aB3\").`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function parseAnchorRef(ref: string): Anchor {
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -78,15 +78,15 @@ export function formatMismatchError(
|
|
|
78
78
|
|
|
79
79
|
if (notFound.length > 0) {
|
|
80
80
|
const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
out.push(
|
|
82
|
+
`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}: ${refList}. Call read() to get fresh anchors, then copy the 4-character HASH from each line into your next replace call.`
|
|
83
|
+
);
|
|
84
84
|
}
|
|
85
85
|
if (ambiguous.length > 0) {
|
|
86
86
|
if (out.length > 0) out.push("");
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
out.push(
|
|
88
|
+
`[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}. Call read() to get fresh anchors, then copy the 4-character HASH from each line into your next replace call.`
|
|
89
|
+
);
|
|
90
90
|
for (const m of ambiguous) {
|
|
91
91
|
const sample = (m.candidates ?? []).slice(0, 5);
|
|
92
92
|
const more =
|
|
@@ -105,15 +105,6 @@ export function formatMismatchError(
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
out.push("");
|
|
109
|
-
out.push("Current state (first lines):");
|
|
110
|
-
const sampleSize = Math.min(fileLines.length, 5);
|
|
111
|
-
for (let i = 0; i < sampleSize; i++) {
|
|
112
|
-
out.push(`>>> ${fileHashes[i]}│${fileLines[i]}`);
|
|
113
|
-
}
|
|
114
|
-
if (fileLines.length > sampleSize) {
|
|
115
|
-
out.push(`... ${fileLines.length - sampleSize} more.`);
|
|
116
|
-
}
|
|
117
108
|
|
|
118
109
|
return out.join("\n");
|
|
119
110
|
}
|
|
@@ -198,7 +189,6 @@ export function assertNoBareHashPrefixLines(
|
|
|
198
189
|
edits: HashlineEdit[],
|
|
199
190
|
fileLines: string[],
|
|
200
191
|
fileHashes: string[],
|
|
201
|
-
filePath?: string,
|
|
202
192
|
): string[] {
|
|
203
193
|
if (fileHashes.length !== fileLines.length) {
|
|
204
194
|
throw new Error(
|
|
@@ -216,18 +206,11 @@ export function assertNoBareHashPrefixLines(
|
|
|
216
206
|
}
|
|
217
207
|
if (suspects.length === 0) return [];
|
|
218
208
|
|
|
219
|
-
const isPython = filePath?.endsWith('.py');
|
|
220
209
|
const fileHashSet = new Set(fileHashes);
|
|
221
210
|
const matched = suspects.filter((s) => fileHashSet.has(s.hash));
|
|
222
211
|
const matchedCount = matched.length;
|
|
223
212
|
const exampleLine = `${suspects[0]!.hash}│${suspects[0]!.line}`;
|
|
224
213
|
|
|
225
|
-
if (isPython) {
|
|
226
|
-
const hint = matchedCount > 0
|
|
227
|
-
? `${matchedCount} prefix(es) match file line hashes.`
|
|
228
|
-
: `None match file line hashes — likely Python syntax.`;
|
|
229
|
-
return [`[W_BARE_HASH_PREFIX] ${suspects.length} edit line(s) start with a hash-like prefix (e.g. ${JSON.stringify(exampleLine)}). ${hint}`];
|
|
230
|
-
}
|
|
231
214
|
|
|
232
215
|
const linesHint =
|
|
233
216
|
matchedCount === 0
|
package/src/replace-response.ts
CHANGED
|
@@ -166,7 +166,7 @@ function truncateOutlineEntry(text: string, max = 88): string {
|
|
|
166
166
|
function collectOutlineEntries(previewText: string): string[] {
|
|
167
167
|
const structural: string[] = [];
|
|
168
168
|
for (const line of previewText.split("\n")) {
|
|
169
|
-
const match = line.match(/^\s*([A-Za-z0-9_\-]{
|
|
169
|
+
const match = line.match(/^\s*([A-Za-z0-9_\-]{3})│(.*)$/);
|
|
170
170
|
if (!match) continue;
|
|
171
171
|
const content = match[2]!.trim();
|
|
172
172
|
if (content.length === 0) continue;
|
|
@@ -222,7 +222,7 @@ function formatRequestedRangePreviews(
|
|
|
222
222
|
},
|
|
223
223
|
precomputedHashes,
|
|
224
224
|
);
|
|
225
|
-
const hasReturnedLines = /^\s*[A-Za-z0-9_\-]{
|
|
225
|
+
const hasReturnedLines = /^\s*[A-Za-z0-9_\-]{3}│/m.test(preview.text);
|
|
226
226
|
const actualEnd = hasReturnedLines
|
|
227
227
|
? preview.nextOffset !== undefined
|
|
228
228
|
? preview.nextOffset - 1
|