@oh-my-pi/hashline 15.5.6 → 15.5.8

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
@@ -19,15 +19,15 @@ import {
19
19
  } from "@oh-my-pi/hashline";
20
20
 
21
21
  const fs = new InMemoryFilesystem();
22
- await fs.writeText(
23
- "hello.ts",
24
- `const greeting = "hi";\nexport { greeting };\n`,
25
- );
26
-
27
- const patcher = new Patcher({ fs });
28
- const patch = Patch.parse(String.raw`¶hello.ts
29
- 1:
30
- |const greeting = "hello";`);
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.recordContiguous("hello.ts", 1, before.split("\n"), { fullText: before });
27
+ const patcher = new Patcher({ fs, snapshots });
28
+ const patch = Patch.parse(String.raw`¶hello.ts#${tag}
29
+ @@ 1..1 @@
30
+ +const greeting = "hello";`);
31
31
  const result = await patcher.apply(patch);
32
32
 
33
33
  console.log(result.sections[0].op); // "update"
@@ -39,19 +39,19 @@ console.log(await fs.readText("hello.ts"));
39
39
  See [`src/prompt.md`](./src/prompt.md) for the user-facing description and
40
40
  [`src/grammar.lark`](./src/grammar.lark) for the formal grammar.
41
41
 
42
- Each hunk starts with a `¶PATH#HASH` header. The hash is a 4-hex-character
43
- xxHash32 truncation of the file's LF-normalized content. The hash protects
44
- against stale anchors: if the file changed between the read that produced the
45
- hash and the edit, the patcher refuses (or, with a `SnapshotStore`, tries
46
- session-aware recovery).
42
+ Each file section starts with `¶PATH#TAG`. The tag is a 3-hex opaque
43
+ pointer into the `SnapshotStore` that minted it; it is not content-derived
44
+ and is not meaningful outside that store. The patcher protects against
45
+ stale anchors by resolving the tag, verifying the recorded snapshot lines
46
+ against live file content, and refusing or attempting session-aware
47
+ recovery on mismatch.
47
48
 
48
- Inside a hunk:
49
-
50
- - `A-B:` anchor lines A..B (single-anchor `A:` is sugar for `A-A:`).
51
- - `BOF:` / `EOF:` virtual anchors at the beginning/end of file.
52
- - `|TEXT` — replace-bucket payload. A non-empty replace bucket replaces A..B.
53
- - `↑TEXT`insert before A (`BOF:` treats `↑`/`↓` equivalently).
54
- - `↓TEXT` — insert after B (`EOF:` treats `↑`/`↓` equivalently).
49
+ Inside a section:
50
+ - `@@ A..B @@` — open a hunk on lines A..B (use `@@ A,A @@` for a single line; bare `@@ A @@` is also accepted).
51
+ - `@@ BOF @@` / `@@ EOF @@` virtual hunks at the beginning/end of file.
52
+ - `+TEXT` literal body row (use `+` alone for a blank line).
53
+ - `&A..B` — repeat original file lines A..B inline (`&A` for one line).
54
+ - Empty body delete the selected range.
55
55
 
56
56
  ## Abstractions
57
57
 
@@ -67,9 +67,9 @@ text-document protocol, a Git tree, anything.
67
67
 
68
68
  ### `SnapshotStore`
69
69
 
70
- Optional. When provided to `Patcher`, hashline tries to recover from a stale
71
- section hash by replaying the edit against a cached pre-edit snapshot of the
72
- file and 3-way-merging onto the current content. See `recovery.ts`.
70
+ Required. Hashline tags are opaque store pointers, so `Patcher` must receive
71
+ the store that minted them. Recovery replays edits against the cached pre-edit
72
+ snapshot and 3-way-merges onto current content when the live file diverged.
73
73
 
74
74
  ### `Patcher`
75
75
 
@@ -1,9 +1,8 @@
1
- import type { ApplyOptions, ApplyResult, Edit } from "./types";
1
+ import type { ApplyResult, Edit } from "./types";
2
2
  /**
3
3
  * Apply a parsed list of edits to a text body. Pure function — no I/O.
4
4
  *
5
- * Returns the post-edit text, the first changed line number (1-indexed), and
6
- * any diagnostic warnings produced by the auto-absorb heuristics or by the
7
- * structural-boundary delete check. Throws if an anchor is out of bounds.
5
+ * Returns the post-edit text and the first changed line number (1-indexed).
6
+ * Throws if an anchor is out of bounds.
8
7
  */
9
- export declare function applyEdits(text: string, edits: Edit[], options?: ApplyOptions): ApplyResult;
8
+ export declare function applyEdits(text: string, edits: Edit[]): ApplyResult;
@@ -1,61 +1,57 @@
1
1
  /**
2
- * Hashline format primitives: sigils, separators, regex fragments, and the
3
- * file-hash computation. These are the single source of truth for the
4
- * parser, the tokenizer, the prompt, and the formal grammar.
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
5
  */
6
- /** Anchor terminator for every hashline operation block. */
7
- export declare const HL_OP_REPLACE = ":";
8
- /** Payload sigil for lines that replace the anchored range in place. */
9
- export declare const HL_PAYLOAD_REPLACE = "|";
10
- /** Payload sigil for lines inserted before the anchored range. */
11
- export declare const HL_PAYLOAD_ABOVE = "\u2191";
12
- /** Payload sigil for lines inserted after the anchored range. */
13
- export declare const HL_PAYLOAD_BELOW = "\u2193";
14
- /** All hashline payload sigils, concatenated for fast membership tests. */
15
- export declare const HL_PAYLOAD_CHARS = "|\u2191\u2193";
16
- /** Hashline edit file-section header marker. */
6
+ /** File-section header prefix: `¶path#hash`. */
17
7
  export declare const HL_FILE_PREFIX = "\u00B6";
18
- /** Separator between a hashline file path and its file hash. */
8
+ /** Payload sigil for literal body rows. */
9
+ export declare const HL_PAYLOAD_REPLACE = "+";
10
+ /** Payload sigil for body rows that repeat original file lines. */
11
+ export declare const HL_PAYLOAD_REPEAT = "&";
12
+ /** All hashline payload sigils, concatenated for fast membership tests. */
13
+ export declare const HL_PAYLOAD_CHARS = "+&";
14
+ /** Separator between a hashline file path and its opaque snapshot tag. */
19
15
  export declare const HL_FILE_HASH_SEP = "#";
16
+ /** Separator between two line numbers in a range, e.g. `5..10`. */
17
+ export declare const HL_RANGE_SEP = "..";
20
18
  /** Separator between a line number and displayed line content in hashline mode. */
21
19
  export declare const HL_LINE_BODY_SEP = ":";
22
20
  /**
23
21
  * Decoration prefix that may precede a line number in tool output:
24
- * `>` (context line in grep), `-` (removed line), `*` (match line).
25
- * Any combination, in any order, surrounded by optional whitespace. Output
26
- * formatters emit at most one decoration per line; the parser stays liberal
27
- * because it accepts whatever the model echoes back.
22
+ * `*` (match line), `>` (context line in grep). Any combination, in any
23
+ * order, surrounded by optional whitespace. Output formatters emit at most
24
+ * one decoration per line; the parser stays liberal because it accepts
25
+ * whatever the model echoes back.
28
26
  */
29
- export declare const HL_ANCHOR_DECORATION_RE_RAW = "\\s*[>\\-*]*\\s*";
27
+ export declare const HL_ANCHOR_DECORATION_RE_RAW = "\\s*[>*]*\\s*";
30
28
  /** Capture-group regex source for a decorated bare line-number anchor. */
31
- export declare const HL_ANCHOR_RE_RAW = "\\s*[>\\-*]*\\s*(\\d+)";
29
+ export declare const HL_ANCHOR_RE_RAW = "\\s*[>*]*\\s*(\\d+)";
32
30
  /** Bare positive line-number Lid (no decorations, no captures, no anchors). */
33
31
  export declare const HL_LINE_RE_RAW = "[1-9]\\d*";
34
32
  /** Capture-group form of {@link HL_LINE_RE_RAW}. */
35
33
  export declare const HL_LINE_CAPTURE_RE_RAW = "([1-9]\\d*)";
36
- /** Four-hex-character file hash carried by a hashline section header. */
37
- export declare const HL_FILE_HASH_RE_RAW = "[0-9a-f]{4}";
34
+ /** Regex for repeat payload rows (`&A..B`). */
35
+ export declare const HL_PAYLOAD_REPEAT_RE: RegExp;
36
+ /** Number of hex characters in an opaque snapshot tag. */
37
+ export declare const HL_FILE_HASH_LENGTH = 3;
38
+ /** Canonical uppercase hexadecimal opaque snapshot tag carried by a hashline section header. */
39
+ export declare const HL_FILE_HASH_RE_RAW = "[0-9A-F]{3}";
38
40
  /** Capture-group form of {@link HL_FILE_HASH_RE_RAW}. */
39
- export declare const HL_FILE_HASH_CAPTURE_RE_RAW = "([0-9a-f]{4})";
41
+ export declare const HL_FILE_HASH_CAPTURE_RE_RAW = "([0-9A-F]{3})";
40
42
  /** Regex-escaped form of {@link HL_LINE_BODY_SEP}, safe for embedding inside a regex. */
41
43
  export declare const HL_LINE_BODY_SEP_RE_RAW: string;
42
44
  /**
43
- * Representative file hashes for use in user-facing error messages and prompt
44
- * examples.
45
+ * Representative snapshot tags for use in user-facing error messages and
46
+ * prompt examples.
45
47
  */
46
- export declare const HL_FILE_HASH_EXAMPLES: readonly ["1a2b", "3c4d", "9f3e"];
48
+ export declare const HL_FILE_HASH_EXAMPLES: readonly ["0A3", "1F7", "3C9"];
47
49
  /**
48
50
  * Format a comma-separated list of example anchors with an optional line-number
49
51
  * prefix, quoted for inclusion in error messages: `"160", "42", "7"`.
50
52
  */
51
53
  export declare function describeAnchorExamples(linePrefix?: string): string;
52
- /**
53
- * Compute the 4-hex-character hash carried by a hashline section header. The
54
- * hash normalizes CR characters and trailing whitespace before hashing so
55
- * platform line endings and display-trimmed lines do not invalidate anchors.
56
- */
57
- export declare function computeFileHash(text: string): string;
58
- /** Format a hashline section header for a file path and file hash. */
54
+ /** Format a hashline section header for a file path and snapshot tag. */
59
55
  export declare function formatHashlineHeader(filePath: string, fileHash: string): string;
60
56
  /** Formats a single numbered line as `LINE:TEXT`. */
61
57
  export declare function formatNumberedLine(lineNumber: number, line: string): string;
@@ -1,4 +1,4 @@
1
- import type { ApplyOptions, ApplyResult, Edit, SplitOptions } from "./types";
1
+ import type { ApplyResult, Edit, SplitOptions } from "./types";
2
2
  interface RawSection {
3
3
  path: string;
4
4
  fileHash?: string;
@@ -36,21 +36,21 @@ export declare class PatchSection {
36
36
  /** Warnings emitted during parsing of this section. */
37
37
  get warnings(): readonly string[];
38
38
  /**
39
- * True when at least one edit anchors to a concrete file line (range or
40
- * before/after_anchor insert). Pure BOF/EOF inserts do not count: those
41
- * are safe to apply to files that don't yet exist.
39
+ * True when at least one edit anchors to concrete file content. Pure BOF/EOF
40
+ * literal inserts do not count: those are safe to apply to files that don't
41
+ * yet exist.
42
42
  */
43
43
  get hasAnchorScopedEdit(): boolean;
44
44
  /** Anchor lines touched by this section, sorted ascending and deduplicated. */
45
45
  collectAnchorLines(): readonly number[];
46
46
  /**
47
47
  * Apply this section's edits to `text` and return the post-edit result.
48
- * Pure: does no I/O, does not validate the section file hash. The
49
- * {@link Patcher} owns hash validation and recovery; reach for this
48
+ * Pure: does no I/O, does not validate the section snapshot tag. The
49
+ * {@link Patcher} owns tag validation and recovery; reach for this
50
50
  * method directly when you've already validated the file content and
51
51
  * just want the result.
52
52
  */
53
- applyTo(text: string, options?: ApplyOptions): ApplyResult;
53
+ applyTo(text: string): ApplyResult;
54
54
  /**
55
55
  * Streaming-tolerant counterpart to {@link applyTo}. Uses
56
56
  * {@link parsePatchStreaming} so a trailing in-flight op (no payload yet,
@@ -58,7 +58,7 @@ export declare class PatchSection {
58
58
  * empty-payload edit. Intended for incremental diff previews; the writer
59
59
  * path should always use {@link applyTo}.
60
60
  */
61
- applyPartialTo(text: string, options?: ApplyOptions): ApplyResult;
61
+ applyPartialTo(text: string): ApplyResult;
62
62
  }
63
63
  /**
64
64
  * A parsed hashline patch — zero or more {@link PatchSection}s, each rooted
@@ -13,24 +13,54 @@ export declare const END_PATCH_MARKER = "*** End Patch";
13
13
  /**
14
14
  * Recovery sentinel emitted by an agent loop when a contaminated tool-call
15
15
  * stream is truncated mid-call. Behaves like {@link END_PATCH_MARKER} for
16
- * parsing — terminates the line loop — and additionally surfaces a warning
17
- * so the caller knows to re-issue any remaining edits.
16
+ * parsing — terminates the line loop — and does not surface a warning.
18
17
  */
19
18
  export declare const ABORT_MARKER = "*** Abort";
20
- /** Warning text appended to the tool result when {@link ABORT_MARKER} terminates parsing. */
21
- export declare const ABORT_WARNING = "Tool stream truncated mid-call due to detected output corruption. Applied ops above are valid. Re-issue any remaining edits.";
22
19
  /**
23
- * Warning text appended when two consecutive blocks target the exact same
24
- * concrete range. The second block wins; the first block is discarded.
20
+ * Warning text appended when two consecutive hunks target the exact same
21
+ * concrete range. The second hunk wins; the first is discarded.
25
22
  */
26
- export declare const REPLACE_PAIR_COALESCED_WARNING = "Detected two identical-range hashline blocks; kept only the second block. Issue ONE block per range \u2014 payload is the final desired content, never both old and new.";
27
- /** Error text prefix emitted when an anchor line carries inline payload. */
28
- export declare const INLINE_PAYLOAD_REJECTED_PREFIX = "Inline payload on the anchor line is rejected.";
29
- /** Error text emitted when `|` replacement payload targets BOF/EOF. */
30
- export declare const VIRTUAL_REPLACE_REJECTED_MESSAGE = "BOF:/EOF: anchors are virtual positions and cannot use `|` replacement payload. Use `\u2191` or `\u2193` payload lines.";
23
+ export declare const REPLACE_PAIR_COALESCED_WARNING = "Detected two identical-range hashline hunks; kept only the second hunk. Issue ONE hunk per range \u2014 payload is the final desired content, never both old and new.";
24
+ /**
25
+ * Warning text appended when a bare hunk header (`A B` with no body)
26
+ * is followed by an overlapping concrete hunk. The earlier bare hunk is
27
+ * dropped on the assumption that the model expressed an old/new pair across
28
+ * two hunks; only the second hunk's payload is applied.
29
+ */
30
+ export declare const REPLACE_PAIR_COALESCED_OVERLAP_WARNING = "Detected an overlapping bare hashline hunk immediately followed by a concrete hunk; dropped the earlier bare hunk. Issue ONE hunk per range \u2014 payload is the final desired content, never both old and new.";
31
+ /**
32
+ * Warning text appended when bare body rows (no `+` / `&` prefix) follow a
33
+ * hunk header and the parser auto-converts them to `+literal` rows because
34
+ * no `+`/`&` row was present in the hunk. Helps the model learn the
35
+ * canonical body-row syntax while keeping the patch applying.
36
+ */
37
+ export declare const BARE_BODY_AUTO_PIPED_WARNING = "Auto-prefixed bare body row(s) with `+`. Always start payload rows with `+TEXT` (literal) or `&A..B` (repeat) \u2014 pasting raw code as payload is not a portable shape.";
38
+ /**
39
+ * Warning text emitted when a body row begins with `+&A..B` — the model
40
+ * mistakenly prefixed a repeat row with the `+` literal sigil. We reroute
41
+ * the row as a `&A..B` repeat so the patch still applies, then surface this
42
+ * warning so the model sees the mistake on the next turn.
43
+ */
44
+ export declare const PLUS_PREFIXED_REPEAT_WARNING = "A body row started with `+&A..B`. `+` (literal text) and `&A..B` (repeat) are sibling row kinds \u2014 a row uses exactly one of them. Treated as `&A..B`; remove the leading `+` next time.";
45
+ /**
46
+ * Warning text emitted when a hunk body contains unified-diff-style rows
47
+ * (`-old`, ` context`) and the parser silently converts them: `-` rows are
48
+ * dropped (the hunk header's range already deletes those lines), and the
49
+ * leading metadata-space on context rows is stripped once unified-diff
50
+ * mode is detected. Bare body rows are auto-prefixed with `+` regardless.
51
+ */
52
+ export declare const UNIFIED_DIFF_BODY_AUTO_CONVERT_WARNING = "Hunk body contained unified-diff-style rows (`-old`, ` context`). The `-` rows were dropped (the hunk header's range already deletes those lines); context rows were treated as `+TEXT` literals. Use `+TEXT` (literal) or `&A..B` (repeat) directly next time.";
31
53
  /** Warning text emitted by `Recovery` when an external write fits a cached snapshot. */
32
54
  export declare const RECOVERY_EXTERNAL_WARNING = "Recovered from a stale file hash using a previous read snapshot (file changed externally between read and edit).";
33
55
  /** Warning text emitted by `Recovery` when a prior in-session edit advanced the hash. */
34
56
  export declare const RECOVERY_SESSION_CHAIN_WARNING = "Recovered from a stale file hash using an earlier in-session snapshot (the file hash advanced after a prior edit in this session).";
35
- /** Warning text emitted by `Recovery` when the session-chain fast-path was taken. */
57
+ /**
58
+ * Warning text emitted by `Recovery` when the session-chain replay
59
+ * fast-path was taken. Distinct from {@link RECOVERY_SESSION_CHAIN_WARNING}
60
+ * because replay is the less-certain mode: the structured-patch 3-way
61
+ * merge refused, the anchor-content gate passed, but a coincidental
62
+ * insert+delete pair earlier in the chain could still leave an anchor's
63
+ * line number pointing at a duplicated row. Surface the hedge so the
64
+ * model verifies before continuing.
65
+ */
36
66
  export declare const RECOVERY_SESSION_REPLAY_WARNING = "Recovered by replaying your edits onto the current file content \u2014 your previous edit in this session changed line(s) you re-targeted with a stale hash. Verify the diff matches your intent before continuing.";
@@ -12,7 +12,7 @@ export interface MismatchDetails {
12
12
  anchorLines?: readonly number[];
13
13
  }
14
14
  /**
15
- * Raised when a hashline section's file hash doesn't match the live file's
15
+ * Raised when a hashline section's snapshot tag doesn't match the live file's
16
16
  * content (and recovery, if configured, declined the merge). Carries the
17
17
  * file lines plus anchored lines so renderers can produce a richer
18
18
  * diagnostic via {@link MismatchError.displayMessage}.
@@ -4,10 +4,10 @@ import type { Edit } from "./types";
4
4
  * Token-driven state machine that turns a stream of {@link Token}s into a
5
5
  * flat list of {@link Edit}s.
6
6
  *
7
- * `feed()` accepts tokens one at a time; block payload rows accumulate until
8
- * the next anchor block or {@link end} flushes them. After `terminated` flips
9
- * true (on `envelope-end` or `abort`) subsequent feeds are silently ignored
10
- * so callers can keep draining their tokenizer.
7
+ * `feed()` accepts tokens one at a time; hunk body rows accumulate until
8
+ * the next hunk header or {@link end} flushes them. After `terminated`
9
+ * flips true (on `envelope-end` or `abort`) subsequent feeds are silently
10
+ * ignored so callers can keep draining their tokenizer.
11
11
  */
12
12
  export declare class Executor {
13
13
  #private;
@@ -20,13 +20,13 @@ export declare class Executor {
20
20
  */
21
21
  feed(token: Token): void;
22
22
  /**
23
- * Flush any open pending block and return the accumulated edits and
24
- * warnings. The executor is single-use; {@link reset} is required for reuse.
23
+ * Flush any open pending hunk and return the accumulated edits and
24
+ * warnings. The executor is single-use; {@link reset} is required for
25
+ * reuse.
25
26
  *
26
- * Throws if two replacement/delete blocks target the same line with
27
- * non-identical ranges. Identical-range blocks in the same hunk are
28
- * coalesced last-wins by `feed()` with a warning, so they never reach the
29
- * validator.
27
+ * Throws if two hunks target the same line with non-identical ranges.
28
+ * Identical-range hunks in the same patch are coalesced last-wins by
29
+ * `feed()` with a warning, so they never reach the validator.
30
30
  */
31
31
  end(): {
32
32
  edits: Edit[];
@@ -34,9 +34,9 @@ export declare class Executor {
34
34
  };
35
35
  /**
36
36
  * Streaming-tolerant variant of {@link end}. Identical, except a pending
37
- * block whose payload has not yet accumulated any rows is treated as still
38
- * in flight and dropped instead of flushed (which would otherwise preview a
39
- * destructive bare delete while the model may still be typing payload).
37
+ * hunk whose body has not yet accumulated any rows is treated as still
38
+ * in flight and dropped instead of flushed (which would otherwise commit
39
+ * a destructive delete while the model may still be typing payload).
40
40
  */
41
41
  endStreaming(): {
42
42
  edits: Edit[];
@@ -61,14 +61,14 @@ export declare function parsePatch(diff: string): {
61
61
  * parsed successfully when the diff is still being typed:
62
62
  *
63
63
  * - per-token feed errors stop the drain but preserve the edits already
64
- * collected (the trailing block is malformed mid-stream — wait for the next
65
- * chunk),
66
- * - the trailing pending block is dropped if it has no payload yet (avoids a
67
- * destructive bare-delete preview while payload may still be coming).
64
+ * collected (the trailing hunk is malformed mid-stream — wait for the
65
+ * next chunk),
66
+ * - the trailing pending hunk is dropped if it has no payload yet (avoids
67
+ * a destructive bare-delete preview while payload may still be coming).
68
68
  *
69
- * Throws only on the cross-block overlap validator, which catches conflicting
70
- * shapes (two replacements/deletes hitting the same anchor). Streaming preview
71
- * callers should treat any throw here as "no preview this tick".
69
+ * Throws only on the cross-hunk overlap validator, which catches conflicting
70
+ * shapes (two hunks hitting the same anchor). Streaming preview callers
71
+ * should treat any throw here as "no preview this tick".
72
72
  */
73
73
  export declare function parsePatchStreaming(diff: string): {
74
74
  edits: Edit[];
@@ -3,21 +3,12 @@ import type { Patch, PatchSection } from "./input";
3
3
  import { type LineEnding } from "./normalize";
4
4
  import { Recovery } from "./recovery";
5
5
  import type { SnapshotStore } from "./snapshots";
6
- import type { ApplyOptions, ApplyResult } from "./types";
6
+ import type { ApplyResult } from "./types";
7
7
  export interface PatcherOptions {
8
8
  /** Storage backend used for all reads and writes. */
9
9
  fs: Filesystem;
10
- /**
11
- * Optional snapshot store that enables stale-hash recovery. When set, a
12
- * section with a stale hash tries a 3-way merge against a cached
13
- * snapshot before the apply fails with {@link MismatchError}.
14
- */
15
- snapshots?: SnapshotStore;
16
- /**
17
- * Optional default {@link ApplyOptions} forwarded to every section.
18
- * Per-call overrides win on a key-by-key basis.
19
- */
20
- applyOptions?: ApplyOptions;
10
+ /** Snapshot store that minted and resolves hashline section tags. Required. */
11
+ snapshots: SnapshotStore;
21
12
  }
22
13
  /** Per-section result returned by {@link Patcher.apply} / {@link Patcher.commit}. */
23
14
  export interface PatchSectionResult {
@@ -35,9 +26,9 @@ export interface PatchSectionResult {
35
26
  persisted: string;
36
27
  /** Final text that the {@link Filesystem} actually wrote (may differ if the FS transformed it). */
37
28
  written: string;
38
- /** 4-hex hash of `after`. Use to anchor follow-up edits. */
29
+ /** 3-hex opaque snapshot tag for `after`. Use to anchor follow-up edits. */
39
30
  fileHash: string;
40
- /** Hashline section header (`¶path#hash`) of the post-edit content. */
31
+ /** Hashline section header (`¶path#tag`) of the post-edit content. */
41
32
  header: string;
42
33
  /** 1-indexed first changed line in `after`, or `undefined` for noops. */
43
34
  firstChangedLine?: number;
@@ -68,7 +59,7 @@ export declare class PreparedSection {
68
59
  get isNoop(): boolean;
69
60
  }
70
61
  /**
71
- * High-level patcher. Wires a {@link Filesystem} and an optional
62
+ * High-level patcher. Wires a {@link Filesystem} and a required
72
63
  * {@link SnapshotStore} together with the parsing + applying core.
73
64
  *
74
65
  * Construct once per FS configuration; reuse across patches.
@@ -76,9 +67,8 @@ export declare class PreparedSection {
76
67
  export declare class Patcher {
77
68
  #private;
78
69
  readonly fs: Filesystem;
79
- readonly snapshots: SnapshotStore | undefined;
80
- readonly recovery: Recovery | undefined;
81
- readonly applyOptions: ApplyOptions;
70
+ readonly snapshots: SnapshotStore;
71
+ readonly recovery: Recovery;
82
72
  constructor(options: PatcherOptions);
83
73
  /**
84
74
  * Apply every section in `patch`. `prepare` runs the full apply for each
@@ -86,27 +76,27 @@ export declare class Patcher {
86
76
  * multi-section batch is naturally all-or-nothing. Returns one
87
77
  * {@link PatchSectionResult} per section in the original patch order.
88
78
  */
89
- apply(patch: Patch, options?: ApplyOptions): Promise<PatcherApplyResult>;
79
+ apply(patch: Patch): Promise<PatcherApplyResult>;
90
80
  /**
91
81
  * Run the preflight pass only: read, parse, validate, apply-in-memory.
92
82
  * No writes hit the filesystem. Use for CI checks and dry runs.
93
83
  */
94
- preflight(patch: Patch, options?: ApplyOptions): Promise<void>;
84
+ preflight(patch: Patch): Promise<void>;
95
85
  /**
96
- * Read a section's target file, parse the section, validate the file
97
- * hash (with recovery), and apply the edits in memory. Returns a
86
+ * Read a section's target file, parse the section, validate the snapshot
87
+ * tag (with recovery), and apply the edits in memory. Returns a
98
88
  * {@link PreparedSection} which can be fed to {@link commit} to land
99
89
  * the result on the filesystem.
100
90
  *
101
91
  * Throws on parse error, missing-file-for-anchored-edit, or unrecovered
102
- * hash mismatch ({@link MismatchError}).
92
+ * tag mismatch ({@link MismatchError}).
103
93
  */
104
- prepare(section: PatchSection, options?: ApplyOptions): Promise<PreparedSection>;
94
+ prepare(section: PatchSection): Promise<PreparedSection>;
105
95
  /**
106
96
  * Commit a previously {@link prepare}d section to the filesystem.
107
97
  * Restores line endings and BOM, writes via the {@link Filesystem}, and
108
- * records a fresh snapshot in the {@link SnapshotStore} (when
109
- * configured) keyed by the filesystem-canonical path.
98
+ * records a fresh snapshot in the {@link SnapshotStore} keyed by the
99
+ * filesystem-canonical path.
110
100
  */
111
101
  commit(prepared: PreparedSection): Promise<PatchSectionResult>;
112
102
  }
@@ -1,11 +1,10 @@
1
1
  import type { SnapshotStore } from "./snapshots";
2
- import type { ApplyOptions, Edit } from "./types";
2
+ import type { Edit } from "./types";
3
3
  export interface RecoveryArgs {
4
4
  path: string;
5
5
  currentText: string;
6
6
  fileHash: string;
7
7
  edits: readonly Edit[];
8
- options?: ApplyOptions;
9
8
  }
10
9
  export interface RecoveryResult {
11
10
  /** Post-recovery text. */
@@ -22,10 +21,15 @@ export interface RecoveryResult {
22
21
  *
23
22
  * 1. Apply on the cached `fullText` snapshot, then 3-way-merge onto current.
24
23
  * 2. (Session chain) If the snapshot wasn't the head, retry on current text
25
- * when line counts match the user's previous edit advanced the hash but
26
- * didn't shift line numbers.
27
- * 3. Reconstruct from a sparse snapshot (lines map only), verify the rebuilt
28
- * text hashes to the expected value, then 3-way-merge.
24
+ * when line counts match AND every edit's anchor line content is unchanged
25
+ * between snapshot and current — the previous in-session edit advanced
26
+ * the hash and the model's anchors still name the same logical rows. Emits
27
+ * a dedicated {@link RECOVERY_SESSION_REPLAY_WARNING} because even with
28
+ * both guards a coincidental insert+delete pair on duplicate rows can
29
+ * still land the edit on the wrong row; see {@link replaySessionChainOnCurrent}.
30
+ * 3. Reconstruct from a sparse snapshot (lines map only), then 3-way-merge.
31
+ * Sparse snapshots that still match the live file are direct-apply cases
32
+ * owned by the patcher, so recovery declines them.
29
33
  */
30
34
  export declare class Recovery {
31
35
  readonly store: SnapshotStore;