pi-hashline-edit-pro 0.4.3 → 0.5.1

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
@@ -13,8 +13,7 @@ The original uses 2-character hashes of a 16-character alphabet, with the hash b
13
13
  This fork makes two changes that compound:
14
14
 
15
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.
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. Symbol-only lines use `"S{lineNumber}"` as the discriminator. 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.
17
-
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.
18
17
  ## Installation
19
18
 
20
19
  From npm:
@@ -114,10 +113,10 @@ Hashes are computed with [xxhash-wasm](https://github.com/jungomi/xxhash-wasm) (
114
113
 
115
114
  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.
116
115
 
117
- Hashes are occurrence-aware: a discriminator prefix is mixed into the xxHash input before the line content. Symbol-only lines (lone `}`, etc.) use `S{lineNumber}` as the discriminator; content lines use `C{occurrence}` where `occurrence` is the running count of that canonical content earlier in the file. This way:
116
+ Hashes are occurrence-aware: a discriminator prefix is mixed into the xxHash input before the line content. All lines (including symbol-only lines like `}`) use `C{occurrence}` as the discriminator, where `occurrence` is the running count of that canonical content earlier in the file. This way:
118
117
 
119
- - `}` on line 5 and `}` on line 17 hash differently (different `S{...}` prefix).
120
- - `import { foo } from 'bar';` on line 3 and the same string on line 47 hash differently (different `C{...}` prefix, 1 vs 2).
118
+ - `}` on line 5 and `}` on line 17 hash differently (1st vs 2nd occurrence of `}`).
119
+ - `import { foo } from 'bar';` on line 3 and the same string on line 47 hash differently (1st vs 2nd occurrence).
121
120
 
122
121
  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` / `edit` response formatting. There is no per-line recomputation that could disagree with what the model saw in its last read.
123
122
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "Strict hashline read/edit tool override for pi-coding-agent with hash-anchored edits (4-char, 24-bit)",
5
5
  "main": "index.ts",
6
6
  "repository": {
package/prompts/edit.md CHANGED
@@ -44,6 +44,7 @@ Rules:
44
44
  - `lines` is literal file content. No `HASH│` prefix, no leading `+`/`-` (those are read/diff metadata, not file content). Lines starting with 4 base64 chars + `│` are checked; if detected, the edit is rejected with `[E_BARE_HASH_PREFIX]`. For `.py` files, this becomes a `[W_BARE_HASH_PREFIX]` warning instead (Python syntax like `else:`, `except:` triggers the detector).
45
45
  - Copy anchors from the most recent `read` of the file. Do not guess or construct them.
46
46
  - All edits in one call must be non-conflicting. The runtime rejects with `[E_EDIT_CONFLICT]` if: two `replace` ranges overlap; two `append`/`prepend` target the same insertion boundary (e.g. two EOF appends on a newline-terminated file); or an `append`/`prepend` falls inside a `replace` range in the same call. Fix: merge into one, use different boundaries, or split into a follow-up `edit` call.
47
+ - When building `replace` ranges, double-check that `start` is on the first line you want changed and `end` is on the last. If two edits would touch the same lines or adjacent lines, merge them into one `replace` with the combined new content.
47
48
  - If `lines` matches the current content byte-for-byte, the edit is classified as `Classification: noop` (file unchanged, not an error).
48
49
 
49
50
  On success (`changed` mode, default), the response text contains an `--- Anchors ---` block with fresh `HASH│content` for the changed region (2 lines of context, capped at ~12 lines / 50 KB). Use those for nearby follow-up edits instead of re-reading. If the response says `Anchors omitted; use read for subsequent edits`, the region was too large — call `read` again. For distant follow-ups, or on any error, call `read` again. `full` and `ranges` modes put previews in `details`; the model only needs what's in the text.
@@ -40,7 +40,7 @@ export const DIFF_MINUS_RE = /^-\s*\d+\s{4}/;
40
40
 
41
41
  export const HASHLINE_BARE_PREFIX_RE = new RegExp(`^\\s*(${HASH_CHARS_CLASS})│`);
42
42
 
43
- const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
43
+
44
44
 
45
45
  // Lazy-initialized xxhash-wasm hasher. Initialization starts at module load
46
46
  // time and completes in ~2ms. By the time any tool calls xxh32(), the hasher
@@ -64,50 +64,35 @@ hasherPromise = xxhash().then((h) => {
64
64
 
65
65
  // Export for tests that need to await readiness.
66
66
  export function ensureHasherReady(): Promise<Hasher> {
67
- return hasherPromise!;
67
+ return hasherPromise!
68
68
  }
69
69
 
70
70
  function xxh32(input: string, seed = 0): number {
71
71
  return getHasher().h32(input, seed) >>> 0;
72
72
  }
73
73
 
74
- const SYMBOL_DISCRIMINATOR = (lineNumber: number): string => `S${lineNumber}`;
75
- const CONTENT_DISCRIMINATOR = (occurrence: number): string => `C${occurrence}`;
74
+ const DISCRIMINATOR = (occurrence: number): string => `C${occurrence}`;
76
75
 
77
76
  function canonicalizeLine(line: string): string {
78
77
  return line.replace(/\r/g, "").trimEnd();
79
78
  }
80
79
 
81
- function isSymbolOnly(canonical: string): boolean {
82
- return !RE_SIGNIFICANT.test(canonical);
83
- }
84
-
85
80
  export function computeLineHashes(content: string): string[] {
86
81
  const lines = content.split("\n");
87
82
  const hashes = new Array<string>(lines.length);
88
83
  const counts = new Map<string, number>();
89
84
  for (let i = 0; i < lines.length; i++) {
90
- const lineNumber = i + 1;
91
85
  const canonical = canonicalizeLine(lines[i]!);
92
- let discriminator: string;
93
- if (isSymbolOnly(canonical)) {
94
- discriminator = SYMBOL_DISCRIMINATOR(lineNumber);
95
- } else {
96
- const occurrence = (counts.get(canonical) ?? 0) + 1;
97
- counts.set(canonical, occurrence);
98
- discriminator = CONTENT_DISCRIMINATOR(occurrence);
99
- }
100
- hashes[i] = hashToString(xxh32(`${discriminator}:${canonical}`));
86
+ const occurrence = (counts.get(canonical) ?? 0) + 1;
87
+ counts.set(canonical, occurrence);
88
+ hashes[i] = hashToString(xxh32(`${DISCRIMINATOR(occurrence)}:${canonical}`));
101
89
  }
102
90
  return hashes;
103
91
  }
104
92
 
105
93
  export function computeLineHash(idx: number, line: string): string {
106
94
  const canonical = canonicalizeLine(line);
107
- const discriminator = isSymbolOnly(canonical)
108
- ? SYMBOL_DISCRIMINATOR(idx)
109
- : CONTENT_DISCRIMINATOR(1);
110
- return hashToString(xxh32(`${discriminator}:${canonical}`));
95
+ return hashToString(xxh32(`${DISCRIMINATOR(1)}:${canonical}`));
111
96
  }
112
97
 
113
98
  export const HASH_FORMAT = {