pi-hashline-edit-pro 0.16.13 → 0.16.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "0.16.13",
3
+ "version": "0.16.14",
4
4
  "description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 18-bit, perfect hashing)",
5
5
  "main": "index.ts",
6
6
  "repository": {
@@ -1,4 +1,4 @@
1
- import { abortIf } from "../utils";
1
+ import { abortIf, visLines, lastNonEmptyIndex, firstNonEmptyIndex } from "../utils";
2
2
  import { _lineHashesPure, HASH_SEP } from "./hash";
3
3
  import {
4
4
  valEdits,
@@ -12,21 +12,6 @@ import {
12
12
  type BDupWarn,
13
13
  type AutoFix,
14
14
  } from "./resolve";
15
- import { visLines } from "../utils";
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
15
 
31
16
  type LIdx = {
32
17
  fileLines: string[];
@@ -1,5 +1,4 @@
1
- import { abortIf } from "../utils";
2
- import { rejectUnknownFields } from "../utils";
1
+ import { abortIf, rejectUnknownFields, lastNonEmpty, firstNonEmpty } from "../utils";
3
2
  import { HL_BARE_PREFIX_RE } from "./hash";
4
3
  import { parseHashRef, parseText, type Anchor } from "./parse";
5
4
  import { CONTENT_LINES_NOT_STRING_MSG } from "../constants";
@@ -240,19 +239,6 @@ export function descEdit(edit: RHEdit): string {
240
239
  return `replace ${edit.hash_range_inclusive[0].hash}-${edit.hash_range_inclusive[1].hash}`;
241
240
  }
242
241
 
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
242
 
257
243
  function checkBoundaryDup(
258
244
  adjacentLine: string | undefined,
@@ -19,10 +19,12 @@ export type RMetrics = {
19
19
  };
20
20
 
21
21
  export type RMeta = {
22
- editsAttempted: number;
23
- noopEditsCount: number;
24
- firstChangedLine?: number;
25
- lastChangedLine?: number;
22
+ editsAttempted: number;
23
+ noopEditsCount: number;
24
+ firstChangedLine?: number;
25
+ lastChangedLine?: number;
26
+ addedLines: number;
27
+ removedLines: number;
26
28
  };
27
29
 
28
30
  type NEditEntry = {
@@ -131,8 +133,8 @@ export function buildChanged(input: SuccessInput): TResult {
131
133
 
132
134
  const resultLines = visLines(result);
133
135
  const diffResult = genDiff(originalNormalized, result, 2, resultHashes, originalHashes);
134
- const addedLines = cntDiff(diffResult.diff, "+");
135
- const removedLines = cntDiff(diffResult.diff, "-");
136
+ const addedLines = editMeta.addedLines;
137
+ const removedLines = editMeta.removedLines;
136
138
  const warningsBlock = warnBlock(warnings);
137
139
  const successPrefix = `Successfully replaced in ${path}.`;
138
140
  const lineSummary = addedLines > 0 || removedLines > 0
package/src/replace.ts CHANGED
@@ -111,6 +111,8 @@ interface PipelineResult {
111
111
  lastChangedLine?: number;
112
112
  originalHashes: string[];
113
113
  resultHashes: string[];
114
+ totalAddedLines: number;
115
+ totalRemovedLines: number;
114
116
  }
115
117
 
116
118
  const ROOT_KS = new Set(["path", "changes", "content_lines", "hash_range_inclusive"]);
@@ -202,6 +204,21 @@ export async function execPipeline(
202
204
  }, hashStore);
203
205
 
204
206
  const warnings = [...(anchorResult.warnings ?? [])];
207
+
208
+ let totalAddedLines = 0;
209
+ let totalRemovedLines = 0;
210
+ const noopIndices = new Set(anchorResult.noopEdits?.map((n) => n.editIndex) ?? []);
211
+ for (let i = 0; i < resolved.length; i++) {
212
+ if (noopIndices.has(i)) continue;
213
+ const edit = resolved[i]!;
214
+ const startLine = originalHashes.indexOf(edit.hash_range_inclusive[0].hash);
215
+ const endLine = originalHashes.indexOf(edit.hash_range_inclusive[1].hash);
216
+ if (startLine >= 0 && endLine >= 0) {
217
+ totalRemovedLines += endLine - startLine + 1;
218
+ }
219
+ totalAddedLines += edit.content_lines.length;
220
+ }
221
+
205
222
  return {
206
223
  path,
207
224
  toolEdits,
@@ -216,6 +233,8 @@ export async function execPipeline(
216
233
  lastChangedLine: anchorResult.lastChangedLine,
217
234
  resultHashes,
218
235
  originalHashes,
236
+ totalAddedLines,
237
+ totalRemovedLines,
219
238
  };
220
239
  }
221
240
 
@@ -266,6 +285,32 @@ export function reuseMarkdown(context: any, content: string, theme: any): Markdo
266
285
  m.setText(content);
267
286
  return m;
268
287
  }
288
+ const MODE_CFG = {
289
+ flat: {
290
+ desc: " Only one edit per call. The `hash_range_inclusive` and `content_lines` fields sit at the top level of the request object.",
291
+ examples: [
292
+ "", "Single line:", "{ \"content_lines\": [\"const x = 1;\"], \"hash_range_inclusive\": [\"MQX\", \"MQX\"], \"path\": \"src/main.ts\" }", "", "Range replace:", "{ \"content_lines\": [\"function greet() {\", \" return 1;\", \"}\"], \"hash_range_inclusive\": [\"ZPM\", \"VRW\"], \"path\": \"src/main.ts\" }",
293
+ ].join("\n"),
294
+ rules: "",
295
+ requestStructure: [
296
+ "Flat mode:", "```json", "{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"], \"path\": \"...\" }", "```",
297
+ ].join("\n"),
298
+ prefix: "one edit per call (flat mode)",
299
+ guidePrefix: "- Use `replace` with HASH anchors for all file changes. Only one edit per call.",
300
+ },
301
+ bulk: {
302
+ desc: "\n\nPut all operations on one file in a single `replace` call. Stack every region into the `changes` array, even when they are far apart. Anchors within one call must all come from the same pre-edit read; the runtime applies them atomically against that one snapshot.",
303
+ examples: [
304
+ "", "Single line:", "{ \"changes\": [{ \"content_lines\": [\"const x = 1;\"], \"hash_range_inclusive\": [\"MQX\", \"MQX\"] }], \"path\": \"src/main.ts\" }", "", "Range replace:", "{ \"changes\": [{ \"content_lines\": [\"function greet() {\", \" return 1;\", \"}\"], \"hash_range_inclusive\": [\"ZPM\", \"VRW\"] }], \"path\": \"src/main.ts\" }",
305
+ ].join("\n"),
306
+ rules: "- Multiple edits in one call must not overlap. Overlapping ranges are rejected with [E_EDIT_CONFLICT].",
307
+ requestStructure: [
308
+ "Bulk mode (default):", "```json", "{ \"changes\": [{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"] }], \"path\": \"...\" }", "```",
309
+ ].join("\n"),
310
+ prefix: "batching all changes to a file in one call",
311
+ guidePrefix: "- Use `replace` with HASH anchors for all file changes; batch every change to one file into a single `replace` call.",
312
+ },
313
+ } as const;
269
314
 
270
315
  export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolDef {
271
316
  const autoRead = opts.autoRead ?? false;
@@ -273,50 +318,20 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
273
318
  ? "Anchors are provided automatically after write and replace operations when auto-read is enabled."
274
319
  : "Call `read` to get fresh anchors for follow-up edits.";
275
320
 
276
- const modeDesc = opts.flat
277
- ? " Only one edit per call. The `hash_range_inclusive` and `content_lines` fields sit at the top level of the request object."
278
- : "\n\nPut all operations on one file in a single `replace` call. Stack every region into the `changes` array, even when they are far apart. Anchors within one call must all come from the same pre-edit read; the runtime applies them atomically against that one snapshot.";
279
-
280
- const modeExamples = opts.flat
281
- ? [
282
- "", "Single line:", "{ \"content_lines\": [\"const x = 1;\"], \"hash_range_inclusive\": [\"MQX\", \"MQX\"], \"path\": \"src/main.ts\" }", "", "Range replace:", "{ \"content_lines\": [\"function greet() {\", \" return 1;\", \"}\"], \"hash_range_inclusive\": [\"ZPM\", \"VRW\"], \"path\": \"src/main.ts\" }",
283
- ].join("\n")
284
- : [
285
- "", "Single line:", "{ \"changes\": [{ \"content_lines\": [\"const x = 1;\"], \"hash_range_inclusive\": [\"MQX\", \"MQX\"] }], \"path\": \"src/main.ts\" }", "", "Range replace:", "{ \"changes\": [{ \"content_lines\": [\"function greet() {\", \" return 1;\", \"}\"], \"hash_range_inclusive\": [\"ZPM\", \"VRW\"] }], \"path\": \"src/main.ts\" }",
286
- ].join("\n")
287
-
288
- const modeRules = opts.flat
289
- ? ""
290
- : "- Multiple edits in one call must not overlap. Overlapping ranges are rejected with [E_EDIT_CONFLICT]."
291
-
292
- const modeRequestStructure = opts.flat
293
- ? [
294
- "Flat mode:", "```json", "{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"], \"path\": \"...\" }", "```",
295
- ].join("\n")
296
- : [
297
- "Bulk mode (default):", "```json", "{ \"changes\": [{ \"content_lines\": [...], \"hash_range_inclusive\": [\"aB3\", \"xY7\"] }], \"path\": \"...\" }", "```",
298
- ].join("\n")
299
-
300
- const modePrefix = opts.flat
301
- ? "one edit per call (flat mode)"
302
- : "batching all changes to a file in one call"
303
-
304
- const modeGuidePrefix = opts.flat
305
- ? "- Use `replace` with HASH anchors for all file changes. Only one edit per call."
306
- : "- Use `replace` with HASH anchors for all file changes; batch every change to one file into a single `replace` call."
321
+ const cfg = MODE_CFG[opts.flat ? "flat" : "bulk"];
307
322
 
308
323
  const E_DESC = loadP("../prompts/replace.md", {
309
- MODE_DESCRIPTION: modeDesc,
310
- MODE_EXAMPLES: modeExamples,
311
- MODE_RULES: modeRules,
312
- MODE_REQUEST_STRUCTURE: modeRequestStructure,
324
+ MODE_DESCRIPTION: cfg.desc,
325
+ MODE_EXAMPLES: cfg.examples,
326
+ MODE_RULES: cfg.rules,
327
+ MODE_REQUEST_STRUCTURE: cfg.requestStructure,
313
328
  AUTO_READ_GUIDANCE: readGuidance,
314
329
  });
315
330
  const E_SNIPPET = loadP("../prompts/replace-snippet.md", {
316
- MODE_PREFIX: modePrefix,
331
+ MODE_PREFIX: cfg.prefix,
317
332
  });
318
333
  const E_GUIDE = loadGuide("../prompts/replace-guidelines.md", {
319
- MODE_PREFIX: modeGuidePrefix,
334
+ MODE_PREFIX: cfg.guidePrefix,
320
335
  AUTO_READ_GUIDANCE: readGuidance,
321
336
  });
322
337
 
@@ -449,6 +464,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
449
464
  firstChangedLine,
450
465
  lastChangedLine,
451
466
  resultHashes,
467
+ totalAddedLines,
468
+ totalRemovedLines,
452
469
  } = await execPipeline(
453
470
  normalizedParams,
454
471
  ctx.cwd,
@@ -471,6 +488,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
471
488
  editMeta: {
472
489
  editsAttempted,
473
490
  noopEditsCount: noopEdits?.length ?? 0,
491
+ addedLines: 0,
492
+ removedLines: 0,
474
493
  },
475
494
  warnings,
476
495
  });
@@ -501,6 +520,8 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
501
520
  noopEditsCount: noopEdits?.length ?? 0,
502
521
  firstChangedLine,
503
522
  lastChangedLine,
523
+ addedLines: totalAddedLines,
524
+ removedLines: totalRemovedLines,
504
525
  };
505
526
 
506
527
  const successInput = {
package/src/utils.ts CHANGED
@@ -52,3 +52,27 @@ export function errCode(error: unknown): string | undefined {
52
52
  }
53
53
  return undefined;
54
54
  }
55
+
56
+ export function lastNonEmptyIndex(lines: string[]): number {
57
+ for (let i = lines.length - 1; i >= 0; i--) {
58
+ if (lines[i]!.length > 0) return i;
59
+ }
60
+ return -1;
61
+ }
62
+
63
+ export function firstNonEmptyIndex(lines: string[]): number {
64
+ for (let i = 0; i < lines.length; i++) {
65
+ if (lines[i]!.length > 0) return i;
66
+ }
67
+ return -1;
68
+ }
69
+
70
+ export function lastNonEmpty(lines: string[]): string | undefined {
71
+ const idx = lastNonEmptyIndex(lines);
72
+ return idx >= 0 ? lines[idx] : undefined;
73
+ }
74
+
75
+ export function firstNonEmpty(lines: string[]): string | undefined {
76
+ const idx = firstNonEmptyIndex(lines);
77
+ return idx >= 0 ? lines[idx] : undefined;
78
+ }