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 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. 4-character content hashes over a 64-character URL-safe base64 alphabet give 24 bits of entropy per anchor, so collisions are effectively zero in any realistic file.
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 two ways: a 4-character hash length and an occurrence-aware discriminator that makes identical content at different positions hash to different values.
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 two changes that compound:
13
+ This fork makes three changes that compound:
14
14
 
15
- 1. **Bump hash length to 4 characters** of the 64-char URL-safe base64 alphabet. That gives 24 bits / 16 777 216 buckets. Birthday-paradox collisions are effectively nullified for any realistic file.
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 4-character hash followed by the `│` separator and the line content. Example output for the source below:
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
- 0qH3│function hello() {
47
- szJr│ console.log("world");
48
- _zlP│}
48
+ 0qH│function hello() {
49
+ szJ│ console.log("world");
50
+ _zl│}
49
51
  ```
50
52
 
51
- - `HASH` is a 4-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3x`).
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": "ve7o", "end": "ve7o", "lines": [" console.log('hashline');"] }
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
- 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:
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 includes fresh `>>> HASH│content` lines for the affected region. The model copies the HASH portion and retries.
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 5 chars of a `lines` entry looking like 4 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.
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 4-character string from the URL-safe base64 alphabet `A-Za-z0-9-_`. That's 64 distinct characters, 6 bits per position, 24 bits of entropy per anchor.
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 5 if you ever need even more entropy.
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_\-]{4}│` is highly specific. It only matches lines starting with exactly 4 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.
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: append hashline read output to write results
21
- // so the model immediately has anchors for subsequent edits.
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
- const normalized = normalizeToLF(content);
34
- const visibleLines = getVisibleLines(normalized);
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.6.0",
4
- "description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (4-char, 24-bit)",
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 characters from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3x`, `4yN-`, `-qkl`).
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
 
@@ -8,15 +8,15 @@ How to use:
8
8
  ```
9
9
  read({ path: "src/main.ts" })
10
10
  // Returns:
11
- // MQXV│const x = 1;
12
- // ZPMQ│const y = 2;
13
- // VRWS│const z = 3;
11
+ // MQX│const x = 1;
12
+ // ZPM│const y = 2;
13
+ // VRW│const z = 3;
14
14
  ```
15
15
 
16
- 2. Copy the 4-character HASH (before `│`) into `start`/`end`:
16
+ 2. Copy the 3-character HASH (before `│`) into `start`/`end`:
17
17
  ```json
18
18
  { "path": "src/main.ts", "edits": [
19
- { "start": "MQXV", "end": "MQXV", "lines": ["const x = 99;"] }
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": "MQXV", "end": "MQXV", "lines": ["const x = 1;"] }
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": "ZPMQ", "end": "VRWS", "lines": [
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": "aB3x", "end": "xY7q", "lines": [] },
47
- { "start": "MQXV", "end": "ZPMQ", "lines": [] }
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. `aB3x`). Do not include `│` or line content.
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.
@@ -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, filePath);
264
+ const barePrefixWarnings = assertNoBareHashPrefixLines(edits, lineIndex.fileLines, fileHashes);
266
265
  warnings.push(...barePrefixWarnings);
267
266
  maybeWarnSuspiciousUnicodeEscapePlaceholder(edits, warnings);
268
267
 
@@ -2,7 +2,7 @@
2
2
  import xxhash from "xxhash-wasm";
3
3
 
4
4
 
5
- export const HASH_LENGTH = 4;
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
- hashes[i] = hashToString(xxh32(`${DISCRIMINATOR(occurrence)}:${canonical}`));
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
  }
@@ -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 4-character base64 anchor (e.g. \"aB3x\").`;
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. \"aB3x\") — no line numbers or trailing content.`;
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 4-character base64 anchor (e.g. \"aB3x\").`;
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 {
@@ -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
- out.push(
82
- `[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}: ${refList}. Re-read the file to refresh.`
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
- out.push(
88
- `[E_AMBIGUOUS_ANCHOR] ${ambiguous.length} ambiguous anchor${ambiguous.length > 1 ? "s" : ""}. Re-read the file to refresh.`
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
@@ -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_\-]{4})│(.*)$/);
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_\-]{4}│/m.test(preview.text);
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
package/src/replace.ts CHANGED
@@ -368,7 +368,6 @@ async function executeEditPipeline(
368
368
  resolved,
369
369
  signal,
370
370
  originalHashes,
371
- absolutePath,
372
371
  );
373
372
 
374
373
  return {