pi-hashline-edit-pro 6.3.0 → 6.3.1

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 CHANGED
@@ -188,7 +188,7 @@ Edge cases:
188
188
  | --- | --- |
189
189
  | `remove_from` | 4-char anchor marking the FIRST line to remove (inclusive). |
190
190
  | `remove_to` | 4-char anchor marking the LAST line to remove (inclusive). |
191
- | `text` | The exact text to write in place of the removed range, as one string: `""` deletes the range, `"\n"` is one blank line, and a trailing line break sets the last line's ending instead of adding a blank line. Embedded `\r\n`/`\r`/`\n` are preserved; JSON decoding happens once, before the tool; the tool writes the string it receives and never decodes — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
191
+ | `text` | The exact text to write in place of the removed range, as one string: `""` deletes the range. A trailing line break sets the newline of the last line written instead of adding a blank line; a lone newline writes one blank line. Embedded `\r\n`/`\r`/`\n` are preserved; JSON decoding happens once, before the tool; the tool writes the string it receives and never decodes — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
192
192
 
193
193
  `replace_from` and `replace_to` are accepted as aliases for `remove_from` and `remove_to`.
194
194
 
@@ -235,7 +235,7 @@ In a same-message batch it joins the other calls on its file: the batch validate
235
235
  | --- | --- |
236
236
  | `anchor` | 4-char anchor marking the line next to which the lines go. The anchor line is preserved. A pasted `+Hasu│x` diff row or `anchor│` prefix is stripped automatically with a warning. |
237
237
  | `direction` | `"after"` inserts below the anchor line, `"before"` above it. |
238
- | `text` | The exact text to insert, as one string: `""` inserts one blank line (the same as `"\n"`), and a trailing line break sets the last line's ending instead of adding a blank line. Never include the anchor line. Embedded `\r\n`/`\r`/`\n` are preserved; JSON decoding happens once, before the tool; the tool writes the string it receives and never decodes — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
238
+ | `text` | The exact text to insert, as one string: `""` inserts one blank line (the same as a lone newline). A trailing line break sets the newline of the last line written instead of adding a blank line. Never include the anchor line. Embedded `\r\n`/`\r`/`\n` are preserved; JSON decoding happens once, before the tool; the tool writes the string it receives and never decodes — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
239
239
 
240
240
  Nothing is removed and the inserted lines are written exactly as given; the anchor line and every other line stay in place. An empty `text` payload inserts one blank line. To seed an empty file, read it and insert after the `anchor│` empty-line row.
241
241
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "6.3.0",
3
+ "version": "6.3.1",
4
4
  "type": "module",
5
5
  "description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
6
6
  "main": "index.ts",
@@ -30,7 +30,7 @@ const HEADER =
30
30
  "[hashline auto-read-all] Every non-ignored project file is attached below as `=== path ===` then `anchor│content` rows with live anchors.\nEdit directly from the attachment with replace and insert; do not call read for attached files.\nFiles listed as not attached can be read normally.\nAnchors are case-sensitive and stay valid until their line is edited.";
31
31
 
32
32
  const OUTLINE_HEADER =
33
- "[hashline auto-read-all] Every non-ignored project file is outlined below as `=== path (language) — lines ===` then `anchor│...` rows with live anchors: multi-line symbols show a label with `[limit N]`, the span to request when reading from the anchor, and single-line symbols show the line content.\nEdit a row's anchor with replace or insert, or read from it with `offset` set to that anchor and the row's `limit`; do not call read for attached files.\nFiles listed as not attached can be read normally.\nAnchors are case-sensitive and stay valid until their line is edited.";
33
+ "[hashline auto-read-all] Every non-ignored project file is outlined below as `=== path (language) — lines ===` then `anchor│...` rows with live anchors: multi-line symbols show a label with `[limit N]`, the span to request when reading from the anchor, and single-line symbols show the line content.\nEdit a row's anchor with replace or insert, or read from it with `offset` set to that anchor (and the row's `limit` when it shows one); do not call read for attached files.\nFiles listed as not attached can be read normally.\nAnchors are case-sensitive and stay valid until their line is edited.";
34
34
 
35
35
  const IMAGE_EXTENSIONS = new Set([
36
36
  ".avif",
package/src/constants.ts CHANGED
@@ -14,10 +14,10 @@ export const NEW_CONTENT_NOT_ARRAY_MSG =
14
14
  `[E_BAD_SHAPE] "text" must be an array of strings, one per line (use [] to delete).`;
15
15
 
16
16
  export const NEW_CONTENT_NOT_STRING_MSG =
17
- `[E_BAD_SHAPE] "text" must be a string holding the exact text to write. Use "" to delete the range and "\\n" for one blank line; line breaks inside the string separate lines.`;
17
+ `[E_BAD_SHAPE] "text" must be a string holding the exact text to write. Use "" to delete the range and a lone newline for one blank line; line breaks inside the string separate lines.`;
18
18
 
19
19
  export const TEXT_NOT_STRING_MSG =
20
- `[E_BAD_SHAPE] "text" must be a string holding the exact text to insert. Use "\\n" for one blank line; line breaks inside the string separate lines.`;
20
+ `[E_BAD_SHAPE] "text" must be a string holding the exact text to insert. Use a lone newline for one blank line; line breaks inside the string separate lines.`;
21
21
 
22
22
  export const NUL_CONTENT_MSG =
23
23
  `[E_BAD_SHAPE] Content contains a NUL byte (U+0000); a text file cannot contain NUL, and writing it would break further reads and edits. Remove the NUL byte and retry.`;
@@ -13,6 +13,7 @@ export interface EditToolFlags {
13
13
  strictInput: boolean;
14
14
  autoRead: boolean;
15
15
  autoReadAllActive: boolean;
16
+ autoReadAllOutline: boolean;
16
17
  replaceMatchEnabled: boolean;
17
18
  copyMoveEnabled: boolean;
18
19
  codemode: boolean;
@@ -23,6 +24,7 @@ export const DEFAULT_EDIT_FLAGS: EditToolFlags = {
23
24
  strictInput: false,
24
25
  autoRead: true,
25
26
  autoReadAllActive: false,
27
+ autoReadAllOutline: false,
26
28
  replaceMatchEnabled: true,
27
29
  copyMoveEnabled: true,
28
30
  codemode: false,
@@ -35,6 +37,7 @@ export async function currentEditFlags(codemode = false): Promise<EditToolFlags>
35
37
  strictInput: config.strictInput === true,
36
38
  autoRead: config.autoRead !== false,
37
39
  autoReadAllActive: (config.autoReadAll ?? "off") !== "off",
40
+ autoReadAllOutline: (config.autoReadAll ?? "off") === "outline",
38
41
  replaceMatchEnabled: config.replaceMatchEnabled !== false,
39
42
  copyMoveEnabled: config.copyMoveEnabled !== false,
40
43
  codemode
@@ -66,7 +69,7 @@ function batchGuideline(flags: EditToolFlags): string {
66
69
  const tools = operationNames(SHARED_EDIT_OPS, flags);
67
70
  const outcome = flags.autoRead ? "diff" : "result";
68
71
  const script = flags.codemode ? ` ${SCRIPT_BATCH_GUIDELINE}` : "";
69
- return `${tools}: same-file calls in one message are grouped into one batch; earlier calls reply \`In batch N (queued)\` and the last applied call shows the combined ${outcome}, with one undo for the whole batch. A call whose own request is invalid (anchors, shape, or \`old_string\`) fails on its own and is left out of the batch; the rest still commits.${script}`;
72
+ return `${tools}: same-file calls in one message are grouped into one batch; earlier calls reply \`In batch N (queued)\` and the last applied call shows the combined ${outcome}, with one undo for the whole batch. A call whose own request is invalid (anchors, shape, or \`old_string\`) fails on its own; any failure once the batch runs aborts the whole batch.${script}`;
70
73
  }
71
74
 
72
75
  function diffGuideline(flags: EditToolFlags): string {
@@ -83,7 +86,7 @@ function pathGuideline(flags: EditToolFlags): string {
83
86
 
84
87
  function payloadGuideline(flags: EditToolFlags): string {
85
88
  const tools = operationNames(SHARED_PAYLOAD_OPS, flags);
86
- return `${tools}: JSON decoding happens once, before the tool; the tool writes the string it receives and never decodes — \`\\uXXXX\` is the character, \`\\\\uXXXX\` the literal text.`;
89
+ return `${tools}: write the character itself, not an escape; the tool writes \`text\` verbatim and never decodes escapes — \`\\uXXXX\` is the character, \`\\\\uXXXX\` the literal text.`;
87
90
  }
88
91
 
89
92
  function strictInputGuideline(flags: EditToolFlags): string {
@@ -115,27 +118,34 @@ export function withReplacePrompts(base: { description: string; snippet: string;
115
118
  return finalizePrompts(description, base.snippet, guidelines, flags);
116
119
  }
117
120
 
121
+ function readGuidelineApplies(guideline: string, flags: EditToolFlags): boolean {
122
+ if (!flags.autoReadAllOutline && guideline.includes("[limit N]")) return false;
123
+ if (!flags.replaceMatchEnabled && guideline.includes("`replace_match`")) return false;
124
+ return true;
125
+ }
126
+
118
127
  export function withReadPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
119
128
  const preference = preferenceGuideline(flags);
120
129
  const script = flags.codemode ? [RESULT_CONTRACT_GUIDELINE] : [];
130
+ const baseGuidelines = base.guidelines.filter((guideline) => readGuidelineApplies(guideline, flags));
121
131
  if (flags.autoReadAllActive) {
122
- const rewritten = base.guidelines
132
+ const rewritten = baseGuidelines
123
133
  .filter((guideline) => !guideline.includes("call again after an edit"))
124
134
  return { description: base.description, snippet: base.snippet, guidelines: [preference, ...rewritten, ...script] };
125
135
  }
126
- if (flags.autoRead) return { description: base.description, snippet: base.snippet, guidelines: [preference, ...base.guidelines, ...script] };
127
- const guidelines = [preference, ...base.guidelines, ...script];
136
+ if (flags.autoRead) return { description: base.description, snippet: base.snippet, guidelines: [preference, ...baseGuidelines, ...script] };
137
+ const guidelines = [preference, ...baseGuidelines, ...script];
128
138
  const mapped = guidelines.map((guideline) => guideline.startsWith("`read`: call again after an edit") ? "`read`: call again after an edit when you need anchors you lack." : guideline);
129
139
  return { description: base.description, snippet: base.snippet, guidelines: mapped };
130
140
  }
131
141
 
132
142
  export function withInsertPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
133
- const guidelines = [...base.guidelines];
143
+ const guidelines = [preferenceGuideline(flags), ...base.guidelines];
134
144
  return finalizePrompts(base.description, base.snippet, guidelines, flags);
135
145
  }
136
146
 
137
147
  export function withReplaceMatchPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
138
- const guidelines = [...base.guidelines];
148
+ const guidelines = [preferenceGuideline(flags), ...base.guidelines];
139
149
  return finalizePrompts(base.description, base.snippet, guidelines, flags);
140
150
  }
141
151
 
@@ -157,8 +167,8 @@ function joinOps(ops: string[], options?: { backtick?: boolean; separator?: "/"
157
167
  }
158
168
 
159
169
  export function withGrepPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
160
- if (!flags.codemode && flags.copyMoveEnabled) return base;
161
- const guidelines = flags.codemode ? [...base.guidelines, RESULT_CONTRACT_GUIDELINE] : base.guidelines;
170
+ const guidelines = [preferenceGuideline(flags), ...base.guidelines];
171
+ if (flags.codemode) guidelines.push(RESULT_CONTRACT_GUIDELINE);
162
172
  if (flags.copyMoveEnabled) return { ...base, guidelines };
163
173
  return { ...base, guidelines, description: base.description.replaceAll("replace, insert, copy, or move", joinOps(gatedEditOps(["replace", "insert", "copy", "move"], flags))) };
164
174
  }
@@ -168,9 +178,9 @@ export function withUndoPrompts(base: { description: string; snippet: string; gu
168
178
  let description = base.description;
169
179
  let snippet = base.snippet;
170
180
  const script = flags.codemode ? [RESULT_CONTRACT_GUIDELINE, SCRIPT_UNDO_GUIDELINE] : [];
171
- let guidelines = [...base.guidelines, ...script];
181
+ let guidelines = [preferenceGuideline(flags), ...base.guidelines, ...script];
172
182
  if (!flags.autoRead) {
173
- guidelines = guidelines.map((guideline) => guideline.includes("bad diff") ? "`undo_last_change`: only the last `replace`/`replace_match`/`insert`/`copy`/`move` per file is undoable; a `write` clears it, so undo right after a bad edit — review what you're restoring." : guideline);
183
+ guidelines = guidelines.map((guideline) => guideline.includes("a `write` clears the history") ? "`undo_last_change`: only the last `replace`/`replace_match`/`insert`/`copy`/`move` per file is undoable; a `write` clears it, so undo before any other edit or write." : guideline);
174
184
  }
175
185
  if (ops.length !== 5) {
176
186
  description = description.replaceAll("replace, replace_match, insert, copy, or move", joinOps(ops));
@@ -184,7 +194,7 @@ export function withUndoPrompts(base: { description: string; snippet: string; gu
184
194
  }
185
195
 
186
196
  export function withTransferPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
187
- const guidelines = flags.codemode ? [...base.guidelines, SCRIPT_TRANSFER_GUIDELINE] : [...base.guidelines];
197
+ const guidelines = [preferenceGuideline(flags), ...base.guidelines, ...(flags.codemode ? [SCRIPT_TRANSFER_GUIDELINE] : [])];
188
198
  return finalizePrompts(base.description, base.snippet, guidelines, flags, { stringPayload: false });
189
199
  }
190
200
 
package/src/insert.ts CHANGED
@@ -28,7 +28,7 @@ const insertDirectionSchema = Type.Union(
28
28
  );
29
29
  const insertTextSchema = Type.String({
30
30
  description:
31
- 'The exact text to insert; an empty string inserts one blank line. "\\n" is one blank line, and a trailing line break sets the last line\'s ending instead of adding a blank line. Never include the anchor line.',
31
+ 'The exact text to insert; an empty string inserts one blank line. A trailing line break sets the newline of the last line written instead of adding a blank line; a lone newline writes one blank line. Never include the anchor line.',
32
32
  });
33
33
 
34
34
  const insertPathRequiredSchema = Type.String({
@@ -4,7 +4,7 @@ import { TEXT_NOT_STRING_MSG, NEW_CONTENT_NOT_STRING_MSG } from "./constants";
4
4
 
5
5
  const textSchema = Type.String({
6
6
  description:
7
- 'The exact text to write in place of the removed range. "" deletes the range, "\\n" is one blank line, and a trailing line break sets the last line\'s ending instead of adding a blank line.',
7
+ 'The exact text to write in place of the removed range. "" deletes the range. A trailing line break sets the newline of the last line written instead of adding a blank line; a lone newline writes one blank line.',
8
8
  });
9
9
 
10
10
  const removeFromSchema = Type.String({
@@ -234,7 +234,7 @@ const replaceMatchOldStringSchema = Type.String({
234
234
  });
235
235
  const replaceMatchNewStringSchema = Type.String({
236
236
  description:
237
- 'The exact replacement for every matched occurrence. "" deletes the matches, "\\n" inserts one blank line, and a trailing line break sets the last line\'s ending instead of adding a blank line; the rest of the range is kept byte-for-byte.',
237
+ 'The exact replacement for every matched occurrence. "" deletes the matches. A trailing line break sets the newline of the last line written instead of adding a blank line; a lone newline writes one blank line; the rest of the range is kept byte-for-byte.',
238
238
  });
239
239
  const replaceMatchPathRequiredSchema = Type.String({
240
240
  description:
@@ -1,3 +1,3 @@
1
1
  Copy a range of lines to another position, targeted by 4-character anchors from any served anchor│content row. Give `source_from` and `source_to` as bare anchors marking the first and last line to copy in the source file, and `insert_after` as the bare anchor of the destination line after which the copy goes. The source lines stay in place, and the copied lines are written exactly as the source file holds them.
2
2
 
3
- A cross-file copy joins the destination file's batch. A copy always duplicates the content its source anchors were served from, so a same-message edit to the source file does not change what is copied.
3
+ A copy always duplicates the content its source anchors were served from, so a same-message edit to the source file does not change what is copied.
@@ -1 +1 @@
1
- Search text files with ripgrep. Hits and context lines come back as `lineNumber │ anchor│content` rows, editable with replace, insert, copy, or move without a new read; the `=== path ===` header and line numbers locate the match. Patterns are regex by default; set `literal: true` to search exact text, `ignoreCase` for case-insensitive search, and `context` for surrounding rows. Avoid backreferences, nested quantifiers, and quantified alternation. Searches respect `.gitignore`, always skip `.git`, and skip binary and image files silently. A match over 500 bytes is shown as a `...` fragment around the hit, but its anchor still covers the whole line. When cut by match limit, raise `limit` (default 100); when cut by rows/bytes, refine `pattern` / narrow path.
1
+ Search text files with ripgrep. Hits and context lines come back as `lineNumber │ anchor│content` rows, editable with replace, insert, copy, or move without a new read; the `=== path ===` header and line numbers locate the match. Patterns are regex by default; set `literal: true` to search exact text, `ignoreCase` for case-insensitive search, and `context` for surrounding rows. Backreferences and look-around are rejected by ripgrep's engine; nested quantifiers and alternation run in linear time. Searches respect `.gitignore`, always skip `.git`, and skip binary and image files silently. A match over 500 bytes is shown as a `...` fragment around the hit, but its anchor still covers the whole line. When cut by match limit, raise `limit` (default 100); when cut by rows/bytes, refine `pattern` / narrow path.
@@ -1 +1 @@
1
- - `insert`: compare both edges of the inserted block in the post-edit diff against the request; a missing or extra `+anchor│` blank row at either boundary is the classic insert slip.
1
+ - `insert`: a missing or extra `+anchor│` blank row at either block edge in the diff is the classic insert slip.
@@ -1 +1 @@
1
- Insert text after or before one existing line in a text file, addressed by the `anchor` field (a bare anchor from any served anchor│content row). `text` goes after the anchor line with `direction: "after"` or before it with `direction: "before"`. `text` is one string holding the exact text to insert. A trailing line break sets the last line's ending instead of adding a blank line, so a blank line next to the anchor line must be an extra break in `text`: `"x\n"` leaves the anchor line directly after `x`, and `"x\n\n"` leaves one blank line between them (`direction: "after"` mirrors this at the start of `text`).
1
+ Insert text after or before one existing line in a text file, addressed by the `anchor` field (a bare anchor from any served anchor│content row). `text` goes after the anchor line with `direction: "after"` or before it with `direction: "before"`. `text` is one string holding the exact text to insert; a trailing line break sets the newline of the last line written instead of adding a blank line, and a leading break is an ordinary blank line.
@@ -1,3 +1,3 @@
1
- Move a range of lines to another position, targeted by 4-character anchors from any served anchor│content row. Give `source_from` and `source_to` as bare anchors marking the first and last line to move in the source file, and `insert_after` as the bare anchor of the destination line after which the range goes. `insert_after` must sit outside the source range when both anchors share a file; moving a range in one file to where it already sits reports no change.
1
+ Move a range of lines to another position, targeted by 4-character anchors from any served anchor│content row. Give `source_from` and `source_to` as bare anchors marking the first and last line to move in the source file, and `insert_after` as the bare anchor of the destination line after which the range goes. `insert_after` must sit outside the source range when both anchors share a file.
2
2
 
3
- A cross-file `move` whose source file also has batched edits in the message commits on its own. A batched cross-file `move` commits its source removal with the batch but shows only the destination diff; read the source file for fresh anchors.
3
+ A cross-file `move` commits on its own when its source file also has batched edits in the message.
@@ -1,3 +1,3 @@
1
1
  - `read`: call again after an edit when you need anchors you lack — post-edit diff `+anchor│`/` anchor│` rows and any served `anchor│content` rows already carry fresh anchors for the changed range.
2
- - `read`: pass a served anchor as `offset` with `limit` to read from it without line-number math; an outline row's `[limit N]` is the span to request, and a row without one is a single line shown as its content.
3
- - `read`: an outline row shows a symbol label, or the line content for a single-line symbol; edit by anchor, and read the anchor (or copy `old_string` from a served row) before `replace_match`.
2
+ - `read`: an auto-read-all outline row shows a symbol label, or the line content for a single-line symbol; its `[limit N]` is the span to request.
3
+ - `read`: `replace_match` needs both boundary lines served; copy `old_string` from a served row or read the lines first.
@@ -1,3 +1,3 @@
1
- Read a text file and return it as `anchor│content` rows, one per line: a 4-character anchor (letters only, case-sensitive), the `│` separator, then the line content. Target lines in edits by anchor, never by content or line number. Page long files with `offset` and `limit`; when the output says truncated, continue with the hinted `offset/limit`. Images attach visually; binary, directory, and UTF-16/UTF-32 text are rejected; an empty file returns one empty row you can replace to seed content.
1
+ Read a text file and return it as `anchor│content` rows, one per line: a 4-character anchor (letters only, case-sensitive), the `│` separator, then the line content. Target edits by anchor, never by content or line number. Page long files with `offset` and `limit`; when the output says truncated, continue from the hinted `offset`. Images attach visually; binary, directory, and UTF-16/UTF-32 text are rejected; an empty file returns one empty row you can replace to seed content.
2
2
 
3
- `offset` takes a 1-indexed line number or a served anchor; with an anchor, `limit` counts the lines to return after it. A stale anchor is refused with the current context rows and fresh anchors.
3
+ `offset` takes a 1-indexed line number or a served anchor; with an anchor, `limit` counts the lines to return after it, so no line-number math is needed. A stale anchor is refused with the current context rows and fresh anchors.
@@ -1,4 +1,4 @@
1
- - `copy`/`move`: for a block transfer, only the source's two boundary rows and the destination line need serving; the interior transfers verbatim and the block lands in one commit. To append at the end, use the destination's last served line as `insert_after`.
1
+ - `copy`/`move`: for a block transfer, only the source's two boundary rows and the destination line need serving; to append at the end, use the destination's last served line as `insert_after`.
2
2
  - `copy`/`move`: `insert_after` must sit outside the source range; `source_to` places the block right after itself.
3
3
  - `copy`/`move`: `source_from` and `source_to` may live in a different file than `insert_after`; an empty destination file is seeded with the copied or moved lines.
4
4
  - `move`: a batched cross-file move does not display the source file's diff; its removal commits with the batch, and the source file needs a read for fresh anchors.
@@ -1,3 +1,3 @@
1
1
  - `undo_last_change`: history is one slot per file, not a stack; a second undo returns `E_UNDO_NONE`.
2
- - `undo_last_change`: a `write` clears the history, so undo right after a bad diff — review the diff's `-anchor│` rows first to confirm what you're restoring.
2
+ - `undo_last_change`: a `write` clears the history, so undo before any other edit or write; the undo diff's `+anchor│` rows are the restored lines.
3
3
  - `undo_last_change`: a cross-file `move` records one undo entry per file; undo both the source and the destination to revert the whole move, because undoing one side alone leaves the moved lines duplicated or missing.