@prometheus-ai/hashline 0.5.0
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 +79 -0
- package/dist/types/apply.d.ts +8 -0
- package/dist/types/block.d.ts +24 -0
- package/dist/types/diff-preview.d.ts +12 -0
- package/dist/types/format.d.ts +76 -0
- package/dist/types/fs.d.ts +80 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/input.d.ts +100 -0
- package/dist/types/messages.d.ts +85 -0
- package/dist/types/mismatch.d.ts +44 -0
- package/dist/types/normalize.d.ts +20 -0
- package/dist/types/parser.d.ts +23 -0
- package/dist/types/patcher.d.ts +109 -0
- package/dist/types/prefixes.d.ts +34 -0
- package/dist/types/recovery.d.ts +40 -0
- package/dist/types/snapshots.d.ts +55 -0
- package/dist/types/stream.d.ts +2 -0
- package/dist/types/tokenizer.d.ts +65 -0
- package/dist/types/types.d.ts +129 -0
- package/package.json +62 -0
- package/src/apply.ts +586 -0
- package/src/block.ts +84 -0
- package/src/diff-preview.ts +49 -0
- package/src/format.ts +134 -0
- package/src/fs.ts +167 -0
- package/src/grammar.lark +25 -0
- package/src/index.ts +17 -0
- package/src/input.ts +423 -0
- package/src/messages.ts +128 -0
- package/src/mismatch.ts +138 -0
- package/src/normalize.ts +38 -0
- package/src/parser.ts +325 -0
- package/src/patcher.ts +392 -0
- package/src/prefixes.ts +132 -0
- package/src/prompt.md +109 -0
- package/src/recovery.ts +186 -0
- package/src/snapshots.ts +128 -0
- package/src/stream.ts +132 -0
- package/src/tokenizer.ts +471 -0
- package/src/types.ts +132 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @prometheus-ai/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 "@prometheus-ai/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
|
+
replace 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
|
+
- `replace A..B:` — replace lines A..B with following `+TEXT` body rows.
|
|
51
|
+
- `replace block A:` — replace the syntactic block beginning on line A.
|
|
52
|
+
- `delete A..B` / `delete block A` — delete concrete lines or a resolved block.
|
|
53
|
+
- `insert before A:` / `insert after A:` / `insert head:` / `insert tail:` — insert following body rows.
|
|
54
|
+
- `+TEXT` — literal body row (use `+` alone for a blank line).
|
|
55
|
+
|
|
56
|
+
## Abstractions
|
|
57
|
+
|
|
58
|
+
### `Filesystem`
|
|
59
|
+
|
|
60
|
+
Read and write text by path. The default implementations:
|
|
61
|
+
|
|
62
|
+
- `InMemoryFilesystem` — backed by a `Map`. Tests, sandboxes.
|
|
63
|
+
- `NodeFilesystem` — disk-backed via `Bun.file`/`Bun.write`. Default for CLIs.
|
|
64
|
+
|
|
65
|
+
Subclass `Filesystem` to wire hashline into any storage: VFS, S3, an LSP
|
|
66
|
+
text-document protocol, a Git tree, anything.
|
|
67
|
+
|
|
68
|
+
### `SnapshotStore`
|
|
69
|
+
|
|
70
|
+
Required. Hashline tags are full-file content hashes recorded per path, so
|
|
71
|
+
`Patcher` must receive the store that observed them. Recovery replays edits
|
|
72
|
+
against the cached pre-edit snapshot and 3-way-merges onto current content
|
|
73
|
+
when the live file diverged.
|
|
74
|
+
|
|
75
|
+
### `Patcher`
|
|
76
|
+
|
|
77
|
+
The orchestration class. Reads, normalizes line endings + BOM, applies edits,
|
|
78
|
+
restores line endings, and writes via the configured `Filesystem`. Multi-section
|
|
79
|
+
patches are preflighted up front so a partial batch never lands.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ApplyResult, Edit } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Apply a parsed list of edits to a text body. Pure function — no I/O.
|
|
4
|
+
*
|
|
5
|
+
* Returns the post-edit text and the first changed line number (1-indexed).
|
|
6
|
+
* Throws if an anchor is out of bounds.
|
|
7
|
+
*/
|
|
8
|
+
export declare function applyEdits(text: string, edits: readonly Edit[]): ApplyResult;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BlockResolver, Edit } from "./types";
|
|
2
|
+
export interface ResolveBlockEditsOptions {
|
|
3
|
+
/**
|
|
4
|
+
* How to handle a block edit that cannot be resolved (missing resolver or a
|
|
5
|
+
* `null` span). `"throw"` (default) raises a `blockUnresolvedMessage` error —
|
|
6
|
+
* used by the authoritative apply + final preview paths. `"drop"` silently
|
|
7
|
+
* skips the edit — used by the streaming preview, where a half-written file
|
|
8
|
+
* or transient parse error must not throw.
|
|
9
|
+
*/
|
|
10
|
+
onUnresolved?: "throw" | "drop";
|
|
11
|
+
}
|
|
12
|
+
/** True when at least one edit is an unresolved `replace block N:` edit. */
|
|
13
|
+
export declare function hasBlockEdit(edits: readonly Edit[]): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve every `replace block N:` edit in `edits` against `text` (parsed as
|
|
16
|
+
* the language inferred from `path`). Non-block edits pass through untouched.
|
|
17
|
+
* Returns a fresh edit list with no `block` variants. The fast path returns the
|
|
18
|
+
* input unchanged when there is nothing to resolve.
|
|
19
|
+
*
|
|
20
|
+
* Synthesized inserts/deletes carry sequential `index` values for readability
|
|
21
|
+
* only — {@link applyEdits} re-derives every edit's index from array order, so
|
|
22
|
+
* the passthrough edits keeping their original indices is harmless.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveBlockEdits(edits: readonly Edit[], text: string, path: string, resolver: BlockResolver | undefined, options?: ResolveBlockEditsOptions): readonly Edit[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-number a unified diff that uses the `+<lineNum>|content` /
|
|
3
|
+
* `-<lineNum>|content` / ` <lineNum>|content` line format into a compact
|
|
4
|
+
* preview that anchors every line to its post-edit position. Added lines,
|
|
5
|
+
* removed lines, and context lines all end up with a hashline-style anchor
|
|
6
|
+
* so a follow-up edit can reuse them directly.
|
|
7
|
+
*
|
|
8
|
+
* This is intentionally decoupled from the diff producer: anything that
|
|
9
|
+
* emits the `<sign><lineNum>|<content>` shape works.
|
|
10
|
+
*/
|
|
11
|
+
import type { CompactDiffOptions, CompactDiffPreview } from "./types";
|
|
12
|
+
export declare function buildCompactDiffPreview(diff: string, _options?: CompactDiffOptions): CompactDiffPreview;
|
|
@@ -0,0 +1,76 @@
|
|
|
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 = "replace";
|
|
14
|
+
/** Hunk-header sub-keyword: `replace block N:` resolves N to a tree-sitter block range. */
|
|
15
|
+
export declare const HL_BLOCK_KEYWORD = "block";
|
|
16
|
+
/** Hunk-header keyword for concrete line deletion. */
|
|
17
|
+
export declare const HL_DELETE_KEYWORD = "delete";
|
|
18
|
+
/** Hunk-header keyword for insertion operations. */
|
|
19
|
+
export declare const HL_INSERT_KEYWORD = "insert";
|
|
20
|
+
/** Insert position keyword for inserting before a concrete line. */
|
|
21
|
+
export declare const HL_INSERT_BEFORE = "before";
|
|
22
|
+
/** Insert position keyword for inserting after a concrete line. */
|
|
23
|
+
export declare const HL_INSERT_AFTER = "after";
|
|
24
|
+
/** Insert position keyword for inserting at the start of the file. */
|
|
25
|
+
export declare const HL_INSERT_HEAD = "head";
|
|
26
|
+
/** Insert position keyword for inserting at the end of the file. */
|
|
27
|
+
export declare const HL_INSERT_TAIL = "tail";
|
|
28
|
+
/** Hunk-header terminator for body-bearing operations. */
|
|
29
|
+
export declare const HL_HEADER_COLON = ":";
|
|
30
|
+
/** Separator between a hashline file path and its opaque snapshot tag. */
|
|
31
|
+
export declare const HL_FILE_HASH_SEP = "#";
|
|
32
|
+
/** Separator between two line numbers in a range, e.g. `5..10`. */
|
|
33
|
+
export declare const HL_RANGE_SEP = "..";
|
|
34
|
+
/** Separator between a line number and displayed line content in hashline mode. */
|
|
35
|
+
export declare const HL_LINE_BODY_SEP = ":";
|
|
36
|
+
/** Bare positive line-number Lid (no decorations, no captures, no anchors). */
|
|
37
|
+
export declare const HL_LINE_RE_RAW = "[1-9]\\d*";
|
|
38
|
+
/** Capture-group form of {@link HL_LINE_RE_RAW}. */
|
|
39
|
+
export declare const HL_LINE_CAPTURE_RE_RAW = "([1-9]\\d*)";
|
|
40
|
+
/** Format a concrete replacement hunk header. */
|
|
41
|
+
export declare function formatReplaceHeader(start: number, end: number): string;
|
|
42
|
+
/** Format a concrete deletion hunk header. */
|
|
43
|
+
export declare function formatDeleteHeader(start: number, end?: number): string;
|
|
44
|
+
/** Format an insertion hunk header for a cursor position. */
|
|
45
|
+
export declare function formatInsertHeader(cursor: Cursor): string;
|
|
46
|
+
/** Number of hex characters in a content-derived file-hash tag. */
|
|
47
|
+
export declare const HL_FILE_HASH_LENGTH = 4;
|
|
48
|
+
/** Canonical uppercase hexadecimal content-hash tag carried by a hashline section header. */
|
|
49
|
+
export declare const HL_FILE_HASH_RE_RAW = "[0-9A-F]{4}";
|
|
50
|
+
/** Capture-group form of {@link HL_FILE_HASH_RE_RAW}. */
|
|
51
|
+
export declare const HL_FILE_HASH_CAPTURE_RE_RAW = "([0-9A-F]{4})";
|
|
52
|
+
/** Regex-escaped form of {@link HL_LINE_BODY_SEP}, safe for embedding inside a regex. */
|
|
53
|
+
export declare const HL_LINE_BODY_SEP_RE_RAW: string;
|
|
54
|
+
/**
|
|
55
|
+
* Representative file-hash tags for use in user-facing error messages and
|
|
56
|
+
* prompt examples.
|
|
57
|
+
*/
|
|
58
|
+
export declare const HL_FILE_HASH_EXAMPLES: readonly ["1A2B", "3C4D", "9F3E"];
|
|
59
|
+
/**
|
|
60
|
+
* Compute the content-derived hash tag carried by a hashline section header.
|
|
61
|
+
* The tag is a 4-hex fingerprint of the whole file's normalized text: any read
|
|
62
|
+
* of byte-identical content mints the same tag, and a follow-up edit anchored
|
|
63
|
+
* at any line validates whenever the live file still hashes to it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function computeFileHash(text: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Format a comma-separated list of example anchors with an optional line-number
|
|
68
|
+
* prefix, quoted for inclusion in error messages: `"160", "42", "7"`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function describeAnchorExamples(linePrefix?: string): string;
|
|
71
|
+
/** Format a hashline section header for a file path and snapshot tag. */
|
|
72
|
+
export declare function formatHashlineHeader(filePath: string, fileHash: string): string;
|
|
73
|
+
/** Formats a single numbered line as `LINE:TEXT`. */
|
|
74
|
+
export declare function formatNumberedLine(lineNumber: number, line: string): string;
|
|
75
|
+
/** Format file text with hashline-mode line-number prefixes for display. */
|
|
76
|
+
export declare function formatNumberedLines(text: string, startLine?: number): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result returned by {@link Filesystem.writeText}. The patcher echoes back
|
|
3
|
+
* `text` so adapters that transform on serialization (e.g. notebooks) can
|
|
4
|
+
* report what actually landed on disk.
|
|
5
|
+
*/
|
|
6
|
+
export interface WriteResult {
|
|
7
|
+
/** Final text that was persisted. May differ from the input if the FS transformed it. */
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* ENOENT-like error thrown by {@link Filesystem.readText} when a path is
|
|
12
|
+
* missing. Carrying a `code` property keeps the contract compatible with
|
|
13
|
+
* `node:fs` callers that already check `err.code === "ENOENT"`.
|
|
14
|
+
*/
|
|
15
|
+
export declare class NotFoundError extends Error {
|
|
16
|
+
readonly code = "ENOENT";
|
|
17
|
+
constructor(path: string, cause?: unknown);
|
|
18
|
+
}
|
|
19
|
+
/** Type guard for {@link NotFoundError} and structurally-compatible errors. */
|
|
20
|
+
export declare function isNotFound(error: unknown): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Abstract storage backend the {@link Patcher} reads from and writes to.
|
|
23
|
+
* Subclass for new backends; the package ships {@link InMemoryFilesystem} and
|
|
24
|
+
* {@link NodeFilesystem} for the most common cases.
|
|
25
|
+
*
|
|
26
|
+
* Implementations work with raw text — the patcher handles BOM stripping and
|
|
27
|
+
* line-ending normalization itself. `readText` MUST throw {@link
|
|
28
|
+
* NotFoundError} (or any error for which {@link isNotFound} returns true)
|
|
29
|
+
* when the path doesn't exist; that's how the patcher detects a create-vs-
|
|
30
|
+
* update.
|
|
31
|
+
*/
|
|
32
|
+
export declare abstract class Filesystem {
|
|
33
|
+
/** Read the file's full text content. Throw on missing file. */
|
|
34
|
+
abstract readText(path: string): Promise<string>;
|
|
35
|
+
/** Validate that `path` is writable before a prepared batch starts committing. */
|
|
36
|
+
preflightWrite(_path: string): Promise<void>;
|
|
37
|
+
/** Persist `content` at `path`. Returns the actual final text that was written. */
|
|
38
|
+
abstract writeText(path: string, content: string): Promise<WriteResult>;
|
|
39
|
+
/** Return true when the path exists and can be read. Default: probe via {@link readText}. */
|
|
40
|
+
exists(path: string): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Canonical path used as a key by external caches (e.g. snapshot
|
|
43
|
+
* stores). The default is identity; override to return an absolute or
|
|
44
|
+
* otherwise canonicalised path so producers and consumers of cached
|
|
45
|
+
* snapshots agree on the key without each having to redo the resolution.
|
|
46
|
+
*/
|
|
47
|
+
canonicalPath(path: string): string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* In-memory {@link Filesystem}. Useful for tests, sandboxes, dry-runs, and as
|
|
51
|
+
* a building block for stacked adapters (e.g. an LRU layer on top).
|
|
52
|
+
*/
|
|
53
|
+
export declare class InMemoryFilesystem extends Filesystem {
|
|
54
|
+
#private;
|
|
55
|
+
constructor(initial?: Iterable<readonly [string, string]>);
|
|
56
|
+
readText(path: string): Promise<string>;
|
|
57
|
+
writeText(path: string, content: string): Promise<WriteResult>;
|
|
58
|
+
exists(path: string): Promise<boolean>;
|
|
59
|
+
/** Synchronous helper for setting up fixtures without awaiting. */
|
|
60
|
+
set(path: string, content: string): void;
|
|
61
|
+
/** Synchronous helper for inspecting state without awaiting. */
|
|
62
|
+
get(path: string): string | undefined;
|
|
63
|
+
/** Remove a single entry. Returns true when something was removed. */
|
|
64
|
+
delete(path: string): boolean;
|
|
65
|
+
/** Wipe all entries. */
|
|
66
|
+
clear(): void;
|
|
67
|
+
/** Iterate `[path, content]` pairs. */
|
|
68
|
+
entries(): IterableIterator<[string, string]>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Disk-backed {@link Filesystem} using Bun's file APIs. The default for CLI
|
|
72
|
+
* use. Paths are accepted as-is; callers responsible for any cwd or
|
|
73
|
+
* jail/sandbox resolution should wrap this with their own subclass.
|
|
74
|
+
*/
|
|
75
|
+
export declare class NodeFilesystem extends Filesystem {
|
|
76
|
+
readText(path: string): Promise<string>;
|
|
77
|
+
writeText(path: string, content: string): Promise<WriteResult>;
|
|
78
|
+
canonicalPath(path: string): string;
|
|
79
|
+
exists(path: string): Promise<boolean>;
|
|
80
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./apply";
|
|
2
|
+
export * from "./block";
|
|
3
|
+
export * from "./diff-preview";
|
|
4
|
+
export * from "./format";
|
|
5
|
+
export * from "./fs";
|
|
6
|
+
export * from "./input";
|
|
7
|
+
export * from "./messages";
|
|
8
|
+
export * from "./mismatch";
|
|
9
|
+
export * from "./normalize";
|
|
10
|
+
export * from "./parser";
|
|
11
|
+
export * from "./patcher";
|
|
12
|
+
export * from "./prefixes";
|
|
13
|
+
export * from "./recovery";
|
|
14
|
+
export * from "./snapshots";
|
|
15
|
+
export * from "./stream";
|
|
16
|
+
export * from "./tokenizer";
|
|
17
|
+
export * from "./types";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ApplyResult, BlockResolver, Edit, SplitOptions } from "./types";
|
|
2
|
+
interface RawSection {
|
|
3
|
+
path: string;
|
|
4
|
+
fileHash?: string;
|
|
5
|
+
diff: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when the input contains at least one line that the tokenizer
|
|
9
|
+
* recognizes as a hashline op. Used by streaming previews to decide whether
|
|
10
|
+
* the partial input is worth treating as a hashline patch yet.
|
|
11
|
+
*/
|
|
12
|
+
export declare function containsRecognizableHashlineOperations(input: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Snapshot of one section in a parsed {@link Patch}: a target file plus the
|
|
15
|
+
* lazily-parsed list of edits that should land on it. Constructed by
|
|
16
|
+
* {@link Patch.parse}; consumers usually iterate `patch.sections` rather
|
|
17
|
+
* than build these directly.
|
|
18
|
+
*/
|
|
19
|
+
export declare class PatchSection {
|
|
20
|
+
#private;
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly fileHash: string | undefined;
|
|
23
|
+
readonly diff: string;
|
|
24
|
+
constructor(raw: RawSection);
|
|
25
|
+
/**
|
|
26
|
+
* Parse this section's diff body. Cached: subsequent calls return the
|
|
27
|
+
* same `{ edits, warnings }` object so callers can safely call this from
|
|
28
|
+
* multiple paths (preflight, apply, diff-preview).
|
|
29
|
+
*/
|
|
30
|
+
parse(): {
|
|
31
|
+
edits: Edit[];
|
|
32
|
+
warnings: readonly string[];
|
|
33
|
+
};
|
|
34
|
+
/** Parsed edits for this section. */
|
|
35
|
+
get edits(): readonly Edit[];
|
|
36
|
+
/** Warnings emitted during parsing of this section. */
|
|
37
|
+
get warnings(): readonly string[];
|
|
38
|
+
/**
|
|
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
|
+
*/
|
|
43
|
+
get hasAnchorScopedEdit(): boolean;
|
|
44
|
+
/** Anchor lines touched by this section, sorted ascending and deduplicated. */
|
|
45
|
+
collectAnchorLines(): readonly number[];
|
|
46
|
+
/**
|
|
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 snapshot tag. The
|
|
49
|
+
* {@link Patcher} owns tag validation and recovery; reach for this
|
|
50
|
+
* method directly when you've already validated the file content and
|
|
51
|
+
* just want the result.
|
|
52
|
+
*
|
|
53
|
+
* `blockResolver` resolves any `replace block N:` edits against `text`; an
|
|
54
|
+
* unresolvable block throws (this is the final, authoritative preview path).
|
|
55
|
+
*/
|
|
56
|
+
applyTo(text: string, blockResolver?: BlockResolver): ApplyResult;
|
|
57
|
+
/**
|
|
58
|
+
* Streaming-tolerant counterpart to {@link applyTo}. Uses
|
|
59
|
+
* {@link parsePatchStreaming} so a trailing in-flight op (no payload yet,
|
|
60
|
+
* or a per-token parse error mid-stream) does not throw or emit a phantom
|
|
61
|
+
* empty-payload edit. Intended for incremental diff previews; the writer
|
|
62
|
+
* path should always use {@link applyTo}.
|
|
63
|
+
*
|
|
64
|
+
* `blockResolver` resolves any `replace block N:` edits against `text`; an
|
|
65
|
+
* unresolvable block is silently dropped so a half-written file does not
|
|
66
|
+
* throw mid-stream.
|
|
67
|
+
*/
|
|
68
|
+
applyPartialTo(text: string, blockResolver?: BlockResolver): ApplyResult;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A parsed hashline patch — zero or more {@link PatchSection}s, each rooted
|
|
72
|
+
* at a `[PATH#HASH]` header. Construct via {@link Patch.parse}.
|
|
73
|
+
*
|
|
74
|
+
* `Patch` is pure data: parsing is line-anchored and does not look at the
|
|
75
|
+
* filesystem. To apply a patch, hand it to {@link Patcher.apply}.
|
|
76
|
+
*/
|
|
77
|
+
export declare class Patch {
|
|
78
|
+
readonly sections: readonly PatchSection[];
|
|
79
|
+
private constructor();
|
|
80
|
+
/**
|
|
81
|
+
* Parse `input` into a {@link Patch}. `options.cwd` resolves absolute
|
|
82
|
+
* paths inside headers to cwd-relative form; `options.path` provides a
|
|
83
|
+
* fallback when the input lacks a header but contains hashline ops
|
|
84
|
+
* (useful for streaming previews).
|
|
85
|
+
*
|
|
86
|
+
* Consecutive sections targeting the same path are merged into a single
|
|
87
|
+
* section with concatenated diff bodies. Anchors authored against the
|
|
88
|
+
* same file snapshot must be applied as one batch; otherwise the first
|
|
89
|
+
* sub-edit shifts line numbers out from under the second's anchors and
|
|
90
|
+
* validation fails.
|
|
91
|
+
*/
|
|
92
|
+
static parse(input: string, options?: SplitOptions): Patch;
|
|
93
|
+
/**
|
|
94
|
+
* Parse `input` and return only the first section. Throws if the input
|
|
95
|
+
* has zero sections. Convenience for the single-section case where the
|
|
96
|
+
* caller already knows the patch is one hunk.
|
|
97
|
+
*/
|
|
98
|
+
static parseSingle(input: string, options?: SplitOptions): PatchSection;
|
|
99
|
+
}
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized error and warning text emitted by the hashline parser, applier,
|
|
3
|
+
* and patcher. Consolidating these as named constants makes them easy to
|
|
4
|
+
* audit and keeps wording stable across the rendering paths that surface
|
|
5
|
+
* them.
|
|
6
|
+
*/
|
|
7
|
+
/** Lines of context shown either side of a hash mismatch. */
|
|
8
|
+
export declare const MISMATCH_CONTEXT = 2;
|
|
9
|
+
/** Optional patch envelope start marker; silently consumed when present. */
|
|
10
|
+
export declare const BEGIN_PATCH_MARKER = "*** Begin Patch";
|
|
11
|
+
/** Optional patch envelope end marker; terminates parsing when encountered. */
|
|
12
|
+
export declare const END_PATCH_MARKER = "*** End Patch";
|
|
13
|
+
/**
|
|
14
|
+
* Recovery sentinel emitted by an agent loop when a contaminated tool-call
|
|
15
|
+
* stream is truncated mid-call. Behaves like {@link END_PATCH_MARKER} for
|
|
16
|
+
* parsing — terminates the line loop — and does not surface a warning.
|
|
17
|
+
*/
|
|
18
|
+
export declare const ABORT_MARKER = "*** Abort";
|
|
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 `replace block N:` hunk has no body. */
|
|
30
|
+
export declare const EMPTY_BLOCK = "`replace block N:` needs at least one `+TEXT` body row. To delete a block, use `delete N..M` with the block's line range.";
|
|
31
|
+
/**
|
|
32
|
+
* Error text emitted when a `replace block N:` anchor cannot be resolved to a
|
|
33
|
+
* syntactic block (unrecognized language, blank/out-of-range line, no node
|
|
34
|
+
* begins on line N such as a lone closing delimiter, or the resolved block has
|
|
35
|
+
* a syntax error). Names the offending line and steers back to an explicit
|
|
36
|
+
* `replace N..M:` range.
|
|
37
|
+
*/
|
|
38
|
+
export declare function blockUnresolvedMessage(line: number): string;
|
|
39
|
+
/**
|
|
40
|
+
* Error text emitted when a `replace block N:` edit reaches a code path that
|
|
41
|
+
* has no {@link BlockResolver} wired in. Indicates a host-configuration bug
|
|
42
|
+
* rather than authored-input error.
|
|
43
|
+
*/
|
|
44
|
+
export declare const BLOCK_RESOLVER_UNAVAILABLE = "`replace block N:` is not available here (no tree-sitter block resolver is configured). Use `replace N..M:` with an explicit range.";
|
|
45
|
+
/**
|
|
46
|
+
* Internal invariant error: `applyEdits` received an unresolved `replace block
|
|
47
|
+
* N:` edit. Block edits must be expanded by `resolveBlockEdits` before reaching
|
|
48
|
+
* the applier; hitting this is a wiring bug, not authored-input error.
|
|
49
|
+
*/
|
|
50
|
+
export declare const UNRESOLVED_BLOCK_INTERNAL = "internal error: unresolved `replace block` edit reached the applier (resolveBlockEdits was not run).";
|
|
51
|
+
/** Error text emitted when a delete hunk receives a body row. */
|
|
52
|
+
export declare const DELETE_TAKES_NO_BODY = "`delete N..M` does not take body rows. Remove the body, or use `replace N..M:`.";
|
|
53
|
+
/** Error text emitted when a `delete block N` hunk receives a body row. */
|
|
54
|
+
export declare const DELETE_BLOCK_TAKES_NO_BODY = "`delete block N` does not take body rows. Remove the body, or use `replace block N:` to replace the block.";
|
|
55
|
+
/** Error text emitted when an insert hunk has no body. */
|
|
56
|
+
export declare const EMPTY_INSERT = "`insert` needs at least one `+TEXT` body row.";
|
|
57
|
+
/** Warning text emitted by `Recovery` when an external write fits a cached snapshot. */
|
|
58
|
+
export declare const RECOVERY_EXTERNAL_WARNING = "Recovered from a stale file hash using a previous read snapshot (file changed externally between read and edit).";
|
|
59
|
+
/** Warning text emitted by `Recovery` when a prior in-session edit advanced the hash. */
|
|
60
|
+
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).";
|
|
61
|
+
/**
|
|
62
|
+
* Warning text emitted by `Recovery` when the session-chain replay
|
|
63
|
+
* fast-path was taken. Distinct from {@link RECOVERY_SESSION_CHAIN_WARNING}
|
|
64
|
+
* because replay is the less-certain mode: the structured-patch 3-way
|
|
65
|
+
* merge refused, the anchor-content gate passed, but a coincidental
|
|
66
|
+
* insert+delete pair earlier in the chain could still leave an anchor's
|
|
67
|
+
* line number pointing at a duplicated row. Surface the hedge so the
|
|
68
|
+
* model verifies before continuing.
|
|
69
|
+
*/
|
|
70
|
+
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.";
|
|
71
|
+
/**
|
|
72
|
+
* Warning emitted when an `insert head:` / `insert tail:` edit is applied to an
|
|
73
|
+
* existing file whose snapshot tag is stale (the file drifted since the read).
|
|
74
|
+
* Head/tail insert position is content-independent — "start"/"end" cannot move
|
|
75
|
+
* with drift — so this is non-fatal: the edit applies onto the live content and
|
|
76
|
+
* we surface the drift instead of hard-failing (unlike an anchored mismatch).
|
|
77
|
+
*/
|
|
78
|
+
export declare const HEADTAIL_DRIFT_WARNING = "Applied an `insert head:`/`insert tail:` edit onto the current file content even though the snapshot tag was stale (the file changed since your read). Head/tail position is content-independent, so the insert was not rejected \u2014 but re-read if the drift was unexpected.";
|
|
79
|
+
/**
|
|
80
|
+
* Error text emitted when a hashline section omits the mandatory snapshot tag.
|
|
81
|
+
* The tag is REQUIRED on every section, enforced identically by the apply path
|
|
82
|
+
* ({@link Patcher.prepare}) and the preview/diff path, so both surfaces reuse
|
|
83
|
+
* this single builder to stay in lockstep.
|
|
84
|
+
*/
|
|
85
|
+
export declare function missingSnapshotTagMessage(sectionPath: string): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Format the required-shape diagnostic shown when a line reference is malformed. */
|
|
2
|
+
export declare function formatFullAnchorRequirement(raw?: string): string;
|
|
3
|
+
/** Parse a decorated bare line-number anchor like `42`, `*42:foo`, ` > 7`. */
|
|
4
|
+
export declare function parseTag(ref: string): {
|
|
5
|
+
line: number;
|
|
6
|
+
};
|
|
7
|
+
export interface MismatchDetails {
|
|
8
|
+
path?: string;
|
|
9
|
+
expectedFileHash: string;
|
|
10
|
+
actualFileHash: string;
|
|
11
|
+
fileLines: string[];
|
|
12
|
+
anchorLines?: readonly number[];
|
|
13
|
+
/**
|
|
14
|
+
* `true` when the section's expected hash resolved to a recorded snapshot
|
|
15
|
+
* (file content drifted since that snapshot), `false` when no snapshot
|
|
16
|
+
* was ever recorded for the hash (likely fabricated or carried over from
|
|
17
|
+
* a prior session). Drives a more actionable rejection message; defaults
|
|
18
|
+
* to `true` for backward compatibility with direct callers.
|
|
19
|
+
*/
|
|
20
|
+
hashRecognized?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Raised when a hashline section's snapshot tag doesn't match the live file's
|
|
24
|
+
* content (and recovery, if configured, declined the merge). Carries the
|
|
25
|
+
* file lines plus anchored lines so renderers can produce a richer
|
|
26
|
+
* diagnostic via {@link MismatchError.displayMessage}.
|
|
27
|
+
*/
|
|
28
|
+
export declare class MismatchError extends Error {
|
|
29
|
+
readonly path: string | undefined;
|
|
30
|
+
readonly expectedFileHash: string;
|
|
31
|
+
readonly actualFileHash: string;
|
|
32
|
+
readonly fileLines: string[];
|
|
33
|
+
readonly anchorLines: readonly number[];
|
|
34
|
+
readonly hashRecognized: boolean;
|
|
35
|
+
constructor(details: MismatchDetails);
|
|
36
|
+
get displayMessage(): string;
|
|
37
|
+
static rejectionHeader(details: MismatchDetails): string[];
|
|
38
|
+
static formatDisplayMessage(details: MismatchDetails): string;
|
|
39
|
+
static formatMessage(details: MismatchDetails): string;
|
|
40
|
+
}
|
|
41
|
+
/** Throws when the line reference is out of bounds for the given file. */
|
|
42
|
+
export declare function validateLineRef(ref: {
|
|
43
|
+
line: number;
|
|
44
|
+
}, fileLines: string[]): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal text-shape normalization: line-ending detection / round-trip and
|
|
3
|
+
* BOM stripping. The patcher uses these to canonicalize text to LF before
|
|
4
|
+
* applying edits and to restore the original shape on write-back.
|
|
5
|
+
*/
|
|
6
|
+
export type LineEnding = "\r\n" | "\n";
|
|
7
|
+
/** Detect the first line ending style in `content`. Defaults to LF when neither is present. */
|
|
8
|
+
export declare function detectLineEnding(content: string): LineEnding;
|
|
9
|
+
/** Normalize every line ending to LF. */
|
|
10
|
+
export declare function normalizeToLF(text: string): string;
|
|
11
|
+
/** Re-encode LF text with the requested line ending. */
|
|
12
|
+
export declare function restoreLineEndings(text: string, ending: LineEnding): string;
|
|
13
|
+
export interface BomResult {
|
|
14
|
+
/** Either the empty string or the BOM sequence (currently UTF-8 BOM). */
|
|
15
|
+
bom: string;
|
|
16
|
+
/** Text with any leading BOM removed. */
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
/** Strip a UTF-8 BOM if present and return both the BOM and the trailing text. */
|
|
20
|
+
export declare function stripBom(content: string): BomResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Token } from "./tokenizer";
|
|
2
|
+
import type { Edit } from "./types";
|
|
3
|
+
export declare class Executor {
|
|
4
|
+
#private;
|
|
5
|
+
feed(token: Token): void;
|
|
6
|
+
end(): {
|
|
7
|
+
edits: Edit[];
|
|
8
|
+
warnings: string[];
|
|
9
|
+
};
|
|
10
|
+
endStreaming(): {
|
|
11
|
+
edits: Edit[];
|
|
12
|
+
warnings: string[];
|
|
13
|
+
};
|
|
14
|
+
reset(): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function parsePatch(diff: string): {
|
|
17
|
+
edits: Edit[];
|
|
18
|
+
warnings: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare function parsePatchStreaming(diff: string): {
|
|
21
|
+
edits: Edit[];
|
|
22
|
+
warnings: string[];
|
|
23
|
+
};
|