pi-hashline-edit-pro 0.16.12 → 0.16.13
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/apply.ts +20 -4
- package/src/hashline/resolve.ts +29 -5
- package/src/replace-normalize.ts +27 -9
- package/src/replace.ts +10 -4
package/package.json
CHANGED
package/src/hashline/apply.ts
CHANGED
|
@@ -14,6 +14,20 @@ import {
|
|
|
14
14
|
} from "./resolve";
|
|
15
15
|
import { visLines } from "../utils";
|
|
16
16
|
|
|
17
|
+
function lastNonEmptyIndex(lines: string[]): number {
|
|
18
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
19
|
+
if (lines[i]!.length > 0) return i;
|
|
20
|
+
}
|
|
21
|
+
return -1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function firstNonEmptyIndex(lines: string[]): number {
|
|
25
|
+
for (let i = 0; i < lines.length; i++) {
|
|
26
|
+
if (lines[i]!.length > 0) return i;
|
|
27
|
+
}
|
|
28
|
+
return -1;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
type LIdx = {
|
|
18
32
|
fileLines: string[];
|
|
19
33
|
lineStarts: number[];
|
|
@@ -306,13 +320,15 @@ export function applyEdits(
|
|
|
306
320
|
const edit = correctedEdits[bw.editIndex];
|
|
307
321
|
if (!edit) continue;
|
|
308
322
|
if (bw.kind === "trailing") {
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
323
|
+
const idx = lastNonEmptyIndex(edit.content_lines);
|
|
324
|
+
if (idx >= 0) {
|
|
325
|
+
const removed = edit.content_lines.splice(idx, 1)[0];
|
|
311
326
|
autoFixes.push({ kind: "trailing", editIndex: bw.editIndex, removedLine: removed });
|
|
312
327
|
}
|
|
313
328
|
} else {
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
329
|
+
const idx = firstNonEmptyIndex(edit.content_lines);
|
|
330
|
+
if (idx >= 0) {
|
|
331
|
+
const removed = edit.content_lines.splice(idx, 1)[0];
|
|
316
332
|
autoFixes.push({ kind: "leading", editIndex: bw.editIndex, removedLine: removed });
|
|
317
333
|
}
|
|
318
334
|
}
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function fmtMismatch(
|
|
|
94
94
|
const refList = notFound.map((m) => `"${m.ref.hash}"`).join(", ");
|
|
95
95
|
if (notFound.length > 0) {
|
|
96
96
|
out.push(
|
|
97
|
-
`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. Call read() to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into hash_range_inclusive of your next replace call.`
|
|
97
|
+
`[E_STALE_ANCHOR] ${notFound.length} stale anchor${notFound.length > 1 ? "s" : ""}${filePath ? ` in ${filePath}` : ""}: ${refList}. The file content has changed since those anchors were read. Call read() to get fresh anchors, then copy the 3-char HASH of the start and end of the range you are replacing into hash_range_inclusive of your next replace call.`
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
100
|
if (ambiguous.length > 0) {
|
|
@@ -153,9 +153,19 @@ function assertItem(edit: Record<string, unknown>, index: number): void {
|
|
|
153
153
|
if ("content_lines" in edit && !isStrArr(edit.content_lines)) {
|
|
154
154
|
const val = edit.content_lines;
|
|
155
155
|
if (typeof val === "string") {
|
|
156
|
-
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(val);
|
|
158
|
+
if (Array.isArray(parsed)) {
|
|
159
|
+
edit.content_lines = parsed;
|
|
160
|
+
} else {
|
|
161
|
+
throw new Error(CONTENT_LINES_NOT_STRING_MSG);
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
throw new Error(CONTENT_LINES_NOT_STRING_MSG);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
throw new Error(`[E_BAD_SHAPE] Edit ${index} field "content_lines" must be a string array.`);
|
|
157
168
|
}
|
|
158
|
-
throw new Error(`[E_BAD_SHAPE] Edit ${index} field "content_lines" must be a string array.`);
|
|
159
169
|
}
|
|
160
170
|
if (!isStrPair(edit.hash_range_inclusive)) {
|
|
161
171
|
throw new Error(
|
|
@@ -230,6 +240,20 @@ export function descEdit(edit: RHEdit): string {
|
|
|
230
240
|
return `replace ${edit.hash_range_inclusive[0].hash}-${edit.hash_range_inclusive[1].hash}`;
|
|
231
241
|
}
|
|
232
242
|
|
|
243
|
+
function lastNonEmpty(lines: string[]): string | undefined {
|
|
244
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
245
|
+
if (lines[i]!.length > 0) return lines[i]!;
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function firstNonEmpty(lines: string[]): string | undefined {
|
|
251
|
+
for (let i = 0; i < lines.length; i++) {
|
|
252
|
+
if (lines[i]!.length > 0) return lines[i]!;
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
233
257
|
function checkBoundaryDup(
|
|
234
258
|
adjacentLine: string | undefined,
|
|
235
259
|
replacementEdge: string | undefined,
|
|
@@ -296,11 +320,11 @@ export function valEdits(
|
|
|
296
320
|
}
|
|
297
321
|
const endLine = endResolved.line;
|
|
298
322
|
const nextLine = fileLines[endLine];
|
|
299
|
-
const replacementLastLine = edit.content_lines
|
|
323
|
+
const replacementLastLine = lastNonEmpty(edit.content_lines);
|
|
300
324
|
const trailing = checkBoundaryDup(nextLine, replacementLastLine, "trailing", endLine, resolved.length);
|
|
301
325
|
if (trailing) boundaryWarnings.push(trailing);
|
|
302
326
|
const prevLine = fileLines[startResolved.line - 2];
|
|
303
|
-
const replacementFirstLine = edit.content_lines
|
|
327
|
+
const replacementFirstLine = firstNonEmpty(edit.content_lines);
|
|
304
328
|
const leading = checkBoundaryDup(prevLine, replacementFirstLine, "leading", startResolved.line - 2, resolved.length);
|
|
305
329
|
if (leading) boundaryWarnings.push(leading);
|
|
306
330
|
resolved.push({
|
package/src/replace-normalize.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { isRec, has } from "./utils";
|
|
2
2
|
import { CONTENT_LINES_NOT_STRING_MSG } from "./constants";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
|
|
4
|
+
function tryParseContentLines(record: Record<string, unknown>, key: string, label: string): void {
|
|
5
|
+
const val = record[key];
|
|
6
|
+
if (typeof val !== "string") return;
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(val);
|
|
9
|
+
if (Array.isArray(parsed)) {
|
|
10
|
+
record[key] = parsed;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
} catch {
|
|
14
|
+
// fall through to error
|
|
15
|
+
}
|
|
16
|
+
throw new Error(CONTENT_LINES_NOT_STRING_MSG);
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
export function normalizeFilePath(record: Record<string, unknown>): void {
|
|
@@ -27,6 +34,17 @@ function normalizeField(
|
|
|
27
34
|
record[to] = raw;
|
|
28
35
|
} else if (isRec(raw)) {
|
|
29
36
|
record[to] = [raw];
|
|
37
|
+
} else if (typeof raw === "string") {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (Array.isArray(parsed)) {
|
|
41
|
+
record[to] = parsed;
|
|
42
|
+
} else if (isRec(parsed)) {
|
|
43
|
+
record[to] = [parsed];
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// not valid JSON, leave as-is for downstream validation
|
|
47
|
+
}
|
|
30
48
|
}
|
|
31
49
|
if (from !== to) delete record[from];
|
|
32
50
|
}
|
|
@@ -41,7 +59,7 @@ export function normReq(input: unknown): unknown {
|
|
|
41
59
|
normalizeFilePath(record);
|
|
42
60
|
|
|
43
61
|
if (has(record, "content_lines") && typeof record.content_lines === "string") {
|
|
44
|
-
|
|
62
|
+
tryParseContentLines(record, "content_lines", "Top-level");
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
normalizeField(record, "changes", "changes");
|
|
@@ -51,7 +69,7 @@ export function normReq(input: unknown): unknown {
|
|
|
51
69
|
for (let i = 0; i < record.changes.length; i++) {
|
|
52
70
|
const item = record.changes[i];
|
|
53
71
|
if (isRec(item) && has(item, "content_lines") && typeof item.content_lines === "string") {
|
|
54
|
-
|
|
72
|
+
tryParseContentLines(item, "content_lines", `changes[${i}]`);
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
75
|
}
|
package/src/replace.ts
CHANGED
|
@@ -113,10 +113,11 @@ interface PipelineResult {
|
|
|
113
113
|
resultHashes: string[];
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
const ROOT_KS = new Set(["path", "changes"]);
|
|
116
|
+
const ROOT_KS = new Set(["path", "changes", "content_lines", "hash_range_inclusive"]);
|
|
117
117
|
|
|
118
118
|
export function assertReq(
|
|
119
119
|
request: unknown,
|
|
120
|
+
flat?: boolean
|
|
120
121
|
): asserts request is ReqParams {
|
|
121
122
|
if (!isRec(request)) {
|
|
122
123
|
throw new Error("[E_BAD_SHAPE] Edit request must be an object.");
|
|
@@ -137,10 +138,14 @@ export function assertReq(
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
if (!Array.isArray(request.changes)) {
|
|
141
|
+
if (flat) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
'[E_BAD_SHAPE] Edit request requires both "content_lines" and "hash_range_inclusive" at the top level.',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
140
146
|
throw new Error('[E_BAD_SHAPE] Edit request requires a "changes" array. Each change is { content_lines: [...], hash_range_inclusive: ["<START>", "<END>"] }.');
|
|
141
147
|
}
|
|
142
148
|
}
|
|
143
|
-
|
|
144
149
|
export async function execPipeline(
|
|
145
150
|
params: ReqParams,
|
|
146
151
|
cwd: string,
|
|
@@ -217,10 +222,11 @@ export async function execPipeline(
|
|
|
217
222
|
export async function compPreview(
|
|
218
223
|
request: unknown,
|
|
219
224
|
cwd: string,
|
|
225
|
+
flat?: boolean
|
|
220
226
|
): Promise<RPreview> {
|
|
221
227
|
try {
|
|
222
228
|
const normalized = normReq(request);
|
|
223
|
-
assertReq(normalized);
|
|
229
|
+
assertReq(normalized, flat);
|
|
224
230
|
const { path, originalNormalized, originalHashes, result, resultHashes } = await execPipeline(
|
|
225
231
|
normalized,
|
|
226
232
|
cwd,
|
|
@@ -352,7 +358,7 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
352
358
|
context.state.preview = undefined;
|
|
353
359
|
const previewGeneration = (context.state.previewGeneration ?? 0) + 1;
|
|
354
360
|
context.state.previewGeneration = previewGeneration;
|
|
355
|
-
compPreview(previewInput, context.cwd)
|
|
361
|
+
compPreview(previewInput, context.cwd, opts.flat)
|
|
356
362
|
.then((preview) => {
|
|
357
363
|
if (
|
|
358
364
|
context.state.argsKey === argsKey &&
|