pi-hashline-edit-pro 0.15.1 → 0.15.3
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/package.json +1 -1
- package/src/hashline/hash.ts +46 -24
- package/src/replace-diff.ts +34 -17
- package/src/replace-flat.ts +2 -1
- package/src/replace-response.ts +43 -42
- package/src/replace.ts +13 -4
package/package.json
CHANGED
package/src/hashline/hash.ts
CHANGED
|
@@ -124,7 +124,7 @@ export function _lineHashesPure(content: string): string[] {
|
|
|
124
124
|
export async function lineHashes(
|
|
125
125
|
content: string,
|
|
126
126
|
path?: string,
|
|
127
|
-
previous?: { content: string; hashes: string[] },
|
|
127
|
+
previous?: { content: string; hashes: string[]; removedHashes?: Set<string> },
|
|
128
128
|
): Promise<string[]> {
|
|
129
129
|
if (!path) {
|
|
130
130
|
return _lineHashesPure(content);
|
|
@@ -137,6 +137,7 @@ export async function lineHashes(
|
|
|
137
137
|
const newHashes = mapStableHashes(
|
|
138
138
|
previous.content, previous.hashes,
|
|
139
139
|
content,
|
|
140
|
+
previous.removedHashes,
|
|
140
141
|
);
|
|
141
142
|
store.snapshots[path] = { content, hashes: newHashes };
|
|
142
143
|
await saveHashStore(store);
|
|
@@ -157,42 +158,63 @@ export async function lineHashes(
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
161
|
+
* Maps old hashes to new positions using hash-aware content matching.
|
|
162
|
+
* Unlike Diff.diffLines (which is content-only and cannot distinguish
|
|
163
|
+
* identical lines at different positions), this algorithm uses the
|
|
164
|
+
* removedHashes set to disambiguate: when a line appears multiple times
|
|
165
|
+
* in the old content and one occurrence was targeted by the edit,
|
|
166
|
+
* the surviving occurrence is matched to the non-removed hash.
|
|
167
|
+
* New/changed lines allocate fresh hashes with collision avoidance.
|
|
162
168
|
*/
|
|
163
169
|
function mapStableHashes(
|
|
164
170
|
oldContent: string,
|
|
165
171
|
oldHashes: string[],
|
|
166
172
|
newContent: string,
|
|
173
|
+
removedHashes?: Set<string>,
|
|
167
174
|
): string[] {
|
|
168
175
|
const newLines = newContent.split("\n");
|
|
169
176
|
const newHashes = new Array<string>(newLines.length);
|
|
170
177
|
const used = new Set<string>();
|
|
171
178
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
for (
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
if (part.added) {
|
|
183
|
-
newIdx += count;
|
|
184
|
-
} else if (part.removed) {
|
|
185
|
-
oldIdx += count;
|
|
179
|
+
// Build a map from line content to list of (index, hash) for the old content.
|
|
180
|
+
// We process occurrences left-to-right so that matching preserves order.
|
|
181
|
+
const contentMap = new Map<string, { index: number; hash: string }[]>();
|
|
182
|
+
const oldLines = oldContent.split("\n");
|
|
183
|
+
for (let i = 0; i < oldLines.length; i++) {
|
|
184
|
+
const line = oldLines[i]!;
|
|
185
|
+
const entry = { index: i, hash: oldHashes[i]! };
|
|
186
|
+
const list = contentMap.get(line);
|
|
187
|
+
if (list) {
|
|
188
|
+
list.push(entry);
|
|
186
189
|
} else {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
contentMap.set(line, [entry]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Match each new line to an old occurrence by content.
|
|
195
|
+
// For lines with duplicate content, prefer occurrences whose hash
|
|
196
|
+
// was NOT targeted by the edit (those are the survivors).
|
|
197
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
198
|
+
const line = newLines[i]!;
|
|
199
|
+
const candidates = contentMap.get(line);
|
|
200
|
+
if (!candidates || candidates.length === 0) continue;
|
|
201
|
+
|
|
202
|
+
// Find the best match: prefer a non-removed occurrence.
|
|
203
|
+
// If all remaining occurrences are removed, use the first one anyway
|
|
204
|
+
// (it will get a fresh hash in the fill step since its hash is in used+removed).
|
|
205
|
+
let bestIdx = 0;
|
|
206
|
+
if (removedHashes && removedHashes.size > 0) {
|
|
207
|
+
for (let j = 0; j < candidates.length; j++) {
|
|
208
|
+
if (!removedHashes.has(candidates[j]!.hash)) {
|
|
209
|
+
bestIdx = j;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
194
212
|
}
|
|
195
213
|
}
|
|
214
|
+
|
|
215
|
+
const match = candidates.splice(bestIdx, 1)[0]!;
|
|
216
|
+
newHashes[i] = match.hash;
|
|
217
|
+
used.add(match.hash);
|
|
196
218
|
}
|
|
197
219
|
|
|
198
220
|
// Fill remaining (new/changed) lines with fresh hashes
|
package/src/replace-diff.ts
CHANGED
|
@@ -45,10 +45,25 @@ export function genDiff(
|
|
|
45
45
|
newContent: string,
|
|
46
46
|
contextLines = 2,
|
|
47
47
|
newContentHashes?: string[],
|
|
48
|
+
oldHashes?: string[],
|
|
48
49
|
): { diff: string; firstChangedLine: number | undefined } {
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// Annotate each line with its hash so Diff.diffLines can distinguish
|
|
51
|
+
// identical lines at different positions. The \0 separator cannot appear
|
|
52
|
+
// in valid text file content.
|
|
53
|
+
const oldLines = oldContent.split("\n");
|
|
54
|
+
const effectiveOldHashes = oldHashes ?? _lineHashesPure(oldContent);
|
|
55
|
+
const annotatedOld = oldLines
|
|
56
|
+
.map((line, i) => `${line}\0${effectiveOldHashes[i] ?? ""}`)
|
|
57
|
+
.join("\n");
|
|
58
|
+
|
|
59
|
+
const newLines = newContent.split("\n");
|
|
51
60
|
const effectiveNewHashes = newContentHashes ?? _lineHashesPure(newContent);
|
|
61
|
+
const annotatedNew = newLines
|
|
62
|
+
.map((line, i) => `${line}\0${effectiveNewHashes[i] ?? ""}`)
|
|
63
|
+
.join("\n");
|
|
64
|
+
|
|
65
|
+
const parts = Diff.diffLines(annotatedOld, annotatedNew);
|
|
66
|
+
const output: string[] = [];
|
|
52
67
|
let oldLineNum = 1;
|
|
53
68
|
let newLineNum = 1;
|
|
54
69
|
let lastWasChange = false;
|
|
@@ -58,18 +73,21 @@ export function genDiff(
|
|
|
58
73
|
const part = parts[i]!;
|
|
59
74
|
const raw = part.value.split("\n");
|
|
60
75
|
if (raw[raw.length - 1] === "") raw.pop();
|
|
76
|
+
// Strip \0hash annotation from each line for display
|
|
77
|
+
const displayLines = raw.map((l) => {
|
|
78
|
+
const nullIdx = l.indexOf("\0");
|
|
79
|
+
return nullIdx >= 0 ? l.slice(0, nullIdx) : l;
|
|
80
|
+
});
|
|
61
81
|
|
|
62
82
|
if (part.added || part.removed) {
|
|
63
83
|
if (firstChangedLine === undefined) firstChangedLine = newLineNum;
|
|
64
|
-
for (
|
|
84
|
+
for (let k = 0; k < displayLines.length; k++) {
|
|
65
85
|
if (part.added) {
|
|
66
86
|
const hash = effectiveNewHashes[newLineNum - 1];
|
|
67
|
-
output.push(fmtDiffLine("+",
|
|
87
|
+
output.push(fmtDiffLine("+", displayLines[k]!, hash));
|
|
68
88
|
newLineNum++;
|
|
69
89
|
} else {
|
|
70
|
-
output.push(
|
|
71
|
-
fmtDiffLine("-", line, undefined),
|
|
72
|
-
);
|
|
90
|
+
output.push(fmtDiffLine("-", displayLines[k]!, undefined));
|
|
73
91
|
oldLineNum++;
|
|
74
92
|
}
|
|
75
93
|
}
|
|
@@ -80,18 +98,18 @@ export function genDiff(
|
|
|
80
98
|
const nextPartIsChange =
|
|
81
99
|
i < parts.length - 1 && (parts[i + 1]!.added || parts[i + 1]!.removed);
|
|
82
100
|
if (lastWasChange || nextPartIsChange) {
|
|
83
|
-
let linesToShow =
|
|
101
|
+
let linesToShow = displayLines;
|
|
84
102
|
let skipStart = 0;
|
|
85
103
|
let skipEnd = 0;
|
|
86
104
|
let skipMiddle = 0;
|
|
87
105
|
|
|
88
106
|
if (!lastWasChange) {
|
|
89
|
-
skipStart = Math.max(0,
|
|
90
|
-
linesToShow =
|
|
91
|
-
} else if (nextPartIsChange &&
|
|
92
|
-
const tail =
|
|
93
|
-
linesToShow = [...
|
|
94
|
-
skipMiddle =
|
|
107
|
+
skipStart = Math.max(0, displayLines.length - contextLines);
|
|
108
|
+
linesToShow = displayLines.slice(skipStart);
|
|
109
|
+
} else if (nextPartIsChange && displayLines.length > contextLines * 2) {
|
|
110
|
+
const tail = displayLines.slice(-contextLines);
|
|
111
|
+
linesToShow = [...displayLines.slice(0, contextLines), "__ELLIPSIS__", ...tail];
|
|
112
|
+
skipMiddle = displayLines.length - contextLines * 2;
|
|
95
113
|
} else if (linesToShow.length > contextLines) {
|
|
96
114
|
skipEnd = linesToShow.length - contextLines;
|
|
97
115
|
linesToShow = linesToShow.slice(0, contextLines);
|
|
@@ -111,13 +129,12 @@ export function genDiff(
|
|
|
111
129
|
}
|
|
112
130
|
const hash = effectiveNewHashes[newLineNum - 1];
|
|
113
131
|
output.push(fmtDiffLine(" ", line, hash));
|
|
114
|
-
|
|
115
132
|
oldLineNum++;
|
|
116
133
|
newLineNum++;
|
|
117
134
|
}
|
|
118
135
|
} else {
|
|
119
|
-
oldLineNum +=
|
|
120
|
-
newLineNum +=
|
|
136
|
+
oldLineNum += displayLines.length;
|
|
137
|
+
newLineNum += displayLines.length;
|
|
121
138
|
}
|
|
122
139
|
lastWasChange = false;
|
|
123
140
|
}
|
package/src/replace-flat.ts
CHANGED
|
@@ -238,6 +238,7 @@ export function buildToolDef(): ToolDef {
|
|
|
238
238
|
|
|
239
239
|
const {
|
|
240
240
|
originalNormalized,
|
|
241
|
+
originalHashes,
|
|
241
242
|
result,
|
|
242
243
|
bom,
|
|
243
244
|
originalEnding,
|
|
@@ -294,13 +295,13 @@ export function buildToolDef(): ToolDef {
|
|
|
294
295
|
const successInput = {
|
|
295
296
|
path,
|
|
296
297
|
originalNormalized,
|
|
298
|
+
originalHashes,
|
|
297
299
|
result,
|
|
298
300
|
resultHashes,
|
|
299
301
|
warnings,
|
|
300
302
|
snapshotId: updatedSnapshotId,
|
|
301
303
|
editMeta,
|
|
302
304
|
};
|
|
303
|
-
|
|
304
305
|
return buildChanged(successInput);
|
|
305
306
|
});
|
|
306
307
|
},
|
package/src/replace-response.ts
CHANGED
|
@@ -40,13 +40,14 @@ export interface NoopInput {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export interface SuccessInput {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
path: string;
|
|
44
|
+
originalNormalized: string;
|
|
45
|
+
originalHashes: string[];
|
|
46
|
+
result: string;
|
|
47
|
+
resultHashes: string[];
|
|
48
|
+
warnings: string[] | undefined;
|
|
49
|
+
snapshotId: string;
|
|
50
|
+
editMeta: RMeta;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
function cntDiff(diff: string, marker: "+" | "-"): number {
|
|
@@ -139,39 +140,39 @@ export function buildNoop(input: NoopInput): TResult {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
export function buildChanged(input: SuccessInput): TResult {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
143
|
+
const { path, result, warnings, snapshotId, originalNormalized, originalHashes, editMeta, resultHashes } = input;
|
|
144
|
+
|
|
145
|
+
const resultLines = visLines(result);
|
|
146
|
+
const diffResult = genDiff(originalNormalized, result, 2, resultHashes, originalHashes);
|
|
147
|
+
const addedLines = cntDiff(diffResult.diff, "+");
|
|
148
|
+
const removedLines = cntDiff(diffResult.diff, "-");
|
|
149
|
+
const warningsBlock = warnBlock(warnings);
|
|
150
|
+
const successPrefix = `Successfully replaced in ${path}.`;
|
|
151
|
+
const text = resultLines.length === 0
|
|
152
|
+
? "File is empty. Use replace to insert content."
|
|
153
|
+
: warningsBlock
|
|
154
|
+
? `${successPrefix}${warningsBlock}`
|
|
155
|
+
: successPrefix;
|
|
156
|
+
|
|
157
|
+
const metrics = buildM({
|
|
158
|
+
classification: "applied",
|
|
159
|
+
editsAttempted: editMeta.editsAttempted,
|
|
160
|
+
noopEditsCount: editMeta.noopEditsCount,
|
|
161
|
+
warningsCount: warnings?.length ?? 0,
|
|
162
|
+
firstChangedLine: editMeta.firstChangedLine,
|
|
163
|
+
lastChangedLine: editMeta.lastChangedLine,
|
|
164
|
+
addedLines,
|
|
165
|
+
removedLines,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text", text }],
|
|
170
|
+
details: {
|
|
171
|
+
diff: diffResult.diff,
|
|
172
|
+
firstChangedLine:
|
|
173
|
+
editMeta.firstChangedLine ?? diffResult.firstChangedLine,
|
|
174
|
+
snapshotId,
|
|
175
|
+
metrics,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
177
178
|
}
|
package/src/replace.ts
CHANGED
|
@@ -165,10 +165,18 @@ export async function execPipeline(
|
|
|
165
165
|
|
|
166
166
|
const result = anchorResult.content;
|
|
167
167
|
|
|
168
|
-
//
|
|
168
|
+
// Collect hashes targeted by the edit for hash-aware diff disambiguation
|
|
169
|
+
const removedHashes = new Set<string>();
|
|
170
|
+
for (const edit of resolved) {
|
|
171
|
+
removedHashes.add(edit.hash_range_inclusive[0].hash);
|
|
172
|
+
removedHashes.add(edit.hash_range_inclusive[1].hash);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Compute stable result hashes using hash-aware preservation
|
|
169
176
|
const resultHashes = await lineHashes(result, absolutePath, {
|
|
170
177
|
content: originalNormalized,
|
|
171
178
|
hashes: originalHashes,
|
|
179
|
+
removedHashes,
|
|
172
180
|
});
|
|
173
181
|
|
|
174
182
|
// Format boundary warnings using stable result hashes
|
|
@@ -220,7 +228,7 @@ export async function compPreview(
|
|
|
220
228
|
try {
|
|
221
229
|
const normalized = normReq(request);
|
|
222
230
|
assertReq(normalized);
|
|
223
|
-
const { path, originalNormalized, result, resultHashes } = await execPipeline(
|
|
231
|
+
const { path, originalNormalized, originalHashes, result, resultHashes } = await execPipeline(
|
|
224
232
|
normalized,
|
|
225
233
|
cwd,
|
|
226
234
|
constants.R_OK,
|
|
@@ -232,7 +240,7 @@ export async function compPreview(
|
|
|
232
240
|
};
|
|
233
241
|
}
|
|
234
242
|
|
|
235
|
-
return { diff: genDiff(originalNormalized, result, 4, resultHashes).diff };
|
|
243
|
+
return { diff: genDiff(originalNormalized, result, 4, resultHashes, originalHashes).diff };
|
|
236
244
|
} catch (error: unknown) {
|
|
237
245
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
238
246
|
}
|
|
@@ -383,6 +391,7 @@ export function buildToolDef(): ToolDef {
|
|
|
383
391
|
|
|
384
392
|
const {
|
|
385
393
|
originalNormalized,
|
|
394
|
+
originalHashes,
|
|
386
395
|
result,
|
|
387
396
|
bom,
|
|
388
397
|
originalEnding,
|
|
@@ -441,13 +450,13 @@ export function buildToolDef(): ToolDef {
|
|
|
441
450
|
const successInput = {
|
|
442
451
|
path,
|
|
443
452
|
originalNormalized,
|
|
453
|
+
originalHashes,
|
|
444
454
|
result,
|
|
445
455
|
resultHashes,
|
|
446
456
|
warnings,
|
|
447
457
|
snapshotId: updatedSnapshotId,
|
|
448
458
|
editMeta,
|
|
449
459
|
};
|
|
450
|
-
|
|
451
460
|
return buildChanged(successInput);
|
|
452
461
|
});
|
|
453
462
|
},
|