pi-hashline-edit-pro 0.17.5 → 0.17.7
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 -0
- package/package.json +8 -2
- package/scripts/cleanup-better-sqlite3.cjs +17 -0
- package/scripts/{ensure-better-sqlite3.js → ensure-better-sqlite3.cjs} +22 -6
- package/src/hash-store.ts +7 -7
- package/src/hashline/apply.ts +1 -4
- package/src/hashline/hash.ts +8 -7
- package/src/hashline/parse.ts +0 -1
- package/src/replace-diff.ts +1 -12
- package/src/replace-normalize.ts +5 -5
- package/src/replace-response.ts +1 -1
package/README.md
CHANGED
|
@@ -154,6 +154,8 @@ Hashes are computed with [xxhash-wasm](https://github.com/jungomi/xxhash-wasm) (
|
|
|
154
154
|
|
|
155
155
|
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.
|
|
156
156
|
|
|
157
|
+
Before hashing, each line is normalized: carriage returns are stripped and trailing whitespace is trimmed. This `canon()` normalization prevents insignificant whitespace changes from cascade-triggering hash churn across the file. Two lines that differ only in trailing spaces or `\r` characters produce the same hash, so anchor stability is preserved across editor-save cycles that add or remove trailing whitespace.
|
|
158
|
+
|
|
157
159
|
**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: `: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. Two byte-identical lines (e.g. repeated `}` or repeated `import` statements) get different hashes automatically.
|
|
158
160
|
The runtime always precomputes the full per-line hash array for a file via `lineHashes(content, path)`, 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. When `path` is provided, `lineHashes` uses a persistent store to preserve hashes for unchanged lines across edits — see [Stable hashing across edits](#stable-hashing-across-edits).
|
|
159
161
|
`HASH_LEN` in `src/hashline/hash.ts` sets the hash body length; bump it to 4 if you need even more entropy without collision resolution.
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.7",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 18-bit, perfect hashing)",
|
|
5
6
|
"main": "index.ts",
|
|
6
7
|
"repository": {
|
|
@@ -45,15 +46,20 @@
|
|
|
45
46
|
"scripts": {
|
|
46
47
|
"test": "vitest run",
|
|
47
48
|
"test:watch": "vitest",
|
|
49
|
+
"lint": "eslint 'src/**/*.ts' 'index.ts'",
|
|
48
50
|
"typecheck": "tsc --noEmit",
|
|
49
|
-
"postinstall": "node scripts/ensure-better-sqlite3.
|
|
51
|
+
"postinstall": "node scripts/ensure-better-sqlite3.cjs",
|
|
52
|
+
"preuninstall": "node scripts/cleanup-better-sqlite3.cjs"
|
|
50
53
|
},
|
|
51
54
|
"devDependencies": {
|
|
52
55
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
56
|
+
"@eslint/js": "^10.0.1",
|
|
53
57
|
"@types/better-sqlite3": "^7.6.13",
|
|
54
58
|
"@types/node": "^22.0.0",
|
|
55
59
|
"@types/sql.js": "^1.4.11",
|
|
60
|
+
"eslint": "^10.7.0",
|
|
56
61
|
"typescript": "^5.8.0",
|
|
62
|
+
"typescript-eslint": "^8.65.0",
|
|
57
63
|
"vitest": "^4.1.8"
|
|
58
64
|
},
|
|
59
65
|
"allowScripts": {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const parentNodeModules = path.resolve(__dirname, "..", "node_modules");
|
|
5
|
+
if (!fs.existsSync(parentNodeModules)) process.exit(0);
|
|
6
|
+
|
|
7
|
+
const entries = fs.readdirSync(parentNodeModules, { withFileTypes: true });
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
if (entry.name.startsWith(".better-sqlite3-")) {
|
|
10
|
+
const full = path.join(parentNodeModules, entry.name);
|
|
11
|
+
try {
|
|
12
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
13
|
+
console.error("Cleaned up stale better-sqlite3 build artifact:", entry.name);
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -5,6 +5,27 @@ const path = require("path");
|
|
|
5
5
|
const root = path.resolve(__dirname, "..");
|
|
6
6
|
const bsqlDir = path.join(root, "node_modules", "better-sqlite3");
|
|
7
7
|
|
|
8
|
+
function removeStaleArtifacts() {
|
|
9
|
+
for (const dir of [root, bsqlDir]) {
|
|
10
|
+
if (!fs.existsSync(dir)) continue;
|
|
11
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
if (entry.name.startsWith(".better-sqlite3-")) {
|
|
14
|
+
const full = path.join(dir, entry.name);
|
|
15
|
+
try {
|
|
16
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
17
|
+
console.error("Removed stale artifact:", entry.name);
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const buildDir = path.join(bsqlDir, "build");
|
|
24
|
+
if (fs.existsSync(buildDir)) {
|
|
25
|
+
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
try {
|
|
9
30
|
require(bsqlDir);
|
|
10
31
|
process.exit(0);
|
|
@@ -12,7 +33,7 @@ try {
|
|
|
12
33
|
const msg = e && typeof e.message === "string" ? e.message : String(e);
|
|
13
34
|
if (/GLIBC|Cannot find module|dlo|not found/i.test(msg)) {
|
|
14
35
|
console.error("better-sqlite3 prebuilt incompatible, rebuilding from source...");
|
|
15
|
-
|
|
36
|
+
removeStaleArtifacts();
|
|
16
37
|
const prebuildDir = path.join(bsqlDir, "prebuilds");
|
|
17
38
|
if (fs.existsSync(prebuildDir)) {
|
|
18
39
|
const platform = process.platform + "-" + process.arch;
|
|
@@ -23,11 +44,6 @@ try {
|
|
|
23
44
|
}
|
|
24
45
|
}
|
|
25
46
|
|
|
26
|
-
const buildDir = path.join(bsqlDir, "build");
|
|
27
|
-
if (fs.existsSync(buildDir)) {
|
|
28
|
-
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
47
|
try {
|
|
32
48
|
execSync("npx --yes node-gyp rebuild", {
|
|
33
49
|
cwd: bsqlDir,
|
package/src/hash-store.ts
CHANGED
|
@@ -36,7 +36,7 @@ function isValidSnapshot(value: unknown): value is LegacySnapshot {
|
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
|
|
41
41
|
interface BackendHandle {
|
|
42
42
|
store: HashStore;
|
|
@@ -47,7 +47,7 @@ interface BackendHandle {
|
|
|
47
47
|
|
|
48
48
|
let cachedHandle: { path: string; handle: BackendHandle } | null = null;
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
|
|
52
52
|
let BetterDatabase: any = undefined;
|
|
53
53
|
|
|
@@ -104,7 +104,7 @@ function openBetterDb(storePath: string): BackendHandle {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
|
|
109
109
|
let SqlJsDatabase: any = null;
|
|
110
110
|
let sqlJsPromise: Promise<void> | null = null;
|
|
@@ -182,7 +182,7 @@ function openSqlJsDb(storePath: string): BackendHandle {
|
|
|
182
182
|
};
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
|
|
186
186
|
|
|
187
187
|
let backendPromise: Promise<void> | null = null;
|
|
188
188
|
|
|
@@ -195,7 +195,7 @@ async function initBackend(): Promise<void> {
|
|
|
195
195
|
return backendPromise;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
|
|
199
199
|
|
|
200
200
|
export async function loadHashStore(): Promise<HashStore> {
|
|
201
201
|
const storePath = hashStorePath();
|
|
@@ -234,7 +234,7 @@ export function shutdownHashStore(): void {
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
|
|
238
238
|
|
|
239
239
|
function withStore(store: HashStore, fn: () => void): void {
|
|
240
240
|
const h = cachedHandle?.handle;
|
|
@@ -296,7 +296,7 @@ async function migrateLegacy(handle: BackendHandle): Promise<void> {
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
|
|
300
300
|
|
|
301
301
|
export function getSnapshot(
|
|
302
302
|
store: HashStore,
|
package/src/hashline/apply.ts
CHANGED
|
@@ -9,14 +9,12 @@ import {
|
|
|
9
9
|
type RHEdit,
|
|
10
10
|
type NEdit,
|
|
11
11
|
type HEdit,
|
|
12
|
-
type BDupWarn,
|
|
13
12
|
type AutoFix,
|
|
14
13
|
} from "./resolve";
|
|
15
14
|
|
|
16
15
|
type LIdx = {
|
|
17
16
|
fileLines: string[];
|
|
18
17
|
lineStarts: number[];
|
|
19
|
-
hasTerminalNewline: boolean;
|
|
20
18
|
};
|
|
21
19
|
|
|
22
20
|
export function buildIdx(content: string): LIdx {
|
|
@@ -35,7 +33,6 @@ export function buildIdx(content: string): LIdx {
|
|
|
35
33
|
return {
|
|
36
34
|
fileLines,
|
|
37
35
|
lineStarts,
|
|
38
|
-
hasTerminalNewline: content.endsWith("\n"),
|
|
39
36
|
};
|
|
40
37
|
};
|
|
41
38
|
|
|
@@ -73,7 +70,7 @@ function resToSpan(
|
|
|
73
70
|
lineIndex: LIdx,
|
|
74
71
|
noopEdits: NEdit[],
|
|
75
72
|
): RESpan | null {
|
|
76
|
-
const { fileLines, lineStarts
|
|
73
|
+
const { fileLines, lineStarts } = lineIndex;
|
|
77
74
|
|
|
78
75
|
const startLine = edit.hash_range_inclusive[0].line;
|
|
79
76
|
const endLine = edit.hash_range_inclusive[1].line;
|
package/src/hashline/hash.ts
CHANGED
|
@@ -25,7 +25,7 @@ export const HASH_CLASS = `[${ALPH_SAFE}]{${HASH_LEN}}`;
|
|
|
25
25
|
function h2s(h: number): string {
|
|
26
26
|
const totalBits = HASH_LEN * ALPH_BITS;
|
|
27
27
|
const shift = 32 - totalBits;
|
|
28
|
-
|
|
28
|
+
const n = h >>> shift;
|
|
29
29
|
let out = "";
|
|
30
30
|
for (let j = 0; j < HASH_LEN; j++) {
|
|
31
31
|
out +=
|
|
@@ -142,12 +142,13 @@ function mapStableHashes(
|
|
|
142
142
|
if (!candidates || candidates.length === 0) continue;
|
|
143
143
|
|
|
144
144
|
let bestIdx = 0;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
let bestDist = Infinity;
|
|
146
|
+
for (let j = 0; j < candidates.length; j++) {
|
|
147
|
+
if (removedHashes?.has(candidates[j]!.hash)) continue;
|
|
148
|
+
const dist = Math.abs(candidates[j]!.index - i);
|
|
149
|
+
if (dist < bestDist) {
|
|
150
|
+
bestDist = dist;
|
|
151
|
+
bestIdx = j;
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
|
package/src/hashline/parse.ts
CHANGED
package/src/replace-diff.ts
CHANGED
|
@@ -45,16 +45,12 @@ export function genDiff(
|
|
|
45
45
|
newContent: string,
|
|
46
46
|
contextLines = 2,
|
|
47
47
|
newContentHashes?: string[],
|
|
48
|
-
|
|
48
|
+
_oldHashes?: string[],
|
|
49
49
|
): { diff: string; firstChangedLine: number | undefined } {
|
|
50
|
-
const oldLines = oldContent.split("\n");
|
|
51
|
-
const newLines = newContent.split("\n");
|
|
52
|
-
const effectiveOldHashes = oldHashes ?? _lineHashesPure(oldContent);
|
|
53
50
|
const effectiveNewHashes = newContentHashes ?? _lineHashesPure(newContent);
|
|
54
51
|
|
|
55
52
|
const parts = Diff.diffLines(oldContent, newContent);
|
|
56
53
|
const output: string[] = [];
|
|
57
|
-
let oldLineNum = 1;
|
|
58
54
|
let newLineNum = 1;
|
|
59
55
|
let lastWasChange = false;
|
|
60
56
|
let firstChangedLine: number | undefined;
|
|
@@ -74,7 +70,6 @@ export function genDiff(
|
|
|
74
70
|
newLineNum++;
|
|
75
71
|
} else {
|
|
76
72
|
output.push(fmtDiffLine("-", displayLines[k]!, undefined));
|
|
77
|
-
oldLineNum++;
|
|
78
73
|
}
|
|
79
74
|
}
|
|
80
75
|
lastWasChange = true;
|
|
@@ -86,7 +81,6 @@ export function genDiff(
|
|
|
86
81
|
if (lastWasChange || nextPartIsChange) {
|
|
87
82
|
let linesToShow = displayLines;
|
|
88
83
|
let skipStart = 0;
|
|
89
|
-
let skipEnd = 0;
|
|
90
84
|
let skipMiddle = 0;
|
|
91
85
|
|
|
92
86
|
if (!lastWasChange) {
|
|
@@ -97,29 +91,24 @@ export function genDiff(
|
|
|
97
91
|
linesToShow = [...displayLines.slice(0, contextLines), "__ELLIPSIS__", ...tail];
|
|
98
92
|
skipMiddle = displayLines.length - contextLines * 2;
|
|
99
93
|
} else if (linesToShow.length > contextLines) {
|
|
100
|
-
skipEnd = linesToShow.length - contextLines;
|
|
101
94
|
linesToShow = linesToShow.slice(0, contextLines);
|
|
102
95
|
}
|
|
103
96
|
|
|
104
97
|
if (skipStart > 0) {
|
|
105
98
|
output.push(" ...");
|
|
106
|
-
oldLineNum += skipStart;
|
|
107
99
|
newLineNum += skipStart;
|
|
108
100
|
}
|
|
109
101
|
for (const line of linesToShow) {
|
|
110
102
|
if (line === "__ELLIPSIS__") {
|
|
111
103
|
output.push(" ...");
|
|
112
|
-
oldLineNum += skipMiddle;
|
|
113
104
|
newLineNum += skipMiddle;
|
|
114
105
|
continue;
|
|
115
106
|
}
|
|
116
107
|
const hash = effectiveNewHashes[newLineNum - 1];
|
|
117
108
|
output.push(fmtDiffLine(" ", line, hash));
|
|
118
|
-
oldLineNum++;
|
|
119
109
|
newLineNum++;
|
|
120
110
|
}
|
|
121
111
|
} else {
|
|
122
|
-
oldLineNum += displayLines.length;
|
|
123
112
|
newLineNum += displayLines.length;
|
|
124
113
|
}
|
|
125
114
|
lastWasChange = false;
|
package/src/replace-normalize.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isRec, has } from "./utils";
|
|
2
2
|
import { CONTENT_LINES_NOT_STRING_MSG } from "./constants";
|
|
3
3
|
|
|
4
|
-
function tryParseContentLines(record: Record<string, unknown>, key: string
|
|
4
|
+
function tryParseContentLines(record: Record<string, unknown>, key: string): void {
|
|
5
5
|
const val = record[key];
|
|
6
6
|
if (typeof val !== "string") return;
|
|
7
7
|
try {
|
|
@@ -11,7 +11,7 @@ function tryParseContentLines(record: Record<string, unknown>, key: string, labe
|
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
13
|
} catch {
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
}
|
|
16
16
|
throw new Error(CONTENT_LINES_NOT_STRING_MSG);
|
|
17
17
|
}
|
|
@@ -43,7 +43,7 @@ function normalizeField(
|
|
|
43
43
|
record[to] = [parsed];
|
|
44
44
|
}
|
|
45
45
|
} catch {
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
if (from !== to) delete record[from];
|
|
@@ -59,7 +59,7 @@ export function normReq(input: unknown): unknown {
|
|
|
59
59
|
normalizeFilePath(record);
|
|
60
60
|
|
|
61
61
|
if (has(record, "content_lines") && typeof record.content_lines === "string") {
|
|
62
|
-
tryParseContentLines(record, "content_lines"
|
|
62
|
+
tryParseContentLines(record, "content_lines");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
normalizeField(record, "changes", "changes");
|
|
@@ -69,7 +69,7 @@ export function normReq(input: unknown): unknown {
|
|
|
69
69
|
for (let i = 0; i < record.changes.length; i++) {
|
|
70
70
|
const item = record.changes[i];
|
|
71
71
|
if (isRec(item) && has(item, "content_lines") && typeof item.content_lines === "string") {
|
|
72
|
-
tryParseContentLines(item, "content_lines"
|
|
72
|
+
tryParseContentLines(item, "content_lines");
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
}
|
package/src/replace-response.ts
CHANGED