@oh-my-pi-zen/hashline 16.3.6-zen.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 ADDED
@@ -0,0 +1,359 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [16.3.3] - 2026-07-02
6
+
7
+ ### Breaking Changes
8
+
9
+ - Removed SnapshotStore.byHashExact. Consumers should now use byHash, which resolves collisions by returning the most recently recorded version.
10
+
11
+ ### Changed
12
+
13
+ - Improved patch application robustness by resolving 16-bit snapshot tag collisions to the most recent version instead of rejecting them.
14
+
15
+ ### Fixed
16
+
17
+ - Fixed frequent edit rejections after a structural-summary read (affecting parseable code over 100 lines) by automatically inlining unseen anchor lines and merging them into the snapshot's seen lines, allowing immediate retries to succeed without requiring a separate range re-read.
18
+
19
+ ## [16.3.0] - 2026-07-02
20
+
21
+ ### Changed
22
+
23
+ - Significantly improved performance on large files by optimizing stale-anchor remap validation.
24
+
25
+ ### Fixed
26
+
27
+ - Fixed an issue where snapshot tag collisions could cause line-anchored edits to be incorrectly applied to unrelated content, improving recovery and edit-preview safety.
28
+ - Fixed tracking of edit anchors when earlier in-session insertions or deletions shift unchanged target lines.
29
+ - Fixed hashline edit guidance and parsing errors for Markdown list rows.
30
+
31
+ ## [16.2.8] - 2026-06-30
32
+
33
+ ### Fixed
34
+
35
+ - Fixed hashline writes preserving UTF-8 BOM bytes when the host text decoder hides the leading `U+FEFF`. ([#3867](https://github.com/cagedbird043/oh-my-pi-zen/issues/3867))
36
+
37
+ ## [16.2.6] - 2026-06-29
38
+
39
+ ### Fixed
40
+
41
+ - Fixed a parser error ("payload line has no preceding hunk header") caused by stray dots before the trailing colon in hunk headers, improving compatibility with GLM 5.2 outputs.
42
+
43
+ ## [16.2.0] - 2026-06-27
44
+
45
+ ### Added
46
+
47
+ - Added `REM` (remove) and `MV` (move/rename) section operations to hashline patches, allowing files to be deleted or relocated (with snapshot history migration) directly within the edit tool.
48
+
49
+ ## [16.1.23] - 2026-06-26
50
+
51
+ ### Added
52
+
53
+ - Updated prompt documentation to include support for Markdown section operations
54
+
55
+ ### Fixed
56
+
57
+ - Improved file path recovery to correctly handle read-only or incorrectly typed paths
58
+
59
+ ## [16.1.14] - 2026-06-22
60
+
61
+ ### Fixed
62
+
63
+ - Improved delimiter-balance repair to correctly identify and spare deleted structural closers
64
+ - Prevented premature deletion of structural closers when existing code below the range covers them
65
+ - Accurate tracking of inserted lines to improve boundary repair logic for surrounding code blocks
66
+ - Fixed delimiter-balance repair so deleted closer suffixes are kept only when the replacement prefix still has unmatched openers for them, avoiding duplicated trailing braces while preserving omitted outer closers.
67
+
68
+ ## [16.1.8] - 2026-06-20
69
+
70
+ ### Fixed
71
+
72
+ - Fixed multi-hunk delimiter-balance repair so a `SWAP` that drops a structural closer no longer keeps it when another hunk already removed the matching opener (a deliberate wrapper removal); the missing-closer repair now weighs each group against the whole patch's residual delimiter balance — summed per hunk so quote/comment state never bleeds across non-contiguous hunks — and consumes that residual per repair so a genuine missing closer elsewhere still fires. ([#3142](https://github.com/cagedbird043/oh-my-pi-zen/issues/3142))
73
+
74
+ ## [16.1.2] - 2026-06-19
75
+
76
+ ### Changed
77
+
78
+ - Refined documentation and prompt instructions for clarity and brevity
79
+
80
+ ## [16.0.2] - 2026-06-16
81
+
82
+ ### Fixed
83
+
84
+ - Auto-repaired duplicated JSX/XML closing boundary lines at the end of single-line replacement expansions. ([#2705](https://github.com/cagedbird043/oh-my-pi-zen/issues/2705))
85
+
86
+ ## [16.0.1] - 2026-06-15
87
+
88
+ ### Fixed
89
+
90
+ - Auto-repaired one-sided multi-line boundary echoes by dropping delimiter-neutral duplicated boundary lines and emitted a boundary-echo warning
91
+ - Parser now treats a leading `\` on inline payload bodies as the payload delimiter, matching standalone payload rows.
92
+ - Restored the warning emitted when escaped indented payload rows (`\\ TEXT`) are accepted as payload delimiters.
93
+
94
+ ## [15.13.3] - 2026-06-15
95
+
96
+ ### Changed
97
+
98
+ - Changed the recommended hashline range separator from `..` to `.=` (e.g. `SWAP 1.=3:`, `DEL 4.=5`) so the inclusive `<=`-style end is self-evident. `HL_RANGE_SEP` is now `.=`; the prompt, grammar, error messages, and emitted headers all use it. The lenient parser still accepts the legacy `..` (and `-`/`…`/space) forms.
99
+
100
+ ## [15.13.2] - 2026-06-15
101
+
102
+ ### Breaking Changes
103
+
104
+ - Renamed all hashline DSL operators to concise abbreviated keywords:
105
+ - `replace` -> `SWAP`
106
+ - `delete` -> `DEL`
107
+ - `insert before`/`after`/`head`/`tail` -> `INS.PRE`/`POST`/`HEAD`/`TAIL`
108
+ - `replace_block` -> `SWAP.BLK`
109
+ - `delete_block` -> `DEL.BLK`
110
+ - `insert_after_block` -> `INS.BLK.POST`
111
+
112
+ ## [15.13.1] - 2026-06-15
113
+
114
+ ### Breaking Changes
115
+
116
+ - Rejected edits anchored to lines not displayed in the tagged read/search output, requiring unseen ranges to be re-read before reapplying
117
+
118
+ ### Changed
119
+
120
+ - Rejected `replace block`, `delete block`, and `insert after block` operations that resolve to a single line and instructed users to use the plain single-line form or anchor the true construct opener
121
+
122
+ ### Fixed
123
+
124
+ - Normalized cwd-relative hashline paths to forward-slash form on Windows.
125
+
126
+ ## [15.12.5] - 2026-06-13
127
+
128
+ ### Fixed
129
+
130
+ - Fixed delimiter-balance boundary repair so it does not keep a deleted structural closer when the replacement payload already restates that closer.
131
+
132
+ ## [15.12.0] - 2026-06-12
133
+
134
+ ### Changed
135
+
136
+ - Condensed all parser/applier/patcher error and warning messages: shorter wording, same diagnostic anchors (op names, line numbers, suggested fallback forms)
137
+
138
+ ## [15.11.4] - 2026-06-12
139
+
140
+ ### Added
141
+
142
+ - Added inward landing correction for `insert after block N:`: a body indented deeper than the block's closing line now slides back across the block's trailing closer lines and lands inside the block at its claimed depth, with a warning naming the landing line. Same conservative guards as the outward shift — comparable indentation only, closers only, abandoned when another hunk targets a crossed line; plain `insert after M:` stays literal
143
+ - Added closer-anchor lowering for `insert after block N:`: anchoring on a pure closing-delimiter line (where no block begins, so resolution previously failed the whole patch) now applies as plain `insert after N:` with a warning teaching the opener-only rule. `resolveBlockEdits` gained an `onWarning` callback; apply, preview, and patcher paths surface it on `warnings`
144
+
145
+ ### Changed
146
+
147
+ - Condensed the edit-tool prompt: one-line op definitions, 5–20-word rules, and a tighter `<critical>` recap; landing-correction mechanics are no longer described to the agent
148
+
149
+ ## [15.11.1] - 2026-06-11
150
+
151
+ ### Fixed
152
+
153
+ - Fixed the `insert after block N:` prompt guidance so it explicitly says N must be the block opener, not the closing delimiter or last visible line, and points visible closing-line edits to plain `insert after M:`. ([#2292](https://github.com/cagedbird043/oh-my-pi-zen/issues/2292))
154
+
155
+ ## [15.11.0] - 2026-06-10
156
+
157
+ ### Changed
158
+
159
+ - Block-unresolved errors (`replace block N:` / `delete block N` / `insert after block N:` failing to resolve a syntactic block) now append a numbered preview of the file around the anchor line — same `*`-marked context rows the hash-mismatch error shows — so the offending line is visible without a re-read
160
+
161
+ ## [15.10.11] - 2026-06-10
162
+
163
+ ### Breaking Changes
164
+
165
+ - Changed `BlockResolution.isDelete` to `BlockResolution.op` (`"replace" | "delete" | "insert_after"`) so resolutions can describe every block-anchored op
166
+
167
+ ### Added
168
+
169
+ - Added `insert after block N:` patch syntax to insert body rows after the last line of the tree-sitter-resolved block beginning on line N, so a statement can be placed after a construct without counting to its closing line
170
+ - Added depth-guided landing correction for `insert after N:` hunks: a body indented shallower than its anchor line slides past the structural closer lines below the anchor until depth returns to the body's level, with a warning naming the final landing line. The shift never crosses content lines, skips incomparable indentation styles and pure-closer bodies, and is abandoned when another hunk targets a crossed line
171
+ - Added a global byte ceiling to `InMemorySnapshotStore` (`maxTotalBytes`, default 64 MiB): the cap was previously per-file only, so a session reading many large files retained up to 30 paths × 4 full-text versions indefinitely
172
+
173
+ ### Changed
174
+
175
+ - Trimmed the `replace block N:` ops entry in the patch prompt to grammar and pointing rules; the usage doctrine it duplicated stays in the rules section
176
+ - Changed `buildCompactDiffPreview` to treat blank rows as gap separators alongside `…` markers: separators never stack (removed lines omitted from the preview no longer leave two adjacent), and leading/trailing separators are trimmed
177
+
178
+ ### Fixed
179
+
180
+ - Fixed the boundary-echo repair stripping payload edges without the balance-neutrality guard its own documentation promised: in brace-heavy code where bare `}` lines repeat, a payload intentionally beginning/ending with lines identical to the range's neighbors had both edges silently dropped, writing content that differed from what was authored
181
+ - Fixed lenient bare-body handling silently mutating payloads: interior blank rows in an un-prefixed body were dropped outright, and a body of numeric-keyed literals (`1: "one"` dict/YAML shapes) satisfied the uniform line-prefix check and had its keys stripped from every line — blank rows are now preserved when proven interior, and the uniform strip refuses lone-literal remainders
182
+ - Fixed the multi-section "all-or-nothing" claim being false for write failures: commits run serially, so a mid-batch write error left earlier sections on disk while the thrown error said nothing — the error now lists exactly which sections were written and which were not
183
+ - Fixed `delete`/`replace` ranges ending on the phantom trailing line of a newline-terminated file silently stripping the file's final newline; such anchors are now rejected with guidance toward `N-1` / `insert tail:` (inserts there remain valid, and genuine empty last lines of unterminated files stay deletable)
184
+
185
+ ## [15.10.5] - 2026-06-08
186
+
187
+ ### Added
188
+
189
+ - Added `maxAddedRunContext` option to control how many added lines are shown at each side of collapsed inserted runs, with `maxUnchangedRun` kept as a backward-compatible alias
190
+
191
+ ### Changed
192
+
193
+ - Changed `buildCompactDiffPreview` to omit removed lines from the preview while preserving removal counts for offset tracking
194
+ - Changed `buildCompactDiffPreview` to collapse long contiguous added runs with a bare `…` marker, keeping only the first and last `maxAddedRunContext` lines visible (the surrounding line numbers convey how many were elided)
195
+
196
+ ### Fixed
197
+
198
+ - Fixed compact edit previews to omit deleted content, keep visible lines anchored to the current file, and collapse long inserted runs with a bare `…` elision marker.
199
+ - Fixed compact edit previews to render added/current lines without diff-prefix padding and normalize adjacent ASCII/Unicode elision markers to one `…`.
200
+
201
+ ## [15.10.3] - 2026-06-08
202
+
203
+ ### Added
204
+
205
+ - Added a `BlockResolution` type and surfaced resolved block spans on `ApplyResult.blockResolutions` / `PatchSectionResult.blockResolutions`. `resolveBlockEdits` now accepts an `onResolved` callback that reports each `replace block N:` / `delete block N` anchor's resolved `[start, end]` span (and whether it was a delete). Spans are surfaced only on the no-drift apply paths, where the resolved line numbers line up with the tag the caller read.
206
+
207
+ ### Changed
208
+
209
+ - Reworked the `edit` tool prompt (`prompt.md`): added a `replace block N` vs `replace N..M` decision rule, documented that a leading decorator/attribute/doc-comment is a separate node not swept into the block (point N at the first decorator line, or use `replace N..M` for a Rust-style `///` sibling comment), reframed the blast-radius guidance so "block replace" no longer reads as the dangerous option, and added a decorated-definition example.
210
+
211
+ ## [15.10.2] - 2026-06-08
212
+
213
+ ### Fixed
214
+
215
+ - Stripped read-output line-number prefixes (`N:`) from auto-piped bare body rows so that pasting `3:text` without a `+` prefix no longer injects `3:` as literal content. Stripping is applied only when *every* bare row in the hunk carries the prefix (the signature of a pasted snapshot) and removes at most one prefix per row, so a genuine body that merely starts with `digits:` (YAML port maps, timestamps) is left intact ([#1492](https://github.com/cagedbird043/oh-my-pi-zen/issues/1492)).
216
+
217
+ ## [15.9.67] - 2026-06-06
218
+
219
+ ### Breaking Changes
220
+
221
+ - Changed hashline file section headers from `¶PATH#TAG` to `[PATH#TAG]` so model-authored edits use ASCII delimiters instead of a pilcrow sigil.
222
+
223
+ ### Fixed
224
+
225
+ - Fixed missing-header diagnostics and copied-content prefix stripping to consistently teach and recognize 4-hex snapshot tags.
226
+
227
+ ## [15.8.2] - 2026-06-03
228
+
229
+ ### Fixed
230
+
231
+ - Fixed delimiter-balance boundary repair to also drop a single duplicated structural opener (e.g. a restated `foo(` / `if (x) {` signature line surviving just above the range), not only duplicated closers. Zero-balance duplicates remain untouched.
232
+
233
+ ## [15.8.0] - 2026-06-02
234
+
235
+ ### Fixed
236
+
237
+ - Fixed hashline replacements that accidentally restated unchanged lines above and below the selected range so they no longer duplicate both boundary lines ([#1664](https://github.com/cagedbird043/oh-my-pi-zen/issues/1664)).
238
+
239
+ ## [15.7.0] - 2026-05-31
240
+
241
+ ### Added
242
+
243
+ - Added `replace block N:` and `delete block N` patch syntax to replace or delete the entire syntactic block that begins on line N using tree-sitter-resolved spans
244
+ - Added `BlockResolver` support in `Patcher` and `PatchSection.applyTo`/`applyPartialTo` to wire language-specific block-resolution at apply time
245
+ - Added `resolveBlockEdits` and block edit type definitions to the package API for resolving deferred `replace block` / `delete block` edits
246
+
247
+ ## [15.5.13] - 2026-05-29
248
+
249
+ ### Breaking Changes
250
+
251
+ - Changed hashline section tags from 3-hex to 4-hex content-hash tags, so legacy 3-digit tags are no longer valid
252
+ - Changed hashline syntax to verb-based v4: body-bearing ops are `replace N..M:`, `insert before N:`, `insert after N:`, `insert head:`, and `insert tail:`, while bodyless `delete N..M` handles deletion. Removed `>A..B` repeat rows and the old `prepend:` / `append:` virtual insert headers; `-` rows remain rejected with a teaching error.
253
+
254
+ ### Added
255
+
256
+ - Added `maxPaths` and `maxVersionsPerPath` options to `InMemorySnapshotStore` to bound tracked paths and per-path snapshot history
257
+ - Re-introduced balance-validated boundary repair in `applyEdits`. A replacement hunk (`replace N..M:` + body) is normalized so its payload preserves the deleted region's delimiter balance: when the body restates a closing delimiter that survives just outside the range (duplicate `}` / `);` / `]`) the echo is dropped, and when the range deletes a structural closer the body never restates (missing closer) the closer is spared instead of deleted. A repair fires only when one boundary operation drives the per-channel `()` / `[]` / `{}` imbalance to exactly zero while leaving surrounding text byte-identical (single-line ops are limited to pure structural-closer lines), so balance-preserving edits and intentional balanced duplicates are never touched. Bracket counting skips strings, template literals, and comments. Each repair surfaces a `delimiter-balance` warning through `ApplyResult.warnings`.
258
+
259
+ ### Changed
260
+
261
+ - Changed patch application to accept edits whenever the live file's normalized content hash matches the section tag, even when that anchor was not covered by a stored snapshot
262
+
263
+ ### Removed
264
+
265
+ - Removed `SnapshotStore.recordContiguous` and `SnapshotStore.recordSparse` in favor of full-file `record(path, fullText)` snapshots
266
+
267
+ ### Fixed
268
+
269
+ - Fixed hash mismatch rejections caused by CRLF or trailing spaces/tabs by normalizing those characters before computing file-hash tags
270
+
271
+ ## [15.5.12] - 2026-05-29
272
+
273
+ ### Changed
274
+
275
+ - `InMemorySnapshotStore` now coalesces consecutive same-path reads into one tag whenever their views agree on every shared line. Overlapping or directly abutting range reads extend the existing snapshot's contiguous run in place; reads separated by a gap union into a `SparseSnapshot` spanning both ranges. A disagreeing shared line is treated as "the file changed on disk" and mints a fresh tag, preserving the prior superset-dedup behavior. This stops sequential range reads of an unchanged file (e.g. `:50-100` then `:100-200`, or `:1-100` then `:150-200`) from fragmenting into separate anchors.
276
+
277
+ ## [15.5.11] - 2026-05-29
278
+
279
+ ### Added
280
+
281
+ - `MismatchError` now distinguishes "hash recognized but file content drifted" from "hash never recorded for this path". The latter (likely fabricated or carried over from a prior session) emits a dedicated `hash #X is not from this session` rejection message with explicit "never invent the tag" guidance. The `MismatchDetails` interface gains an optional `hashRecognized?: boolean` (defaults to `true` for backward compatibility); `MismatchError` exposes it as a readonly field so callers can branch on the cause.
282
+
283
+ ## [15.5.8] - 2026-05-28
284
+
285
+ ### Breaking Changes
286
+
287
+ - Removed the single-number hunk header shorthand. A hunk header now REQUIRES two line numbers (`A A` for a single line, `A B` for a range); a bare `A` row throws `single-number hunk header "A" is no longer accepted`. The `&A` body-row shorthand for `&A..A` is unchanged.
288
+ - Changed hunk header syntax from `A-B:` to `@@ A..B @@` with `@@ A @@` shorthand for single lines
289
+ - Changed repeat payload sigil from `^A-B` to `&A..B` with `&A` shorthand for single lines
290
+ - Changed range separator from `-` to `..` in all contexts (anchors and repeats)
291
+ - Changed empty hunk behavior: concrete ranges now delete (no blank-line insertion); BOF/EOF empty hunks are now no-ops
292
+ - Removed `ApplyOptions` parameter from `applyEdits()` and related APIs; auto-absorb behavior is no longer configurable
293
+ - Removed diagnostic warnings for auto-absorbed duplicates from `ApplyResult`; warnings now come only from parser, patcher, or recovery
294
+ - Removed legacy hashline block syntax `A-B:`, `A-B:-`, and `^A-B` and replaced edits with `@@ A..B @@` hunks using `+` and `&` body rows
295
+ - Removed `A:` shorthand syntax; use explicit `A-A:` for single-line anchors
296
+ - Removed `↑` and `↓` payload sigils; use `|TEXT` for literal rows and `^A-B` for repeating original lines
297
+ - Removed standalone delete rows; use inline `A-B:-` syntax instead
298
+ - Removed `after_anchor` cursor kind; all inserts now use `before_anchor` positioning
299
+ - Replaced insert-above/insert-below payload sigils with linear body rows: `|TEXT` emits literal text and `^A-B` repeats original file lines inline.
300
+ - Replaced standalone delete rows with inline range deletes: use `A-B:-`.
301
+ - Changed empty `A-B:`, `BOF:`, and `EOF:` blocks to write one blank line instead of being rejected.
302
+
303
+ ### Added
304
+
305
+ - Added compatibility parsing for apply_patch-style and unified-diff row noise by stripping path noise and converting context/delete body rows into hashline-compatible operations with warnings
306
+ - Added `A-B:-` inline delete syntax for concrete range anchors
307
+ - Added `^A-B` repeat payload syntax to emit original file lines inline
308
+ - Added support for empty anchor blocks to write one blank line at the anchor position
309
+
310
+ ### Changed
311
+
312
+ - Changed unified-diff compatibility mode to silently drop `-old` rows and convert context rows to `+TEXT` literals with a warning instead of rejecting them
313
+ - Changed `ABORT_MARKER` behavior to terminate parsing without surfacing a warning
314
+ - Changed numeric ranges to `A..B` form and accepted `@@ A @@` as shorthand for `@@ A..A @@`
315
+ - Changed empty hunk behavior so a concrete empty hunk deletes the selected range and `BOF`/`EOF` empty hunks no longer insert a blank line
316
+ - Changed parse behavior for `*** Abort` to stop processing without returning a speculative truncation warning
317
+ - Changed payload row format from three sigils (`|`, `↑`, `↓`) to two (`|`, `^`)
318
+ - Changed range anchor syntax to require explicit `A-B` form (no single-line shorthand)
319
+ - Changed error messages to reference new syntax and remove references to removed sigils
320
+
321
+ ## [15.5.5] - 2026-05-27
322
+
323
+ ### Breaking Changes
324
+
325
+ - Changed hashline payload continuations from `+TEXT` to `\TEXT`; use `\` for an explicit blank payload line.
326
+
327
+ ### Added
328
+
329
+ - Added `parsePatchStreaming(diff)` and `PatchSection.applyPartialTo(text, options)` for incremental diff previews. Both tolerate a trailing in-flight op (no payload yet, or a per-token parse error mid-stream) instead of throwing or emitting a phantom empty-payload edit.
330
+ - Added `Executor.endStreaming()` — sibling of `end()` that drops a pending op with no accumulated payload rather than flushing it.
331
+
332
+ ### Fixed
333
+
334
+ - Parser now skips markdown-style `# ...` lines when they directly precede a hashline operation, making model-generated explanatory rows in prompt examples non-blocking.
335
+
336
+ ### Removed
337
+
338
+ - Removed the `A-B!` / `A!` deletion operator. Use `A-B:` with the desired payload (or empty payload to blank the range) instead.
339
+
340
+ All notable changes to this package will be documented in this file.
341
+
342
+ ## [15.5.4] - 2026-05-27
343
+
344
+ ### Added
345
+
346
+ - Added a high-level `Patcher` API with all-or-nothing `apply` and staged `prepare`/`commit` flows for multi-file patch updates
347
+ - Added pluggable `Filesystem` and `SnapshotStore` abstractions with built-in `NodeFilesystem`, `InMemoryFilesystem`, and `InMemorySnapshotStore` adapters
348
+ - Added patch parsing that consumes `¶PATH#HASH` hunk headers, validates section file hashes, and supports optional patch envelope markers
349
+ - Added tolerant input handling that strips read/search prefixes and supports optional `cwd`/fallback-path resolution when parsing patch payloads
350
+ - Added automatic line-ending and BOM normalization on read, with original encoding shape restored on write
351
+ - Added follow-up helpers `buildCompactDiffPreview` and `streamHashLines` for compact diff previews and chunked streaming of numbered lines
352
+ - Added stale-file-hash recovery that replays edits against snapshots and merges results onto current file content when direct hash validation fails
353
+ - Initial standalone release. Extracted from `@oh-my-pi-zen/pi-coding-agent`.
354
+
355
+ ### Fixed
356
+
357
+ - Fixed repeated patch application mutating cached `after_anchor` edits between target snapshots
358
+ - Fixed multi-section patching to preflight write policies and reject duplicate canonical targets before any section is committed
359
+ - Fixed mixed line-ending restoration to preserve the first newline style instead of rewriting ties to LF
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @oh-my-pi-zen/hashline
2
+
3
+ A compact, line-anchored patch language and applier.
4
+
5
+ Hashline is a diff format designed for LLM-driven file edits. It binds every
6
+ hunk to a file-content hash so stale anchors are rejected before they corrupt
7
+ code, and it abstracts over the filesystem so the same patcher works on disk,
8
+ in memory, over the network, or against any custom backend.
9
+
10
+ ## Quick start
11
+
12
+ ```ts
13
+ import {
14
+ Filesystem,
15
+ InMemoryFilesystem,
16
+ InMemorySnapshotStore,
17
+ Patcher,
18
+ Patch,
19
+ } from "@oh-my-pi-zen/hashline";
20
+
21
+ const fs = new InMemoryFilesystem();
22
+ const snapshots = new InMemorySnapshotStore();
23
+ const before = `const greeting = "hi";\nexport { greeting };\n`;
24
+ await fs.writeText("hello.ts", before);
25
+
26
+ const tag = snapshots.record("hello.ts", before);
27
+ const patcher = new Patcher({ fs, snapshots });
28
+ const patch = Patch.parse(String.raw`[hello.ts#${tag}]
29
+ SWAP 1.=1:
30
+ +const greeting = "hello";`);
31
+ const result = await patcher.apply(patch);
32
+
33
+ console.log(result.sections[0].op); // "update"
34
+ console.log(await fs.readText("hello.ts"));
35
+ ```
36
+
37
+ ## Format
38
+
39
+ See [`src/prompt.md`](./src/prompt.md) for the user-facing description and
40
+ [`src/grammar.lark`](./src/grammar.lark) for the formal grammar.
41
+
42
+ Each file section starts with `[PATH#TAG]`. The tag is a 4-hex
43
+ content hash of the full normalized file text recorded by the
44
+ `SnapshotStore`, and it is not meaningful outside that store. The patcher
45
+ protects against stale anchors by resolving the tag, verifying the live file
46
+ still matches the recorded content hash, and refusing or attempting
47
+ session-aware recovery on mismatch.
48
+
49
+ Inside a section:
50
+ - `SWAP A.=B:` — replace lines A.=B with following `+TEXT` body rows.
51
+ - `SWAP.BLK A:` — replace the syntactic block beginning on line A.
52
+ - `DEL A.=B` / `DEL.BLK A` — delete concrete lines or a resolved block.
53
+ - `INS.PRE A:` / `INS.POST A:` / `INS.HEAD:` / `INS.TAIL:` — insert following body rows.
54
+ - `INS.BLK.POST A:` — insert following body rows after the resolved block's last line.
55
+ - `REM` — delete the whole file named by the section header.
56
+ - `MV DEST` — move/rename the section file to `DEST` (optionally after line edits).
57
+ - `+TEXT` — literal body row (use `+` alone for a blank line).
58
+
59
+ ## Abstractions
60
+
61
+ ### `Filesystem`
62
+
63
+ Read and write text by path. The default implementations:
64
+
65
+ - `InMemoryFilesystem` — backed by a `Map`. Tests, sandboxes.
66
+ - `NodeFilesystem` — disk-backed via `Bun.file`/`Bun.write`. Default for CLIs.
67
+
68
+ Subclass `Filesystem` to wire hashline into any storage: VFS, S3, an LSP
69
+ text-document protocol, a Git tree, anything.
70
+
71
+ ### `SnapshotStore`
72
+
73
+ Required. Hashline tags are full-file content hashes recorded per path, so
74
+ `Patcher` must receive the store that observed them. Recovery replays edits
75
+ against the cached pre-edit snapshot and 3-way-merges onto current content
76
+ when the live file diverged.
77
+
78
+ ### `Patcher`
79
+
80
+ The orchestration class. Reads, normalizes line endings + BOM, applies edits,
81
+ restores line endings, and writes via the configured `Filesystem`. Multi-section
82
+ patches are preflighted up front so a partial batch never lands.
@@ -0,0 +1,10 @@
1
+ import type { ApplyResult, Edit } from "./types";
2
+ /** A line that is nothing but closing delimiters: `}`, `)`, `];`, `})`, `},`. */
3
+ export declare const STRUCTURAL_CLOSER_RE: RegExp;
4
+ /**
5
+ * Apply a parsed list of edits to a text body. Pure function — no I/O.
6
+ *
7
+ * Returns the post-edit text and the first changed line number (1-indexed).
8
+ * Throws if an anchor is out of bounds.
9
+ */
10
+ export declare function applyEdits(text: string, edits: readonly Edit[]): ApplyResult;
@@ -0,0 +1,39 @@
1
+ import type { BlockResolution, BlockResolver, Edit } from "./types";
2
+ export interface ResolveBlockEditsOptions {
3
+ /**
4
+ * How to handle a replace/delete block edit that cannot be resolved
5
+ * (missing resolver or a `null` span). `"throw"` (default) raises a
6
+ * `blockUnresolvedMessage` error — used by the authoritative apply + final
7
+ * preview paths. `"drop"` silently skips the edit — used by the streaming
8
+ * preview, where a half-written file or transient parse error must not
9
+ * throw. Unresolvable `insert_after_block N:` edits never reach this: they
10
+ * are lowered to plain `insert after N:` with a warning.
11
+ */
12
+ onUnresolved?: "throw" | "drop";
13
+ /**
14
+ * Invoked once per successfully resolved block edit, in patch order, with
15
+ * the anchor line and the concrete span it resolved to. Lets the host echo
16
+ * the resolution back to the caller. Never fired for dropped/unresolvable
17
+ * edits.
18
+ */
19
+ onResolved?: (resolution: BlockResolution) => void;
20
+ /**
21
+ * Invoked once per diagnostic produced while resolving — currently the
22
+ * `insert_after_block N:` lowerings (closer anchor or unresolvable block).
23
+ * Hosts should surface these on the apply result's `warnings`.
24
+ */
25
+ onWarning?: (message: string) => void;
26
+ }
27
+ /** True when at least one edit is an unresolved deferred block edit. */
28
+ export declare function hasBlockEdit(edits: readonly Edit[]): boolean;
29
+ /**
30
+ * Resolve every deferred block edit in `edits` against `text` (parsed as the
31
+ * language inferred from `path`). Non-block edits pass through untouched.
32
+ * Returns a fresh edit list with no `block` variants. The fast path returns the
33
+ * input unchanged when there is nothing to resolve.
34
+ *
35
+ * Synthesized inserts/deletes carry sequential `index` values for readability
36
+ * only — {@link applyEdits} re-derives every edit's index from array order, so
37
+ * the passthrough edits keeping their original indices is harmless.
38
+ */
39
+ export declare function resolveBlockEdits(edits: readonly Edit[], text: string, path: string, resolver: BlockResolver | undefined, options?: ResolveBlockEditsOptions): readonly Edit[];
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Re-number a unified diff that uses the `+<lineNum>|content` /
3
+ * `-<lineNum>|content` / ` <lineNum>|content` line format into a compact
4
+ * current-file preview. Removed lines are counted for stats and post-edit
5
+ * offset tracking, but omitted from the preview. Added and context lines are
6
+ * anchored to their post-edit positions so a follow-up edit can reuse visible
7
+ * concrete lines directly. Long contiguous added runs are summarized with a
8
+ * `…` marker instead of echoing every inserted line.
9
+ *
10
+ * This is intentionally decoupled from the diff producer: anything that
11
+ * emits the `<sign><lineNum>|<content>` shape works.
12
+ */
13
+ import type { CompactDiffOptions, CompactDiffPreview } from "./types";
14
+ export declare function buildCompactDiffPreview(diff: string, options?: CompactDiffOptions): CompactDiffPreview;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Hashline format primitives: sigils, separators, regex fragments, and
3
+ * display helpers. These are the single source of truth for the parser, the
4
+ * tokenizer, the prompt, and the formal grammar.
5
+ */
6
+ import type { Cursor } from "./types";
7
+ /** File-section header delimiters: `[path#hash]`. */
8
+ export declare const HL_FILE_PREFIX = "[";
9
+ export declare const HL_FILE_SUFFIX = "]";
10
+ /** Payload sigil for literal body rows. */
11
+ export declare const HL_PAYLOAD_REPLACE = "+";
12
+ /** Hunk-header keyword for concrete line replacement. */
13
+ export declare const HL_REPLACE_KEYWORD = "SWAP";
14
+ /** Hunk-header keyword for concrete line deletion. */
15
+ export declare const HL_DELETE_KEYWORD = "DEL";
16
+ /** Hunk-header keyword for insertion operations. */
17
+ export declare const HL_INSERT_KEYWORD = "INS";
18
+ /** Insert position keyword for inserting before a concrete line. */
19
+ export declare const HL_INSERT_BEFORE = "PRE";
20
+ /** Insert position keyword for inserting after a concrete line. */
21
+ export declare const HL_INSERT_AFTER = "POST";
22
+ /** Insert position keyword for inserting at the start of the file. */
23
+ export declare const HL_INSERT_HEAD = "HEAD";
24
+ /** Insert position keyword for inserting at the end of the file. */
25
+ export declare const HL_INSERT_TAIL = "TAIL";
26
+ /** Hunk-header keyword: `SWAP.BLK N:` resolves N to a tree-sitter block range and replaces its span. */
27
+ export declare const HL_REPLACE_BLOCK_KEYWORD = "SWAP.BLK";
28
+ /** Hunk-header keyword: `DEL.BLK N` resolves N to a tree-sitter block range and deletes its span. */
29
+ export declare const HL_DELETE_BLOCK_KEYWORD = "DEL.BLK";
30
+ /** Hunk-header keyword: `INS.BLK.POST N:` inserts after the last line of the tree-sitter block at N. */
31
+ export declare const HL_INSERT_AFTER_BLOCK_KEYWORD = "INS.BLK.POST";
32
+ /** File-level keyword: `REM` deletes the whole file named by the section header. */
33
+ export declare const HL_REM_KEYWORD = "REM";
34
+ /** File-level keyword: `MV DEST` renames/moves the section file to `DEST`. */
35
+ export declare const HL_MOVE_KEYWORD = "MV";
36
+ export declare const HL_HEADER_COLON = ":";
37
+ /** Separator between a hashline file path and its opaque snapshot tag. */
38
+ export declare const HL_FILE_HASH_SEP = "#";
39
+ /** Separator between two line numbers in a range, e.g. `5.=10`. */
40
+ export declare const HL_RANGE_SEP = ".=";
41
+ /** Separator between a line number and displayed line content in hashline mode. */
42
+ export declare const HL_LINE_BODY_SEP = ":";
43
+ /** Bare positive line-number Lid (no decorations, no captures, no anchors). */
44
+ export declare const HL_LINE_RE_RAW = "[1-9]\\d*";
45
+ /** Capture-group form of {@link HL_LINE_RE_RAW}. */
46
+ export declare const HL_LINE_CAPTURE_RE_RAW = "([1-9]\\d*)";
47
+ /** Format a concrete replacement hunk header. */
48
+ export declare function formatReplaceHeader(start: number, end: number): string;
49
+ /** Format a concrete deletion hunk header. */
50
+ export declare function formatDeleteHeader(start: number, end?: number): string;
51
+ /** Format an insertion hunk header for a cursor position. */
52
+ export declare function formatInsertHeader(cursor: Cursor): string;
53
+ /** Number of hex characters in a content-derived file-hash tag. */
54
+ export declare const HL_FILE_HASH_LENGTH = 4;
55
+ /** Canonical uppercase hexadecimal content-hash tag carried by a hashline section header. */
56
+ export declare const HL_FILE_HASH_RE_RAW = "[0-9A-F]{4}";
57
+ /** Capture-group form of {@link HL_FILE_HASH_RE_RAW}. */
58
+ export declare const HL_FILE_HASH_CAPTURE_RE_RAW = "([0-9A-F]{4})";
59
+ /** Regex-escaped form of {@link HL_LINE_BODY_SEP}, safe for embedding inside a regex. */
60
+ export declare const HL_LINE_BODY_SEP_RE_RAW: string;
61
+ /**
62
+ * Representative file-hash tags for use in user-facing error messages and
63
+ * prompt examples.
64
+ */
65
+ export declare const HL_FILE_HASH_EXAMPLES: readonly ["1A2B", "3C4D", "9F3E"];
66
+ /**
67
+ * Compute the content-derived hash tag carried by a hashline section header.
68
+ * The tag is a 4-hex fingerprint of the whole file's normalized text: any read
69
+ * of byte-identical content mints the same tag, and a follow-up edit anchored
70
+ * at any line validates whenever the live file still hashes to it.
71
+ */
72
+ export declare function computeFileHash(text: string): string;
73
+ /**
74
+ * Format a comma-separated list of example anchors with an optional line-number
75
+ * prefix, quoted for inclusion in error messages: `"160", "42", "7"`.
76
+ */
77
+ export declare function describeAnchorExamples(linePrefix?: string): string;
78
+ /** Format a hashline section header for a file path and snapshot tag. */
79
+ export declare function formatHashlineHeader(filePath: string, fileHash: string): string;
80
+ /** Formats a single numbered line as `LINE:TEXT`. */
81
+ export declare function formatNumberedLine(lineNumber: number, line: string): string;
82
+ /** Format file text with hashline-mode line-number prefixes for display. */
83
+ export declare function formatNumberedLines(text: string, startLine?: number): string;