pi-hashline-edit-pro 0.13.3 → 0.13.4
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 +7 -7
- package/package.json +1 -1
- package/src/hashline/apply.ts +6 -4
- package/src/replace-normalize.ts +28 -26
- package/src/replace-response.ts +5 -2
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.
|
|
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. Every line gets a unique content hash, so edits stay precise and stale anchors are caught before they reach the file.
|
|
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
|
|
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 with 3-character hashes and collision resolution for unique per-line anchors.
|
|
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
|
|
|
@@ -12,8 +12,8 @@ The original uses 2-character hashes of a 16-character alphabet, with the hash b
|
|
|
12
12
|
|
|
13
13
|
This fork makes two changes that compound:
|
|
14
14
|
|
|
15
|
-
1. **
|
|
16
|
-
2. **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
|
|
15
|
+
1. **3-character hash length** over a 64-char URL-safe base64 alphabet (up from 2 characters in the upstream), expanding the hash space from 256 to 262,144 buckets.
|
|
16
|
+
2. **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 gets a unique anchor, even within a 3-character hash space. Two byte-identical lines (e.g. repeated `}` or repeated `import` statements) get different hashes automatically.
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
@@ -49,7 +49,7 @@ szJ│ console.log("world");
|
|
|
49
49
|
_zl│}
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
- `HASH` is a 3-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3`).
|
|
52
|
+
- `HASH` is a 3-character content hash from the URL-safe base64 alphabet `A-Za-z0-9-_` (e.g. `aB3`). See [Hashing](#hashing) for details.
|
|
53
53
|
|
|
54
54
|
Optional parameters:
|
|
55
55
|
|
|
@@ -82,7 +82,7 @@ Replaces using the `HASH│content` anchors from `read` output to target lines p
|
|
|
82
82
|
|
|
83
83
|
### Chained edits
|
|
84
84
|
|
|
85
|
-
After a successful replace, the response
|
|
85
|
+
After a successful replace, the response confirms with `Successfully replaced in {path}.` (warnings are still shown if present). To get fresh anchors for follow-up edits, call `read` on the file first. This avoids token overhead from re-displaying content the model already knows.
|
|
86
86
|
|
|
87
87
|
### Auto-read after write
|
|
88
88
|
|
|
@@ -122,7 +122,7 @@ The runtime always precomputes the full per-line hash array for a file via `line
|
|
|
122
122
|
|
|
123
123
|
### Bare-prefix detector
|
|
124
124
|
|
|
125
|
-
With the `│` delimiter format, the bare-prefix detector regex `^\s*[A-Za-z0-9_\-]{3}│` is highly specific. It only matches lines starting with
|
|
125
|
+
With the `│` delimiter format, the bare-prefix detector regex `^\s*[A-Za-z0-9_\-]{3}│` is highly specific. It only matches lines starting with a hash-like prefix. 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.
|
|
126
126
|
|
|
127
127
|
## Development
|
|
128
128
|
|
package/package.json
CHANGED
package/src/hashline/apply.ts
CHANGED
|
@@ -92,11 +92,13 @@ function resToSpan(
|
|
|
92
92
|
return null;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
const label = descEdit(edit);
|
|
96
|
+
|
|
95
97
|
if (edit.content_lines.length > 0) {
|
|
96
98
|
return {
|
|
97
99
|
kind: "replace",
|
|
98
100
|
index,
|
|
99
|
-
label
|
|
101
|
+
label,
|
|
100
102
|
start: lineStarts[startLine - 1]!,
|
|
101
103
|
end: lineStarts[endLine - 1]! + fileLines[endLine - 1]!.length,
|
|
102
104
|
replacement: edit.content_lines.join("\n"),
|
|
@@ -107,7 +109,7 @@ function resToSpan(
|
|
|
107
109
|
return {
|
|
108
110
|
kind: "replace",
|
|
109
111
|
index,
|
|
110
|
-
label
|
|
112
|
+
label,
|
|
111
113
|
start: 0,
|
|
112
114
|
end: content.length,
|
|
113
115
|
replacement: "",
|
|
@@ -118,7 +120,7 @@ function resToSpan(
|
|
|
118
120
|
return {
|
|
119
121
|
kind: "replace",
|
|
120
122
|
index,
|
|
121
|
-
label
|
|
123
|
+
label,
|
|
122
124
|
start: lineStarts[startLine - 1]!,
|
|
123
125
|
end: lineStarts[endLine]!,
|
|
124
126
|
replacement: "",
|
|
@@ -128,7 +130,7 @@ function resToSpan(
|
|
|
128
130
|
return {
|
|
129
131
|
kind: "replace",
|
|
130
132
|
index,
|
|
131
|
-
label
|
|
133
|
+
label,
|
|
132
134
|
start: Math.max(0, lineStarts[startLine - 1]! - 1),
|
|
133
135
|
end: lineStarts[endLine - 1]! + fileLines[endLine - 1]!.length,
|
|
134
136
|
replacement: "",
|
package/src/replace-normalize.ts
CHANGED
|
@@ -27,6 +27,32 @@ function coerceEditArray(items: unknown[]): unknown[] {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Normalizes a field from `from` to `to`: JSON-string arrays → real arrays,
|
|
32
|
+
* single objects → wrapped in array. Shared by the `changes` and `edits`
|
|
33
|
+
* normalization branches.
|
|
34
|
+
*/
|
|
35
|
+
function normalizeField(
|
|
36
|
+
record: Record<string, unknown>,
|
|
37
|
+
from: string,
|
|
38
|
+
to: string,
|
|
39
|
+
): void {
|
|
40
|
+
if (!has(record, from)) return;
|
|
41
|
+
const raw = tryParseJSON(record[from], Array.isArray) ?? record[from];
|
|
42
|
+
if (Array.isArray(raw)) {
|
|
43
|
+
record[to] = coerceEditArray(raw);
|
|
44
|
+
} else {
|
|
45
|
+
const single =
|
|
46
|
+
typeof raw === "string"
|
|
47
|
+
? tryParseJSON(raw, isRec)
|
|
48
|
+
: isRec(raw)
|
|
49
|
+
? raw
|
|
50
|
+
: undefined;
|
|
51
|
+
if (single) record[to] = coerceEditArray([single]);
|
|
52
|
+
}
|
|
53
|
+
if (from !== to) delete record[from];
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
export function normReq(input: unknown): unknown {
|
|
31
57
|
if (!isRec(input)) {
|
|
32
58
|
return input;
|
|
@@ -39,32 +65,8 @@ export function normReq(input: unknown): unknown {
|
|
|
39
65
|
delete record.file_path;
|
|
40
66
|
}
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (hasChangesField) {
|
|
46
|
-
const raw = tryParseJSON(record.changes, Array.isArray) ?? record.changes;
|
|
47
|
-
if (Array.isArray(raw)) {
|
|
48
|
-
record.changes = coerceEditArray(raw);
|
|
49
|
-
} else {
|
|
50
|
-
// Single object (JSON string or literal) → wrap in array
|
|
51
|
-
const single = typeof raw === "string"
|
|
52
|
-
? tryParseJSON(raw, isRec)
|
|
53
|
-
: isRec(raw) ? raw : undefined;
|
|
54
|
-
if (single) record.changes = coerceEditArray([single]);
|
|
55
|
-
}
|
|
56
|
-
} else if (hasEditsField) {
|
|
57
|
-
const raw = tryParseJSON(record.edits, Array.isArray) ?? record.edits;
|
|
58
|
-
if (Array.isArray(raw)) {
|
|
59
|
-
record.changes = coerceEditArray(raw);
|
|
60
|
-
} else {
|
|
61
|
-
const single = typeof raw === "string"
|
|
62
|
-
? tryParseJSON(raw, isRec)
|
|
63
|
-
: isRec(raw) ? raw : undefined;
|
|
64
|
-
if (single) record.changes = coerceEditArray([single]);
|
|
65
|
-
}
|
|
66
|
-
delete record.edits;
|
|
67
|
-
}
|
|
68
|
+
normalizeField(record, "changes", "changes");
|
|
69
|
+
normalizeField(record, "edits", "changes");
|
|
68
70
|
|
|
69
71
|
return record;
|
|
70
72
|
}
|
package/src/replace-response.ts
CHANGED
|
@@ -142,7 +142,7 @@ export function buildNoop(input: NoopInput): TResult {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
export function buildChanged(input: SuccessInput): TResult {
|
|
145
|
-
const { result, warnings, snapshotId, originalNormalized, editMeta } = input;
|
|
145
|
+
const { path, result, warnings, snapshotId, originalNormalized, editMeta } = input;
|
|
146
146
|
|
|
147
147
|
const resultLines = visLines(result);
|
|
148
148
|
const resultHashes = input.resultHashes ?? lineHashes(result);
|
|
@@ -150,9 +150,12 @@ export function buildChanged(input: SuccessInput): TResult {
|
|
|
150
150
|
const addedLines = cntDiff(diffResult.diff, "+");
|
|
151
151
|
const removedLines = cntDiff(diffResult.diff, "-");
|
|
152
152
|
const warningsBlock = warnBlock(warnings);
|
|
153
|
+
const successPrefix = `Successfully replaced in ${path}.`;
|
|
153
154
|
const text = resultLines.length === 0
|
|
154
155
|
? "File is empty. Use replace to insert content."
|
|
155
|
-
: warningsBlock
|
|
156
|
+
: warningsBlock
|
|
157
|
+
? `${successPrefix}${warningsBlock}`
|
|
158
|
+
: successPrefix;
|
|
156
159
|
|
|
157
160
|
const metrics = buildM({
|
|
158
161
|
classification: "applied",
|