@oh-my-pi/hashline 17.2.15 → 17.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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.3.0] - 2026-08-13
6
+
7
+ ### Fixed
8
+
9
+ - Repaired mis-set replacement ranges using exact outside-row matches, indentation, tree-sitter structure, and a narrow pure-closer shape: opening comment fences and other syntax-essential edges are retained only when a parse-valid candidate satisfies those constraints; ambiguous placements are rejected.
10
+
5
11
  ## [17.2.15] - 2026-08-12
6
12
 
7
13
  ### Added
@@ -3,9 +3,10 @@
3
3
  * post-edit lines plus any diagnostic warnings. Pure function: no FS, no
4
4
  * mutation of the input.
5
5
  *
6
- * Replacement groups are first normalized by {@link repairReplacementBoundaries},
7
- * which absorbs common model mistakes where a payload restates unchanged range
8
- * boundaries or duplicates/drops structural closers.
6
+ * Mis-set replacement range boundaries are repaired by bounded candidate
7
+ * search. Exact line equality, indentation, tree-sitter structure, and a
8
+ * narrow pure-closer shape gate constrain candidates; tree-sitter validates
9
+ * the selected result.
9
10
  */
10
11
  import type { ApplyResult, Clipboard, Edit } from "./types.js";
11
12
  /** A line that is nothing but closing delimiters: `}`, `)`, `];`, `})`, `},`. */
@@ -21,11 +22,10 @@ export interface ApplyEditsOptions {
21
22
  /** Anonymous `PASTE` with an empty register: `throw` (default) or `drop` (streaming previews). An empty named-register paste never throws — it warns and pastes nothing. */
22
23
  onEmptyPaste?: "throw" | "drop";
23
24
  /**
24
- * Target file path, used only to infer a language for the tree-sitter
25
- * syntax probe (see {@link parsesCleanly}). Supplying it lets the applier
26
- * confirm that the edit as authored still parses, in which case no
27
- * delimiter-shape repair or advisory may touch it. Omitted, the probe casts
28
- * no veto and the delimiter heuristics decide alone.
25
+ * Target path used to infer a language for the tree-sitter syntax probe.
26
+ * Required for syntax-essential boundary retention and post-apply syntax
27
+ * advisories. Without it, only exact-text boundary normalization and its
28
+ * evidence-complete rejections run.
29
29
  */
30
30
  path?: string;
31
31
  }
@@ -35,12 +35,10 @@ export interface ApplyEditsOptions {
35
35
  * Returns the post-edit text and the first changed line number (1-indexed).
36
36
  * Throws if an anchor is out of bounds.
37
37
  *
38
- * Repairs that hinge on delimiter *semantics* (a range that swallowed the `}`
39
- * closing the construct above or below it) are subject to a parser veto when
40
- * `options.path` is supplied: the authored edits are materialized first, and if
41
- * that result parses it is returned untouched. A `}` in prose, a string, or a
42
- * regex literal is therefore never mistaken for a block closer. Only when the
43
- * authored result does not parse — or the language is unknown to the parser, so
44
- * balance arithmetic is the sole evidence — do the closer-spare repairs run.
38
+ * Mis-set replacement boundaries are repaired by {@link repairBoundaryVariants}
39
+ * when `options.path` lets tree-sitter judge the result. A parsing authored
40
+ * result is never second-guessed. For a broken result, only syntax-essential
41
+ * edge retention with matching indentation and exact outside-row echo removal
42
+ * are considered; every selected candidate must parse.
45
43
  */
46
44
  export declare function applyEdits(text: string, edits: readonly Edit[], options?: ApplyEditsOptions): ApplyResult;
@@ -86,50 +86,35 @@ export declare function pasteAfterBlockCloserLoweredWarning(line: number): strin
86
86
  /** Register `PUT >N*` anchor unresolvable; applied as `PUT >N`. */
87
87
  export declare function pasteAfterBlockUnresolvedLoweredWarning(line: number): string;
88
88
  /**
89
- * A one-sided boundary echo whose payload is too short to be the widened
90
- * range's full content: dropping the echo deletes range line(s) the payload
91
- * never restates (the "widened range" reading), while the "range shifted by
92
- * the echo" reading keeps them. The readings produce different files, so the
93
- * edit is rejected instead of repaired.
89
+ * A one-sided exact boundary echo cannot cover the selected range after the
90
+ * duplicated body rows are removed. Applying or dropping it would lose
91
+ * distinct range content, so the edit is rejected unless a parse-restoring
92
+ * boundary combination proves another reading.
94
93
  */
95
94
  export declare function ambiguousBoundaryEchoMessage(startLine: number, endLine: number, side: "leading" | "trailing", count: number): string;
96
95
  /**
97
- * A replacement range deletes trailing structural closer(s) the payload never
98
- * restates, and nothing anchors the payload inside the block those closers
99
- * terminate: the payload has no unmatched opener for them and its indentation
100
- * is not deeper than the closer. Sparing the closer would have to guess
101
- * whether the payload belongs before it (inside the block) or after it (a
102
- * sibling), so the edit is rejected instead of repaired.
96
+ * A syntax-essential selected edge can be retained on either side of the
97
+ * payload, but indentation does not establish which placement was intended.
103
98
  */
104
- export declare function ambiguousCloserSpareMessage(startLine: number, endLine: number, closerLine: number, count: number): string;
99
+ export declare function ambiguousBoundaryPlacementMessage(startLine: number, endLine: number): string;
105
100
  /**
106
- * A replacement range starts by deleting structural closer(s) the payload
107
- * never restates — the "range started one line early, on the `}` that ends
108
- * the construct above" mistake — but the payload's indentation claims a depth
109
- * inside the block those closers terminate, so whether the new content
110
- * belongs before or after the spared closer is ambiguous. Rejected instead of
111
- * repaired; the at-or-above-depth reading is auto-repaired by sparing the
112
- * closer ahead of the payload.
101
+ * Exact-text boundary rows were removed because the remaining payload covers
102
+ * the selected range and the same rows already survive immediately outside it.
113
103
  */
114
- export declare function ambiguousLeadingCloserSpareMessage(startLine: number, endLine: number, count: number): string;
104
+ export declare function textualBoundaryEchoWarning(startLine: number, leading: number, trailing: number): string;
115
105
  /**
116
- * A replacement range deletes more opening delimiter(s) than the payload
117
- * reopens while the matching closer(s) survive below the range — the
118
- * "payload is a complete construct but the range ends mid-block" mistake.
119
- * Surfaced as a warning, never a rejection: the applier is language-agnostic
120
- * and opener/closer text shape cannot prove a syntactic block (the braces may
121
- * be literal prose), so the edit applies as authored and the author decides.
106
+ * A replacement range's boundary disposition was corrected by the
107
+ * syntax-probe-judged search: syntax-essential source boundary rows were
108
+ * retained, or exact body echoes of surviving outside rows were removed. The
109
+ * authored result did not parse and the selected result does.
122
110
  */
123
- export declare function midBlockRangeWarning(startLine: number, endLine: number, orphaned: number): string;
111
+ export declare function boundaryVariantRepairWarning(startLine: number, kept: number, dropped: number): string;
124
112
  /**
125
113
  * The applied result no longer parses while the pre-edit content did: the
126
114
  * patch introduced a syntax error. Advisory, never a rejection — the applier
127
- * honors the authored edit — but the breakage is machine-confirmed (the
128
- * tree-sitter probe parsed the original and rejects the result), so it is
129
- * surfaced in the same response instead of waiting for a compiler pass. The
130
- * classic trigger is a balance-neutral misplacement: a statement landed on
131
- * the wrong line number with no delimiter anomaly for the repair heuristics
132
- * to notice.
115
+ * honors the authored edit — but the breakage is machine-confirmed by
116
+ * tree-sitter and surfaced in the same response instead of waiting for a
117
+ * compiler pass.
133
118
  */
134
119
  export declare function editBrokeParseWarning(firstChangedLine: number | undefined): string;
135
120
  /**
@@ -137,6 +122,8 @@ export declare function editBrokeParseWarning(firstChangedLine: number | undefin
137
122
  * `resolveBlockEdits` must run first.
138
123
  */
139
124
  export declare const UNRESOLVED_BLOCK_INTERNAL = "internal error: unresolved block edit reached the applier (resolveBlockEdits was not run).";
125
+ /** Internal invariant: clipboard edits must be concrete before application. */
126
+ export declare const UNRESOLVED_CLIPBOARD_INTERNAL = "internal error: unresolved clipboard edit reached the applier (resolveClipboardEdits was not run).";
140
127
  /** `REM` received a body row or coexists with line edits. */
141
128
  export declare const REM_TAKES_NO_BODY = "`REM` deletes the whole file and takes no body rows or line ops. Issue it alone under the header.";
142
129
  /** `MV` received a body row. */
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Syntax probe for candidate edit results, via the native tree-sitter parser.
3
3
  *
4
- * Delimiter-balance arithmetic cannot tell a block closer from a `}` inside a
5
- * regex literal, a string, or Markdown prose. A parser can, so it holds veto
6
- * power over every repair whose justification is "this line closes a syntactic
7
- * block": when the edit the author actually wrote still parses, no such repair
8
- * may rewrite it. The probe never *forces* a repair — an unrecognized language
9
- * or an already-broken file simply yields no veto, leaving the delimiter
10
- * heuristics as the only available evidence.
4
+ * Replacement-boundary repair uses parsing as its semantic filter: exact
5
+ * outside-row equality may justify dropping a duplicated payload edge, while
6
+ * retaining a selected source boundary additionally requires source-range
7
+ * structure, indentation, or a narrow pure-closer shape. An unrecognized
8
+ * language yields no structural proof, so only evidence-complete textual
9
+ * normalization remains available.
11
10
  */
11
+ /** Syntactic node boundaries outside a visible source range. */
12
+ export declare function enclosingBoundaries(lines: readonly string[], path: string, startLine: number, endLine: number): readonly number[];
12
13
  /**
13
14
  * `true` when `text` parses without a syntax error under the language inferred
14
15
  * from `path`. `false` covers "does not parse" and "cannot tell" alike — no
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/hashline",
4
- "version": "17.2.15",
4
+ "version": "17.3.1",
5
5
  "description": "Hashline: a compact, line-anchored patch language and applier. Pluggable FS/IO so it works over disk, in-memory, or any custom backend.",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -33,8 +33,8 @@
33
33
  "fmt": "biome format --write ."
34
34
  },
35
35
  "dependencies": {
36
- "@oh-my-pi/pi-natives": "17.2.15",
37
- "@oh-my-pi/pi-utils": "17.2.15"
36
+ "@oh-my-pi/pi-natives": "17.3.1",
37
+ "@oh-my-pi/pi-utils": "17.3.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/bun": "^1.3.14"