pi-hashline-edit-pro 1.1.2 → 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 +2 -1
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/hashline/hash.ts +11 -41
- package/src/hashline/index.ts +1 -0
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,6 +193,7 @@ 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.
|
package/package.json
CHANGED
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 =
|
|
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"].`;
|
package/src/hashline/hash.ts
CHANGED
|
@@ -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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 +
|
|
86
|
+
hint.value = baseIdx + HASH_PROBE_STRIDE;
|
|
116
87
|
return hashAt(baseIdx);
|
|
117
88
|
}
|
|
118
|
-
const
|
|
119
|
-
const nextIdx = nextZeroBit(used, start);
|
|
89
|
+
const nextIdx = nextZeroBit(used, hint.value);
|
|
120
90
|
setBit(used, nextIdx);
|
|
121
|
-
hint.value = nextIdx +
|
|
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 +
|
|
250
|
+
if (idx + HASH_PROBE_STRIDE > hint.value) hint.value = idx + HASH_PROBE_STRIDE;
|
|
281
251
|
}
|
|
282
252
|
};
|
|
283
253
|
|