pi-hashline-edit-pro 1.1.1 → 1.2.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
@@ -179,7 +179,7 @@ Each line is canonicalized (carriage returns stripped, trailing whitespace trimm
179
179
 
180
180
  The alphabet is sized for an LLM consumer: the model tokenizes rather than squinting at glyphs, so case and digits are all included. The URL-safe specials `-` and `_` are deliberately excluded — a hash starting with `-` is shape-identical to a diff-preview deletion row, and `-`/`_` at a line start are markdown-active, inviting mis-copying and false autocorrections.
181
181
 
182
- **Unique anchors by construction.** If a line's base hash collides with an already-assigned hash, the next free hash is allocated from a bitset (O(1) amortized). Every line in a file therefore gets a unique anchor — two byte-identical lines (repeated `}`, repeated `import` statements) never share one. The same guarantee sets the file size cap: at most 238,328 lines per file, beyond which `read` and `replace` reject with `[E_FILE_TOO_LARGE]` (use `write` for very large files).
182
+ **Unique anchors by construction.** If a line's base hash collides with an already-assigned hash, the next free hash is allocated from a bitset by probing with a stride coprime to the hash space (O(1) amortized). The stride is `62² + 62 + 1`, so consecutive collisions — runs of blank lines, repeated `}` — land on anchors that differ in all three characters instead of sharing a prefix. Every line in a file therefore gets a unique anchor — two byte-identical lines (repeated `}`, repeated `import` statements) never share one. The same guarantee sets the file size cap: at most 238,328 lines per file, beyond which `read` and `replace` reject with `[E_FILE_TOO_LARGE]` (use `write` for very large files).
183
183
 
184
184
  ## Design decisions
185
185
 
@@ -193,13 +193,14 @@ The alphabet is sized for an LLM consumer: the model tokenizes rather than squin
193
193
 
194
194
  - **Stale anchors.** `[E_STALE_ANCHOR]` / `[E_AMBIGUOUS_ANCHOR]` mean the file changed since the anchors were read, or an earlier `read` never happened. Call `read` for fresh anchors and retry.
195
195
  - **Reset the hash store.** Anchors live in `~/.config/pi-hashline-edit-pro/hash-store.sqlite` (with `-wal`/`-shm` sidecars). Quit pi, delete those three files, and the store is rebuilt on the next session. Anchor history is lost, but no project files are touched.
196
+ - **Upgrading.** A hash-allocation change clears the hash store once on the first run after upgrade — anchors are rebuilt on the next read and undo history is lost, but no project files are touched.
196
197
  - **Corrupt store.** If the store fails its health check it is renamed to `hash-store.sqlite.corrupt-<timestamp>` (plus `-wal`/`-shm` variants) and rebuilt automatically; the quarantined files can be deleted once a healthy store exists.
197
198
  - **Legacy migration.** On first run after upgrading from an older version, the previous `hash-store.json` is imported once and renamed to `hash-store.json.bak`, which can be deleted.
198
199
  - **`[E_UNDO_UNAVAILABLE]`.** The edit was refused because the undo record could not be written — check disk space and that the config directory is writable, then retry.
199
200
 
200
201
  ## Development
201
202
 
202
- Requires [Node.js](https://nodejs.org) ≥ 22.13 and npm.
203
+ Requires [Node.js](https://nodejs.org) ≥ 22.19 and npm.
203
204
 
204
205
  ```bash
205
206
  npm install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 62-symbol, perfect hashing)",
6
6
  "main": "index.ts",
@@ -34,15 +34,15 @@
34
34
  "dependencies": {
35
35
  "diff": "^8.0.2",
36
36
  "file-type": "^21.3.0",
37
- "typebox": "^1.1.24",
37
+ "typebox": "^1.3.7",
38
38
  "xxhash-wasm": "^1.1.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@earendil-works/pi-coding-agent": ">=0.74.0",
41
+ "@earendil-works/pi-coding-agent": ">=0.75.0",
42
42
  "@earendil-works/pi-tui": "*"
43
43
  },
44
44
  "engines": {
45
- "node": ">=22.13.0"
45
+ "node": ">=22.19.0"
46
46
  },
47
47
  "scripts": {
48
48
  "test": "vitest run",
@@ -52,7 +52,7 @@
52
52
  "typecheck": "tsc --noEmit"
53
53
  },
54
54
  "devDependencies": {
55
- "@earendil-works/pi-coding-agent": "^0.74.0",
55
+ "@earendil-works/pi-coding-agent": "^0.84.0",
56
56
  "@eslint/js": "^10.0.1",
57
57
  "@types/node": "^24.0.0",
58
58
  "@vitest/coverage-v8": "^4.1.10",
package/src/constants.ts CHANGED
@@ -4,7 +4,7 @@ export const MAX_BYTES = 100 * 1024 * 1024;
4
4
  export const MAX_READ_LINE_BYTES = 200 * 1024;
5
5
 
6
6
  export const HASH_STORE_BUSY_TIMEOUT = 1000;
7
- export const HASH_STORE_VERSION = 4;
7
+ export const HASH_STORE_VERSION = 5;
8
8
  export const CONTENT_LINES_NOT_STRING_MSG =
9
9
  `[E_BAD_SHAPE] "content_lines" must be a native JSON array of strings, not a JSON string.`
10
10
  + ` Do not serialize the array (e.g. '["line1", "line2"]') — pass it as a proper JSON array: ["line1", "line2"].`;
@@ -22,6 +22,8 @@ export const HASH_CLASS = `[${ALPH_SAFE}]{${HASH_LEN}}`;
22
22
  export const HASH_SPACE = ALPH.length ** HASH_LEN;
23
23
  export const MAX_HASH_LINES = HASH_SPACE;
24
24
 
25
+ export const HASH_PROBE_STRIDE = ALPH.length ** 2 + ALPH.length + 1;
26
+
25
27
  function idxToHash(idx: number): string {
26
28
  let out = "";
27
29
  for (let j = 0; j < HASH_LEN; j++) {
@@ -66,44 +68,13 @@ function setBit(bits: Uint32Array, idx: number): void {
66
68
  }
67
69
 
68
70
  function nextZeroBit(bits: Uint32Array, start: number): number {
69
- const totalWords = bits.length;
70
71
  const totalBits = HASH_SPACE;
71
- const lastWordBits = HASH_SPACE - (totalWords - 1) * 32;
72
-
73
- if (start >= totalBits) start = 0;
74
-
75
- const wordIdx = start >>> 5;
76
- const bitOffset = start & 31;
77
- const wordBits = (w: number): number => (w === totalWords - 1 ? lastWordBits : 32);
78
-
79
- let word = bits[wordIdx];
80
- for (let b = bitOffset; b < wordBits(wordIdx); b++) {
81
- if ((word >>> b & 1) === 0) return wordIdx * 32 + b;
82
- }
83
-
84
- for (let w = wordIdx + 1; w < totalWords; w++) {
85
- word = bits[w];
86
- if (~word !== 0) {
87
- for (let b = 0; b < wordBits(w); b++) {
88
- if ((word >>> b & 1) === 0) return w * 32 + b;
89
- }
90
- }
72
+ let idx = start % totalBits;
73
+ for (let i = 0; i < totalBits; i++) {
74
+ if (!getBit(bits, idx)) return idx;
75
+ idx += HASH_PROBE_STRIDE;
76
+ if (idx >= totalBits) idx -= totalBits;
91
77
  }
92
-
93
- for (let w = 0; w < wordIdx; w++) {
94
- word = bits[w];
95
- if (~word !== 0) {
96
- for (let b = 0; b < wordBits(w); b++) {
97
- if ((word >>> b & 1) === 0) return w * 32 + b;
98
- }
99
- }
100
- }
101
-
102
- word = bits[wordIdx];
103
- for (let b = 0; b < bitOffset; b++) {
104
- if ((word >>> b & 1) === 0) return wordIdx * 32 + b;
105
- }
106
-
107
78
  throw new Error(
108
79
  `[E_FILE_TOO_LARGE] Cannot allocate a unique hash anchor: the file exceeds the ${HASH_SPACE}-line limit for ${HASH_LEN}-char hashline anchors. For very large files use write or a non-line-based approach.`,
109
80
  );
@@ -112,13 +83,12 @@ function nextZeroBit(bits: Uint32Array, start: number): number {
112
83
  function assignHash(used: Uint32Array, baseIdx: number, hint: { value: number }): string {
113
84
  if (!getBit(used, baseIdx)) {
114
85
  setBit(used, baseIdx);
115
- hint.value = baseIdx + 1;
86
+ hint.value = baseIdx + HASH_PROBE_STRIDE;
116
87
  return hashAt(baseIdx);
117
88
  }
118
- const start = hint.value > baseIdx + 1 ? hint.value : baseIdx + 1;
119
- const nextIdx = nextZeroBit(used, start);
89
+ const nextIdx = nextZeroBit(used, hint.value);
120
90
  setBit(used, nextIdx);
121
- hint.value = nextIdx + 1;
91
+ hint.value = nextIdx + HASH_PROBE_STRIDE;
122
92
  return hashAt(nextIdx);
123
93
  }
124
94
 
@@ -277,7 +247,7 @@ function mapStableHashes(
277
247
  const idx = hashToIndex(hash);
278
248
  if (idx >= 0) {
279
249
  setBit(used, idx);
280
- if (idx + 1 > hint.value) hint.value = idx + 1;
250
+ if (idx + HASH_PROBE_STRIDE > hint.value) hint.value = idx + HASH_PROBE_STRIDE;
281
251
  }
282
252
  };
283
253
 
@@ -4,6 +4,7 @@ export {
4
4
  HASH_SEP,
5
5
  HASH_CLASS,
6
6
  HASH_SPACE,
7
+ HASH_PROBE_STRIDE,
7
8
  MAX_HASH_LINES,
8
9
  HL_PREFIX_PLUS_RE,
9
10
  HL_PREFIX_MINUS_RE,