pi-hashline-edit-pro 0.16.4 → 0.16.5

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.4",
3
+ "version": "0.16.5",
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": {
@@ -3,4 +3,5 @@
3
3
  - On `[E_STALE_ANCHOR]`, call `read` to get fresh anchors, copy the 3-character HASH of the start and end of the range you are replacing into `hash_range_inclusive`, and retry.
4
4
  - `hash_range_inclusive` replaces the ENTIRE range inclusively. Every line from the first anchor through the second anchor is deleted. Only put the replacement lines in `content_lines` — do not include lines that already exist outside the range.
5
5
  - **Preserve leading whitespace (indentation) exactly.** The content after `│` in read output includes all leading spaces and tabs — copy them into `content_lines` unchanged.
6
+ - **`content_lines` must be a native JSON array of strings**, not a JSON string. Do NOT serialize it: `"content_lines": "[\"line1\", \"line2\"]"` is WRONG. Use `"content_lines": ["line1", "line2"]` instead.
6
7
  - If the response includes a "Boundary duplication" warning, the edge line of your `content_lines` matches a line outside the replaced range. Remove that line from `content_lines` — the original is still in the file outside the range.
@@ -80,6 +80,18 @@ Right:
80
80
  ```json
81
81
  { "content_lines": [...], "hash_range_inclusive": ["F4T", "F4T"] }
82
82
 
83
+ ⚠️ Common mistake: do not serialize `content_lines` as a JSON string.
84
+
85
+ Wrong:
86
+ ```json
87
+ { "changes": [{ "content_lines": "[\"line1\", \"line2\"]", "hash_range_inclusive": ["F4T", "F4T"] }], "path": "src/main.ts" }
88
+
89
+ Right:
90
+ ```json
91
+ { "changes": [{ "content_lines": ["line1", "line2"], "hash_range_inclusive": ["F4T", "F4T"] }], "path": "src/main.ts" }
92
+
93
+ `content_lines` must be a native JSON array of strings, not a string that looks like an array. Pass it as a proper JSON array value.
94
+
83
95
  Rules:
84
96
  - `hash_range_inclusive` is a pair `[start, end]`. A single-line replace is `hash_range_inclusive: ["X", "X"]`.
85
97
  - To delete a range, use `content_lines: []`.
@@ -3,4 +3,5 @@
3
3
  - On `[E_STALE_ANCHOR]`, call `read` to get fresh anchors, copy the 3-character HASH of the start and end of the range you are replacing into `hash_range_inclusive`, and retry.
4
4
  - `hash_range_inclusive` replaces the ENTIRE range inclusively. Every line from the first anchor through the second anchor is deleted. Only put the replacement lines in `content_lines` — do not include lines that already exist outside the range.
5
5
  - **Preserve leading whitespace (indentation) exactly.** The content after `│` in read output includes all leading spaces and tabs — copy them into `content_lines` unchanged.
6
+ - **`content_lines` must be a native JSON array of strings**, not a JSON string. Do NOT serialize it: `"content_lines": "[\"line1\", \"line2\"]"` is WRONG. Use `"content_lines": ["line1", "line2"]` instead.
6
7
  - If the response includes a "Boundary duplication" warning, the edge line of your `content_lines` matches a line outside the replaced range. Remove that line from `content_lines` — the original is still in the file outside the range.
@@ -65,6 +65,18 @@ Right:
65
65
  ```json
66
66
  { "content_lines": [...], "hash_range_inclusive": ["F4T", "F4T"] }
67
67
 
68
+ ⚠️ Common mistake: do not serialize `content_lines` as a JSON string.
69
+
70
+ Wrong:
71
+ ```json
72
+ { "content_lines": "[\"line1\", \"line2\"]", "hash_range_inclusive": ["F4T", "F4T"], "path": "src/main.ts" }
73
+
74
+ Right:
75
+ ```json
76
+ { "content_lines": ["line1", "line2"], "hash_range_inclusive": ["F4T", "F4T"], "path": "src/main.ts" }
77
+
78
+ `content_lines` must be a native JSON array of strings, not a string that looks like an array. Pass it as a proper JSON array value.
79
+
68
80
  Rules:
69
81
  - `hash_range_inclusive` is a pair `[start, end]`. A single-line replace is `hash_range_inclusive: ["X", "X"]`.
70
82
  - To delete a range, use `content_lines: []`.
@@ -3,5 +3,6 @@
3
3
  - On `[E_STALE_ANCHOR]`, call `read` to get fresh anchors, copy the 3-character HASH of the start and end of the range you are replacing into `hash_range_inclusive`, and retry.
4
4
  - `hash_range_inclusive` replaces the ENTIRE range inclusively. Every line from the first anchor through the second anchor is deleted. Only put the replacement lines in `content_lines` — do not include lines that already exist outside the range.
5
5
  - **Preserve leading whitespace (indentation) exactly.** The content after `│` in read output includes all leading spaces and tabs — copy them into `content_lines` unchanged.
6
+ - **`content_lines` must be a native JSON array of strings**, not a JSON string. Do NOT serialize it: `"content_lines": "[\"line1\", \"line2\"]"` is WRONG. Use `"content_lines": ["line1", "line2"]` instead.
6
7
  - Two modes are available: bulk (default, uses `changes` array) and flat (top-level `hash_range_inclusive`/`content_lines`). Toggle with `/toggle-replace-mode`.
7
8
  - If the response includes a "Boundary duplication" warning, the edge line of your `content_lines` matches a line outside the replaced range. Remove that line from `content_lines` — the original is still in the file outside the range.
@@ -127,6 +127,18 @@ Right:
127
127
  ```json
128
128
  { "content_lines": [...], "hash_range_inclusive": ["F4T", "F4T"] }
129
129
 
130
+ ⚠️ Common mistake: do not serialize `content_lines` as a JSON string.
131
+
132
+ Wrong:
133
+ ```json
134
+ { "content_lines": "[\"line1\", \"line2\"]", "hash_range_inclusive": ["F4T", "F4T"] }
135
+
136
+ Right:
137
+ ```json
138
+ { "content_lines": ["line1", "line2"], "hash_range_inclusive": ["F4T", "F4T"] }
139
+
140
+ `content_lines` must be a native JSON array of strings, not a string that looks like an array. Pass it as a proper JSON array value.
141
+
130
142
  Rules:
131
143
  - `hash_range_inclusive` is a pair `[start, end]`. A single-line replace is `hash_range_inclusive: ["X", "X"]`.
132
144
  - To delete a range, use `content_lines: []`.
@@ -56,13 +56,13 @@ function assertNoPrefixes(lines: string[]): void {
56
56
  }
57
57
 
58
58
  export function parseText(edit: string[] | string | null): string[] {
59
- if (edit === null) return [];
60
- const lines =
61
- typeof edit === "string"
62
- ? (edit.endsWith("\n") ? edit.slice(0, -1) : edit)
63
- .replaceAll("\r", "")
64
- .split("\n")
65
- : edit;
66
- assertNoPrefixes(lines);
67
- return lines;
59
+ if (edit === null) return [];
60
+ if (typeof edit === "string") {
61
+ throw new Error(
62
+ `[E_BAD_SHAPE] "content_lines" must be a native JSON array of strings, not a JSON string.`
63
+ + ` Do not serialize the array (e.g. '["line1", "line2"]') — pass it as a proper JSON array: ["line1", "line2"].`
64
+ );
65
+ }
66
+ assertNoPrefixes(edit);
67
+ return edit;
68
68
  }
@@ -148,6 +148,13 @@ function assertItem(edit: Record<string, unknown>, index: number): void {
148
148
  throw new Error(`[E_BAD_SHAPE] Edit ${index} requires a "content_lines" field. Provide the replacement lines (use [] to delete).`);
149
149
  }
150
150
  if ("content_lines" in edit && !isStrArr(edit.content_lines)) {
151
+ const val = edit.content_lines;
152
+ if (typeof val === "string") {
153
+ throw new Error(
154
+ `[E_BAD_SHAPE] Edit ${index} field "content_lines" must be a native JSON array of strings, not a JSON string.`
155
+ + ` Do not serialize the array (e.g. '["line1", "line2"]') — pass it as a proper JSON array: ["line1", "line2"].`
156
+ );
157
+ }
151
158
  throw new Error(`[E_BAD_SHAPE] Edit ${index} field "content_lines" must be a string array.`);
152
159
  }
153
160
  if (!isStrPair(edit.hash_range_inclusive)) {
@@ -1,5 +1,17 @@
1
1
  import { isRec, has } from "./utils";
2
2
 
3
+ function assertContentLinesNotString(
4
+ value: unknown,
5
+ label: string,
6
+ ): void {
7
+ if (typeof value === "string") {
8
+ throw new Error(
9
+ `[E_BAD_SHAPE] ${label}: "content_lines" must be a native JSON array of strings, not a JSON string.`
10
+ + ` Do not serialize the array (e.g. '["line1", "line2"]') — pass it as a proper JSON array: ["line1", "line2"].`
11
+ );
12
+ }
13
+ }
14
+
3
15
  export function normalizeFilePath(record: Record<string, unknown>): void {
4
16
  if (typeof record.path !== "string" && typeof record.file_path === "string") {
5
17
  record.path = record.file_path;
@@ -31,9 +43,24 @@ export function normReq(input: unknown): unknown {
31
43
 
32
44
  normalizeFilePath(record);
33
45
 
46
+ // Early validation: reject string-typed content_lines at the top level
47
+ if (has(record, "content_lines") && typeof record.content_lines === "string") {
48
+ assertContentLinesNotString(record.content_lines, "Top-level");
49
+ }
50
+
34
51
  normalizeField(record, "changes", "changes");
35
52
  normalizeField(record, "edits", "changes");
36
53
 
54
+ // Validate items in the changes array before wrapping flat format
55
+ if (Array.isArray(record.changes)) {
56
+ for (let i = 0; i < record.changes.length; i++) {
57
+ const item = record.changes[i];
58
+ if (isRec(item) && has(item, "content_lines") && typeof item.content_lines === "string") {
59
+ assertContentLinesNotString(item.content_lines, `changes[${i}]`);
60
+ }
61
+ }
62
+ }
63
+
37
64
  if (!Array.isArray(record.changes) && has(record, "hash_range_inclusive") && has(record, "content_lines")) {
38
65
  const hri = record.hash_range_inclusive;
39
66
  const cl = record.content_lines;