@oh-my-pi/hashline 15.5.12 → 15.5.13
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/dist/types/format.d.ts +37 -23
- package/dist/types/input.d.ts +3 -3
- package/dist/types/messages.d.ts +14 -34
- package/dist/types/parser.d.ts +0 -53
- package/dist/types/recovery.d.ts +11 -13
- package/dist/types/snapshots.d.ts +36 -114
- package/dist/types/tokenizer.d.ts +10 -53
- package/dist/types/types.d.ts +7 -11
- package/package.json +3 -2
- package/src/apply.ts +334 -53
- package/src/format.ts +64 -28
- package/src/grammar.lark +10 -10
- package/src/input.ts +10 -13
- package/src/messages.ts +17 -36
- package/src/mismatch.ts +3 -4
- package/src/parser.ts +71 -329
- package/src/patcher.ts +21 -43
- package/src/prompt.md +43 -44
- package/src/recovery.ts +22 -72
- package/src/snapshots.ts +84 -390
- package/src/tokenizer.ts +102 -155
- package/src/types.ts +9 -13
package/dist/types/format.d.ts
CHANGED
|
@@ -3,49 +3,63 @@
|
|
|
3
3
|
* display helpers. These are the single source of truth for the parser, the
|
|
4
4
|
* tokenizer, the prompt, and the formal grammar.
|
|
5
5
|
*/
|
|
6
|
+
import type { Cursor } from "./types";
|
|
6
7
|
/** File-section header prefix: `¶path#hash`. */
|
|
7
8
|
export declare const HL_FILE_PREFIX = "\u00B6";
|
|
8
9
|
/** Payload sigil for literal body rows. */
|
|
9
10
|
export declare const HL_PAYLOAD_REPLACE = "+";
|
|
10
|
-
/**
|
|
11
|
-
export declare const
|
|
12
|
-
/**
|
|
13
|
-
export declare const
|
|
11
|
+
/** Hunk-header keyword for concrete line replacement. */
|
|
12
|
+
export declare const HL_REPLACE_KEYWORD = "replace";
|
|
13
|
+
/** Hunk-header keyword for concrete line deletion. */
|
|
14
|
+
export declare const HL_DELETE_KEYWORD = "delete";
|
|
15
|
+
/** Hunk-header keyword for insertion operations. */
|
|
16
|
+
export declare const HL_INSERT_KEYWORD = "insert";
|
|
17
|
+
/** Insert position keyword for inserting before a concrete line. */
|
|
18
|
+
export declare const HL_INSERT_BEFORE = "before";
|
|
19
|
+
/** Insert position keyword for inserting after a concrete line. */
|
|
20
|
+
export declare const HL_INSERT_AFTER = "after";
|
|
21
|
+
/** Insert position keyword for inserting at the start of the file. */
|
|
22
|
+
export declare const HL_INSERT_HEAD = "head";
|
|
23
|
+
/** Insert position keyword for inserting at the end of the file. */
|
|
24
|
+
export declare const HL_INSERT_TAIL = "tail";
|
|
25
|
+
/** Hunk-header terminator for body-bearing operations. */
|
|
26
|
+
export declare const HL_HEADER_COLON = ":";
|
|
14
27
|
/** Separator between a hashline file path and its opaque snapshot tag. */
|
|
15
28
|
export declare const HL_FILE_HASH_SEP = "#";
|
|
16
29
|
/** Separator between two line numbers in a range, e.g. `5..10`. */
|
|
17
30
|
export declare const HL_RANGE_SEP = "..";
|
|
18
31
|
/** Separator between a line number and displayed line content in hashline mode. */
|
|
19
32
|
export declare const HL_LINE_BODY_SEP = ":";
|
|
20
|
-
/**
|
|
21
|
-
* Decoration prefix that may precede a line number in tool output:
|
|
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.
|
|
26
|
-
*/
|
|
27
|
-
export declare const HL_ANCHOR_DECORATION_RE_RAW = "\\s*[>*]*\\s*";
|
|
28
|
-
/** Capture-group regex source for a decorated bare line-number anchor. */
|
|
29
|
-
export declare const HL_ANCHOR_RE_RAW = "\\s*[>*]*\\s*(\\d+)";
|
|
30
33
|
/** Bare positive line-number Lid (no decorations, no captures, no anchors). */
|
|
31
34
|
export declare const HL_LINE_RE_RAW = "[1-9]\\d*";
|
|
32
35
|
/** Capture-group form of {@link HL_LINE_RE_RAW}. */
|
|
33
36
|
export declare const HL_LINE_CAPTURE_RE_RAW = "([1-9]\\d*)";
|
|
34
|
-
/**
|
|
35
|
-
export declare
|
|
36
|
-
/**
|
|
37
|
-
export declare
|
|
38
|
-
/**
|
|
39
|
-
export declare
|
|
37
|
+
/** Format a concrete replacement hunk header. */
|
|
38
|
+
export declare function formatReplaceHeader(start: number, end: number): string;
|
|
39
|
+
/** Format a concrete deletion hunk header. */
|
|
40
|
+
export declare function formatDeleteHeader(start: number, end?: number): string;
|
|
41
|
+
/** Format an insertion hunk header for a cursor position. */
|
|
42
|
+
export declare function formatInsertHeader(cursor: Cursor): string;
|
|
43
|
+
/** Number of hex characters in a content-derived file-hash tag. */
|
|
44
|
+
export declare const HL_FILE_HASH_LENGTH = 4;
|
|
45
|
+
/** Canonical uppercase hexadecimal content-hash tag carried by a hashline section header. */
|
|
46
|
+
export declare const HL_FILE_HASH_RE_RAW = "[0-9A-F]{4}";
|
|
40
47
|
/** Capture-group form of {@link HL_FILE_HASH_RE_RAW}. */
|
|
41
|
-
export declare const HL_FILE_HASH_CAPTURE_RE_RAW = "([0-9A-F]{
|
|
48
|
+
export declare const HL_FILE_HASH_CAPTURE_RE_RAW = "([0-9A-F]{4})";
|
|
42
49
|
/** Regex-escaped form of {@link HL_LINE_BODY_SEP}, safe for embedding inside a regex. */
|
|
43
50
|
export declare const HL_LINE_BODY_SEP_RE_RAW: string;
|
|
44
51
|
/**
|
|
45
|
-
* Representative
|
|
52
|
+
* Representative file-hash tags for use in user-facing error messages and
|
|
46
53
|
* prompt examples.
|
|
47
54
|
*/
|
|
48
|
-
export declare const HL_FILE_HASH_EXAMPLES: readonly ["
|
|
55
|
+
export declare const HL_FILE_HASH_EXAMPLES: readonly ["1A2B", "3C4D", "9F3E"];
|
|
56
|
+
/**
|
|
57
|
+
* Compute the content-derived hash tag carried by a hashline section header.
|
|
58
|
+
* The tag is a 4-hex fingerprint of the whole file's normalized text: any read
|
|
59
|
+
* of byte-identical content mints the same tag, and a follow-up edit anchored
|
|
60
|
+
* at any line validates whenever the live file still hashes to it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function computeFileHash(text: string): string;
|
|
49
63
|
/**
|
|
50
64
|
* Format a comma-separated list of example anchors with an optional line-number
|
|
51
65
|
* prefix, quoted for inclusion in error messages: `"160", "42", "7"`.
|
package/dist/types/input.d.ts
CHANGED
|
@@ -36,9 +36,9 @@ 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 concrete file content. Pure
|
|
40
|
-
* literal inserts do not count: those are
|
|
41
|
-
* yet exist.
|
|
39
|
+
* True when at least one edit anchors to concrete file content. Pure
|
|
40
|
+
* `insert head:` / `insert tail:` literal inserts do not count: those are
|
|
41
|
+
* safe to apply to files that don't yet exist.
|
|
42
42
|
*/
|
|
43
43
|
get hasAnchorScopedEdit(): boolean;
|
|
44
44
|
/** Anchor lines touched by this section, sorted ascending and deduplicated. */
|
package/dist/types/messages.d.ts
CHANGED
|
@@ -16,40 +16,20 @@ export declare const END_PATCH_MARKER = "*** End Patch";
|
|
|
16
16
|
* parsing — terminates the line loop — and does not surface a warning.
|
|
17
17
|
*/
|
|
18
18
|
export declare const ABORT_MARKER = "*** Abort";
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*/
|
|
30
|
-
export declare const
|
|
31
|
-
/**
|
|
32
|
-
|
|
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.";
|
|
19
|
+
/** Warning text appended when two consecutive hunks target the exact same concrete range. */
|
|
20
|
+
export declare const REPLACE_PAIR_COALESCED_WARNING = "Detected two identical-range hashline hunks; kept only the second hunk. Issue ONE `replace N..M:` hunk per range \u2014 payload is the final desired content, never both old and new.";
|
|
21
|
+
/** Warning text appended when an empty bodyless hunk is followed by an overlapping concrete hunk. */
|
|
22
|
+
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 `replace N..M:` hunk per range \u2014 payload is the final desired content, never both old and new.";
|
|
23
|
+
/** Warning text appended when bare body rows are auto-converted to literal rows. */
|
|
24
|
+
export declare const BARE_BODY_AUTO_PIPED_WARNING = "Auto-prefixed bare body row(s) with `+`. Body rows must be `+TEXT` literal lines; pasting raw code as payload is not a portable shape.";
|
|
25
|
+
/** Error text emitted when a hunk body contains a unified-diff-style `-` row. */
|
|
26
|
+
export declare const MINUS_ROW_REJECTED = "`-` rows are not valid; hashline ranges already name the lines being changed. To insert a literal line starting with `-`, write `+-\u2026`.";
|
|
27
|
+
/** Error text emitted when a replace hunk has no body. */
|
|
28
|
+
export declare const EMPTY_REPLACE = "`replace N..M:` needs at least one `+TEXT` body row. To delete lines, use `delete N..M`.";
|
|
29
|
+
/** Error text emitted when a delete hunk receives a body row. */
|
|
30
|
+
export declare const DELETE_TAKES_NO_BODY = "`delete N..M` does not take body rows. Remove the body, or use `replace N..M:`.";
|
|
31
|
+
/** Error text emitted when an insert hunk has no body. */
|
|
32
|
+
export declare const EMPTY_INSERT = "`insert` needs at least one `+TEXT` body row.";
|
|
53
33
|
/** Warning text emitted by `Recovery` when an external write fits a cached snapshot. */
|
|
54
34
|
export declare const RECOVERY_EXTERNAL_WARNING = "Recovered from a stale file hash using a previous read snapshot (file changed externally between read and edit).";
|
|
55
35
|
/** Warning text emitted by `Recovery` when a prior in-session edit advanced the hash. */
|
package/dist/types/parser.d.ts
CHANGED
|
@@ -1,75 +1,22 @@
|
|
|
1
1
|
import { type Token } from "./tokenizer";
|
|
2
2
|
import type { Edit } from "./types";
|
|
3
|
-
/**
|
|
4
|
-
* Token-driven state machine that turns a stream of {@link Token}s into a
|
|
5
|
-
* flat list of {@link Edit}s.
|
|
6
|
-
*
|
|
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
|
-
*/
|
|
12
3
|
export declare class Executor {
|
|
13
4
|
#private;
|
|
14
|
-
/** True once an `envelope-end` or `abort` token has been observed. */
|
|
15
|
-
get terminated(): boolean;
|
|
16
|
-
/**
|
|
17
|
-
* Consume one token. After `terminated` flips true subsequent feeds are
|
|
18
|
-
* silently ignored so callers can keep draining the tokenizer without
|
|
19
|
-
* explicit early-exit guards.
|
|
20
|
-
*/
|
|
21
5
|
feed(token: Token): void;
|
|
22
|
-
/**
|
|
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.
|
|
26
|
-
*
|
|
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
|
-
*/
|
|
31
6
|
end(): {
|
|
32
7
|
edits: Edit[];
|
|
33
8
|
warnings: string[];
|
|
34
9
|
};
|
|
35
|
-
/**
|
|
36
|
-
* Streaming-tolerant variant of {@link end}. Identical, except a pending
|
|
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
|
-
*/
|
|
41
10
|
endStreaming(): {
|
|
42
11
|
edits: Edit[];
|
|
43
12
|
warnings: string[];
|
|
44
13
|
};
|
|
45
|
-
/** Reset to a fresh state so the same instance can drive another parse. */
|
|
46
14
|
reset(): void;
|
|
47
15
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Drive a full hashline diff through the tokenizer + executor pipeline and
|
|
50
|
-
* return the resulting edits plus any parse-time warnings. This is the
|
|
51
|
-
* convenience entry point most callers want; reach for {@link Tokenizer} /
|
|
52
|
-
* {@link Executor} directly only when you need streaming feeds, cross-section
|
|
53
|
-
* state, or custom token handling.
|
|
54
|
-
*/
|
|
55
16
|
export declare function parsePatch(diff: string): {
|
|
56
17
|
edits: Edit[];
|
|
57
18
|
warnings: string[];
|
|
58
19
|
};
|
|
59
|
-
/**
|
|
60
|
-
* Streaming-tolerant variant of {@link parsePatch}. Returns whatever edits
|
|
61
|
-
* parsed successfully when the diff is still being typed:
|
|
62
|
-
*
|
|
63
|
-
* - per-token feed errors stop the drain but preserve the edits already
|
|
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
|
-
*
|
|
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
|
-
*/
|
|
73
20
|
export declare function parsePatchStreaming(diff: string): {
|
|
74
21
|
edits: Edit[];
|
|
75
22
|
warnings: string[];
|
package/dist/types/recovery.d.ts
CHANGED
|
@@ -16,20 +16,18 @@ export interface RecoveryResult {
|
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Stateless recovery driver over a {@link SnapshotStore}. Construct once and
|
|
19
|
-
* call {@link Recovery.tryRecover} per stale-
|
|
20
|
-
* implementation tries
|
|
19
|
+
* call {@link Recovery.tryRecover} per stale-tag incident. The default
|
|
20
|
+
* implementation tries two strategies in order:
|
|
21
21
|
*
|
|
22
|
-
* 1. Apply on the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* Sparse snapshots that still match the live file are direct-apply cases
|
|
32
|
-
* owned by the patcher, so recovery declines them.
|
|
22
|
+
* 1. Apply the edits on the full-file version the tag names, then 3-way-merge
|
|
23
|
+
* the resulting patch onto the live content (handles external writes).
|
|
24
|
+
* 2. (Session chain) If that version wasn't the head, replay the edits onto
|
|
25
|
+
* the live content directly when line counts match AND every edit's anchor
|
|
26
|
+
* line content is unchanged between version and current — a prior in-session
|
|
27
|
+
* edit advanced the tag and the model's anchors still name the same logical
|
|
28
|
+
* rows. Emits a dedicated {@link RECOVERY_SESSION_REPLAY_WARNING} because
|
|
29
|
+
* even with both guards a coincidental insert+delete pair on duplicate rows
|
|
30
|
+
* can still land the edit on the wrong row; see {@link replaySessionChainOnCurrent}.
|
|
33
31
|
*/
|
|
34
32
|
export declare class Recovery {
|
|
35
33
|
readonly store: SnapshotStore;
|
|
@@ -1,133 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Producers (typically `read` / `search` tools) record the lines they showed
|
|
6
|
-
* the model. The store returns a three-hex opaque tag. Consumers resolve that
|
|
7
|
-
* tag back to the recorded snapshot and verify the recorded lines against the
|
|
8
|
-
* live file before applying anchored edits.
|
|
9
|
-
*
|
|
10
|
-
* Tags are scrambled by a deterministic permutation of the 12-bit hex space
|
|
11
|
-
* built once at module load. Slot index `i` always maps to the same tag
|
|
12
|
-
* across restarts (good for tests and reproducibility), but consecutive slot
|
|
13
|
-
* indices produce unrelated tags — `mint()` followed by another `mint()` does
|
|
14
|
-
* not return e.g. `000` then `001`, and the first tag a store ever hands out
|
|
15
|
-
* is not `000`. This is hallucination prevention, not adversarial security: it
|
|
16
|
-
* stops an LLM from guessing `001` after observing `000`, or assuming any
|
|
17
|
-
* monotonic counter pattern. The patcher catches stale-tag misuse via
|
|
18
|
-
* content verification at apply time, so determinism doesn't reduce safety.
|
|
19
|
-
*
|
|
20
|
-
* Tag → slot resolution is a single `Map` lookup (no `parseInt`, no regex):
|
|
21
|
-
* the inverse table is populated with both lowercase and uppercase forms of
|
|
22
|
-
* every tag at module load.
|
|
23
|
-
*
|
|
24
|
-
* Snapshots are an open abstract type. The two concrete impls cover the
|
|
25
|
-
* shapes producers actually emit: {@link ContiguousSnapshot} for `read`-style
|
|
26
|
-
* runs (no map allocation, range arithmetic for lookup and superset checks)
|
|
27
|
-
* and {@link SparseSnapshot} for `search`-style hits.
|
|
28
|
-
*
|
|
29
|
-
* The abstract base class lets callers plug in whatever storage they like.
|
|
30
|
-
* {@link InMemorySnapshotStore} ships as a single 4096-slot ring shared across
|
|
31
|
-
* paths — snapshots carry their own path, so the global ring is fine and
|
|
32
|
-
* `byHash` rejects cross-path lookups.
|
|
2
|
+
* One full-file version observed at a point in time. The tag the model sees is
|
|
3
|
+
* {@link Snapshot.hash}; recovery replays edits against {@link Snapshot.text}.
|
|
33
4
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* primitive methods subclasses must supply — `get` and `entries` — give callers
|
|
37
|
-
* (and the default `isSuperset` / `matchesLiveFile` impls) everything they
|
|
38
|
-
* need to verify recorded content against the live file.
|
|
39
|
-
*/
|
|
40
|
-
export declare abstract class Snapshot {
|
|
41
|
-
/** Canonical path this snapshot belongs to. */
|
|
42
|
-
abstract readonly path: string;
|
|
43
|
-
/** Timestamp (ms since epoch) the snapshot was recorded. */
|
|
44
|
-
abstract readonly recordedAt: number;
|
|
45
|
-
/** Full normalized text when the read observed the whole file. */
|
|
46
|
-
abstract readonly fullText?: string;
|
|
47
|
-
/** Recorded content for 1-indexed `lineNumber`, or `undefined` if this snapshot doesn't cover that line. */
|
|
48
|
-
abstract get(lineNumber: number): string | undefined;
|
|
49
|
-
/** Iterate (1-indexed lineNumber, content) pairs in stable order. */
|
|
50
|
-
abstract entries(): Iterable<[number, string]>;
|
|
51
|
-
/** True iff every (line, content) the `other` snapshot asserts is also present here with matching content. */
|
|
52
|
-
isSuperset(other: Snapshot): boolean;
|
|
53
|
-
/** True iff every recorded line matches `currentLines` (0-indexed array of live file lines). */
|
|
54
|
-
matchesLiveFile(currentLines: readonly string[]): boolean;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* A contiguous run of lines starting at `offset` (1-indexed). Backed by a
|
|
58
|
-
* plain `string[]`; lookup is `lines[n - offset]`, superset against another
|
|
59
|
-
* contiguous snapshot is pure range arithmetic.
|
|
60
|
-
*/
|
|
61
|
-
export declare class ContiguousSnapshot extends Snapshot {
|
|
62
|
-
readonly path: string;
|
|
63
|
-
readonly offset: number;
|
|
64
|
-
readonly lines: readonly string[];
|
|
65
|
-
readonly fullText?: string | undefined;
|
|
66
|
-
readonly recordedAt: number;
|
|
67
|
-
constructor(path: string, offset: number, lines: readonly string[], fullText?: string | undefined, recordedAt?: number);
|
|
68
|
-
get(lineNumber: number): string | undefined;
|
|
69
|
-
entries(): IterableIterator<[number, string]>;
|
|
70
|
-
isSuperset(other: Snapshot): boolean;
|
|
71
|
-
matchesLiveFile(currentLines: readonly string[]): boolean;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* A sparse `(lineNumber → content)` map, used for snapshots that don't cover a
|
|
75
|
-
* single contiguous run — e.g. search hits plus their context windows.
|
|
76
|
-
*/
|
|
77
|
-
export declare class SparseSnapshot extends Snapshot {
|
|
5
|
+
export interface Snapshot {
|
|
6
|
+
/** Canonical path this version belongs to. */
|
|
78
7
|
readonly path: string;
|
|
79
|
-
|
|
80
|
-
readonly
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
/** Optional metadata supplied at snapshot record time. */
|
|
87
|
-
export interface SnapshotMetadata {
|
|
88
|
-
/** Full normalized text, when the producer observed the whole file. */
|
|
89
|
-
fullText?: string;
|
|
8
|
+
/** Full normalized (LF, no BOM) file text as observed. */
|
|
9
|
+
readonly text: string;
|
|
10
|
+
/** Content-derived tag for {@link Snapshot.text} (see {@link computeFileHash}). */
|
|
11
|
+
readonly hash: string;
|
|
12
|
+
/** Timestamp (ms since epoch) the version was recorded. */
|
|
13
|
+
recordedAt: number;
|
|
90
14
|
}
|
|
91
15
|
/**
|
|
92
|
-
* Storage seam for file
|
|
93
|
-
*
|
|
16
|
+
* Storage seam for full-file version snapshots. The patcher calls {@link head}
|
|
17
|
+
* for the latest version of a path and {@link byHash} when it needs the
|
|
18
|
+
* specific historical version a section's stale tag names.
|
|
94
19
|
*/
|
|
95
20
|
export declare abstract class SnapshotStore {
|
|
96
|
-
/** Most-recently
|
|
21
|
+
/** Most-recently recorded version for `path`, or `null` if none. */
|
|
97
22
|
abstract head(path: string): Snapshot | null;
|
|
98
|
-
/**
|
|
99
|
-
abstract byHash(path: string,
|
|
100
|
-
/** Record
|
|
101
|
-
abstract
|
|
102
|
-
/**
|
|
103
|
-
abstract recordSparse(path: string, entries: Iterable<readonly [number, string]>, metadata?: SnapshotMetadata): string;
|
|
104
|
-
/** Drop snapshots belonging to a single path. */
|
|
23
|
+
/** Recorded version for `path` whose tag equals `hash`, or `null`. */
|
|
24
|
+
abstract byHash(path: string, hash: string): Snapshot | null;
|
|
25
|
+
/** Record the full normalized text of `path` and return its content tag. */
|
|
26
|
+
abstract record(path: string, fullText: string): string;
|
|
27
|
+
/** Drop the version history for a single path. */
|
|
105
28
|
abstract invalidate(path: string): void;
|
|
106
|
-
/** Drop every
|
|
29
|
+
/** Drop every version history. */
|
|
107
30
|
abstract clear(): void;
|
|
108
31
|
}
|
|
32
|
+
export interface InMemorySnapshotStoreOptions {
|
|
33
|
+
/** Maximum number of distinct paths tracked at once (default 30). LRU eviction. */
|
|
34
|
+
maxPaths?: number;
|
|
35
|
+
/** Maximum full-file versions retained per path (default 4). Oldest dropped first. */
|
|
36
|
+
maxVersionsPerPath?: number;
|
|
37
|
+
}
|
|
109
38
|
/**
|
|
110
|
-
* In-memory {@link SnapshotStore} backed by
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* (coalesce). All reuse the original tag, so sequential reads of an unchanged
|
|
118
|
-
* file collapse onto one anchor instead of fragmenting. A disagreeing shared
|
|
119
|
-
* line means the file changed on disk, so a fresh slot (new tag) is minted.
|
|
120
|
-
* Slot reuse on wrap is intentional: stale tags may
|
|
121
|
-
* alias after 4096 distinct pushes, and the patcher catches misuse by verifying
|
|
122
|
-
* the resolved snapshot's content (and path) against the live file before
|
|
123
|
-
* applying edits.
|
|
39
|
+
* In-memory {@link SnapshotStore} backed by `lru-cache`. Per-path history is a
|
|
40
|
+
* short ring of full-file versions (oldest dropped first); per-session path
|
|
41
|
+
* tracking is LRU-bounded so cold paths age out automatically.
|
|
42
|
+
*
|
|
43
|
+
* Recording byte-identical content again refreshes recency and reuses the
|
|
44
|
+
* existing tag (read fusion); recording new content unshifts a fresh version
|
|
45
|
+
* onto the front of the path history.
|
|
124
46
|
*/
|
|
125
47
|
export declare class InMemorySnapshotStore extends SnapshotStore {
|
|
126
48
|
#private;
|
|
49
|
+
constructor(options?: InMemorySnapshotStoreOptions);
|
|
127
50
|
head(path: string): Snapshot | null;
|
|
128
|
-
byHash(path: string,
|
|
129
|
-
|
|
130
|
-
recordSparse(path: string, entries: Iterable<readonly [number, string]>, metadata?: SnapshotMetadata): string;
|
|
51
|
+
byHash(path: string, hash: string): Snapshot | null;
|
|
52
|
+
record(path: string, fullText: string): string;
|
|
131
53
|
invalidate(path: string): void;
|
|
132
54
|
clear(): void;
|
|
133
55
|
}
|
|
@@ -1,43 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stateful, line-oriented classifier for hashline diff text.
|
|
3
|
-
*
|
|
4
|
-
* The {@link Tokenizer} can be fed in chunks ({@link Tokenizer.feed}/{@link
|
|
5
|
-
* Tokenizer.end}) for streaming use, or in one shot ({@link
|
|
6
|
-
* Tokenizer.tokenizeAll}). Each emitted token carries its 1-indexed source
|
|
7
|
-
* line number so downstream consumers (parser, validators, error messages)
|
|
8
|
-
* can refer back to the input precisely.
|
|
9
|
-
*
|
|
10
|
-
* Format shape:
|
|
11
|
-
* ```
|
|
12
|
-
* *** path/to/file.ts#0A3
|
|
13
|
-
* @@ 5,7 @@
|
|
14
|
-
* +literal new line
|
|
15
|
-
* &3,4
|
|
16
|
-
* ```
|
|
17
|
-
* Each `***` line opens a new file section; each `@@ A,B @@` line opens a
|
|
18
|
-
* new hunk whose body (zero or more `+`/`&` rows) replaces the selected
|
|
19
|
-
* range. Empty body = delete the selected range.
|
|
20
|
-
*/
|
|
21
1
|
import type { Anchor, Cursor, ParsedRange } from "./types";
|
|
22
|
-
/**
|
|
23
|
-
* Split a hashline diff into individual lines without losing the trailing
|
|
24
|
-
* empty line that callers may rely on for explicit blank payloads. CRLF pairs
|
|
25
|
-
* are normalized to a single line break.
|
|
26
|
-
*/
|
|
27
2
|
export declare function splitHashlineLines(text: string): string[];
|
|
28
3
|
export declare function cloneCursor(cursor: Cursor): Cursor;
|
|
29
4
|
/** Parse a bare line-number anchor. Throws on malformed input. */
|
|
30
5
|
export declare function parseLid(raw: string, lineNum: number): Anchor;
|
|
31
6
|
export type BlockTarget = {
|
|
32
|
-
kind: "
|
|
7
|
+
kind: "replace";
|
|
33
8
|
range: ParsedRange;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "delete";
|
|
11
|
+
range: ParsedRange;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "insert_before";
|
|
14
|
+
anchor: Anchor;
|
|
15
|
+
} | {
|
|
16
|
+
kind: "insert_after";
|
|
17
|
+
anchor: Anchor;
|
|
34
18
|
} | {
|
|
35
19
|
kind: "bof";
|
|
36
20
|
} | {
|
|
37
21
|
kind: "eof";
|
|
38
22
|
};
|
|
39
23
|
interface TokenBase {
|
|
40
|
-
/** 1-indexed line number in the original input stream. */
|
|
41
24
|
lineNum: number;
|
|
42
25
|
}
|
|
43
26
|
export type Token = (TokenBase & {
|
|
@@ -58,42 +41,16 @@ export type Token = (TokenBase & {
|
|
|
58
41
|
}) | (TokenBase & {
|
|
59
42
|
kind: "payload-literal";
|
|
60
43
|
text: string;
|
|
61
|
-
}) | (TokenBase & {
|
|
62
|
-
kind: "payload-repeat";
|
|
63
|
-
range: ParsedRange;
|
|
64
44
|
}) | (TokenBase & {
|
|
65
45
|
kind: "raw";
|
|
66
46
|
text: string;
|
|
67
47
|
});
|
|
68
|
-
/**
|
|
69
|
-
* Stateful, line-oriented classifier for hashline diff text. Use the
|
|
70
|
-
* streaming {@link feed}/{@link end} pair to ingest text in chunks (each
|
|
71
|
-
* completed line emits exactly one token; a trailing partial line stays
|
|
72
|
-
* buffered until the next chunk or {@link end}). Use the stateless
|
|
73
|
-
* {@link tokenize}/predicate methods for callers that already hold whole
|
|
74
|
-
* lines and only need classification without buffering.
|
|
75
|
-
*/
|
|
76
48
|
export declare class Tokenizer {
|
|
77
49
|
#private;
|
|
78
|
-
/**
|
|
79
|
-
* Ingest a chunk of input text. Each newline-terminated line in the
|
|
80
|
-
* combined buffer produces one token. A trailing partial line (no `\n`
|
|
81
|
-
* yet, possibly ending in a lone `\r`) stays buffered until the next
|
|
82
|
-
* `feed`/`end` call so CRLF pairs that straddle chunk boundaries are
|
|
83
|
-
* still normalized correctly.
|
|
84
|
-
*/
|
|
85
50
|
feed(chunk: string): Token[];
|
|
86
|
-
/**
|
|
87
|
-
* Flush any buffered residual line (the last line of input when it lacks
|
|
88
|
-
* a trailing newline) and mark the tokenizer closed. Calling `end` a
|
|
89
|
-
* second time returns `[]`; reuse requires `reset`.
|
|
90
|
-
*/
|
|
91
51
|
end(): Token[];
|
|
92
|
-
/** Discard any buffered text and reset the line counter to 1. */
|
|
93
52
|
reset(): void;
|
|
94
|
-
/** Convenience: feed an entire text and immediately flush. */
|
|
95
53
|
tokenizeAll(text: string): Token[];
|
|
96
|
-
/** Stateless one-shot classification. Does not touch the streaming buffer. */
|
|
97
54
|
tokenize(line: string, lineNum?: number): Token;
|
|
98
55
|
isOp(line: string): boolean;
|
|
99
56
|
isHeader(line: string): boolean;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
export interface Anchor {
|
|
8
8
|
line: number;
|
|
9
9
|
}
|
|
10
|
-
/** Where an `insert`
|
|
10
|
+
/** Where an `insert` edit should land relative to existing content. */
|
|
11
11
|
export type Cursor = {
|
|
12
12
|
kind: "bof";
|
|
13
13
|
} | {
|
|
@@ -15,12 +15,15 @@ export type Cursor = {
|
|
|
15
15
|
} | {
|
|
16
16
|
kind: "before_anchor";
|
|
17
17
|
anchor: Anchor;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "after_anchor";
|
|
20
|
+
anchor: Anchor;
|
|
18
21
|
};
|
|
19
22
|
/**
|
|
20
23
|
* A single low-level edit produced by the parser and consumed by the applier.
|
|
21
|
-
* Multi-line replacements decompose to one `insert
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
+
* Multi-line replacements decompose to one `insert` per replacement line plus
|
|
25
|
+
* one `delete` per consumed line. Replacement payloads are tagged so the
|
|
26
|
+
* applier can distinguish literal insertion from new content for a deleted
|
|
24
27
|
* line.
|
|
25
28
|
*/
|
|
26
29
|
export type Edit = {
|
|
@@ -30,13 +33,6 @@ export type Edit = {
|
|
|
30
33
|
lineNum: number;
|
|
31
34
|
index: number;
|
|
32
35
|
mode?: "replacement";
|
|
33
|
-
} | {
|
|
34
|
-
kind: "repeat";
|
|
35
|
-
cursor: Cursor;
|
|
36
|
-
range: ParsedRange;
|
|
37
|
-
lineNum: number;
|
|
38
|
-
index: number;
|
|
39
|
-
mode?: "replacement";
|
|
40
36
|
} | {
|
|
41
37
|
kind: "delete";
|
|
42
38
|
anchor: Anchor;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/hashline",
|
|
4
|
-
"version": "15.5.
|
|
4
|
+
"version": "15.5.13",
|
|
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,7 +33,8 @@
|
|
|
33
33
|
"fmt": "biome format --write ."
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"diff": "^9.0.0"
|
|
36
|
+
"diff": "^9.0.0",
|
|
37
|
+
"lru-cache": "11.3.6"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/bun": "^1.3.14"
|