@prometheus-ai/hashline 0.5.4 → 0.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/CHANGELOG.md +7 -0
- package/dist/types/apply.d.ts +2 -0
- package/dist/types/block.d.ts +24 -9
- package/dist/types/diff-preview.d.ts +6 -4
- package/dist/types/messages.d.ts +73 -60
- package/dist/types/patcher.d.ts +7 -1
- package/dist/types/prefixes.d.ts +8 -0
- package/dist/types/snapshots.d.ts +6 -0
- package/dist/types/tokenizer.d.ts +3 -0
- package/dist/types/types.d.ts +45 -9
- package/package.json +3 -1
- package/src/apply.ts +267 -9
- package/src/block.ts +89 -15
- package/src/diff-preview.ts +96 -21
- package/src/grammar.lark +3 -1
- package/src/input.ts +16 -7
- package/src/messages.ts +121 -68
- package/src/mismatch.ts +5 -25
- package/src/parser.ts +89 -5
- package/src/patcher.ts +47 -9
- package/src/prefixes.ts +10 -0
- package/src/prompt.md +53 -19
- package/src/snapshots.ts +17 -1
- package/src/tokenizer.ts +11 -0
- package/src/types.ts +46 -9
package/src/prefixes.ts
CHANGED
|
@@ -31,6 +31,16 @@ function stripLeadingHashlinePrefixes(line: string): string {
|
|
|
31
31
|
} while (result !== previous);
|
|
32
32
|
return result;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Single-pass variant of {@link stripLeadingHashlinePrefixes} that strips at
|
|
36
|
+
* most one leading hashline prefix (`N:`, `>>>N:`, `+N:` etc.) and does NOT
|
|
37
|
+
* loop. Use this when the input carries at most one snapshot prefix (e.g. a
|
|
38
|
+
* bare body row paste from `read` output) — recursive stripping would corrupt
|
|
39
|
+
* content whose own text starts with `digits:`.
|
|
40
|
+
*/
|
|
41
|
+
export function stripOneLeadingHashlinePrefix(line: string): string {
|
|
42
|
+
return line.replace(HL_PREFIX_RE, "");
|
|
43
|
+
}
|
|
34
44
|
|
|
35
45
|
interface LinePrefixStats {
|
|
36
46
|
nonEmpty: number;
|
package/src/prompt.md
CHANGED
|
@@ -5,14 +5,15 @@ Every file section starts with `[PATH#TAG]`. `TAG` is the 4-hex snapshot tag fro
|
|
|
5
5
|
</headers>
|
|
6
6
|
|
|
7
7
|
<ops>
|
|
8
|
-
replace N..M
|
|
9
|
-
replace block N
|
|
10
|
-
delete N..M
|
|
11
|
-
delete block N
|
|
12
|
-
insert before N
|
|
13
|
-
insert after N
|
|
14
|
-
insert
|
|
15
|
-
insert
|
|
8
|
+
`replace N..M:` — replace original lines N..M with the body rows below. INCLUSIVE — line M is consumed too.
|
|
9
|
+
`replace block N:` — replace the whole syntactic block that BEGINS on line N; tree-sitter resolves the closing line. Body rows below.
|
|
10
|
+
`delete N..M` — delete original lines N..M. No body.
|
|
11
|
+
`delete block N` — delete the whole syntactic block that BEGINS on line N.
|
|
12
|
+
`insert before N:` — insert the body rows immediately before line N.
|
|
13
|
+
`insert after N:` — insert the body rows immediately after line N.
|
|
14
|
+
`insert after block N:` — insert the body rows after the END of the block that BEGINS on line N — outside it, at sibling depth. To append inside a block, use `insert after`.
|
|
15
|
+
`insert head:` — insert the body rows at the very start of the file.
|
|
16
|
+
`insert tail:` — insert the body rows at the very end of the file.
|
|
16
17
|
Single line: `replace N..N:` / `delete N`. The range is the ORIGINAL lines you touch; body length is irrelevant (replacing 1 line with 10 is still `replace N..N:`).
|
|
17
18
|
</ops>
|
|
18
19
|
|
|
@@ -23,15 +24,22 @@ There is NO other body row kind. NEVER write `-old` or a bare/context line. To k
|
|
|
23
24
|
</body-rows>
|
|
24
25
|
|
|
25
26
|
<rules>
|
|
26
|
-
- Line numbers
|
|
27
|
-
- Numbers refer to the ORIGINAL file
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
27
|
+
- Line numbers and the `[PATH#TAG]` header come from your latest `read`/`search` (`LINE:TEXT` rows).
|
|
28
|
+
- Numbers refer to the ORIGINAL file; they do not shift as hunks apply.
|
|
29
|
+
- They die with the call: every applied edit mints a fresh `#TAG` and renumbers — anchor the next edit on the edit response or a fresh `read`.
|
|
30
|
+
- Touch only lines you literally saw as `LINE:TEXT`; the tag certifies the snapshot, not your knowledge of it.
|
|
31
|
+
- Elided regions (`…`) are UNSEEN — never place or span a hunk across one; `read` it first.
|
|
32
|
+
- Never start or end a range mid-expression or mid-block.
|
|
33
|
+
- Indent body rows exactly for the depth they should live at.
|
|
34
|
+
- On a stale-tag rejection or any surprising result: STOP and re-`read` before further edits.
|
|
31
35
|
- One hunk per range; the body is the final content, never an old/new pair.
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
36
|
+
- Ranges cover ONLY lines whose content changes. Never widen over unchanged lines — a stale wide range shreds everything it spans.
|
|
37
|
+
- Whole construct → `replace block N` (tree-sitter resolves the end); lines inside it → `replace N..M`.
|
|
38
|
+
- `replace block N` resolves EXACTLY the node at N. Leading decorators/attributes/doc-comments are separate nodes: point N at the FIRST decorator to sweep both; standalone line-comments are never swept — use `replace N..M`.
|
|
39
|
+
- `insert after block N`: N is the opener, never the closer or last visible line; saw the closer? Use plain `insert after M:`.
|
|
40
|
+
- Non-adjacent changes = separate hunks; untouched lines stay out of every range.
|
|
41
|
+
- Pure additions use `insert`, never a widened `replace` — retyped keepers are exactly what gets dropped.
|
|
42
|
+
- NEVER format/restyle code with this tool; run the project formatter instead.
|
|
35
43
|
</rules>
|
|
36
44
|
|
|
37
45
|
<example>
|
|
@@ -81,6 +89,15 @@ replace block 1:
|
|
|
81
89
|
+def greet(name):
|
|
82
90
|
+ print(f"Hello, {name}")
|
|
83
91
|
```
|
|
92
|
+
|
|
93
|
+
A decorator or doc-comment is a SEPARATE block — `replace block` on the `def`/`fn` line keeps it. Point N at the decorator to take both; here line 1 is `@cache`, so anchoring on the `def` (line 2) would resolve only the function and orphan `@cache`:
|
|
94
|
+
```
|
|
95
|
+
[svc.py#C3D4]
|
|
96
|
+
replace block 1:
|
|
97
|
+
+@cache
|
|
98
|
+
+def load(key):
|
|
99
|
+
+ return store[key]
|
|
100
|
+
```
|
|
84
101
|
</example>
|
|
85
102
|
|
|
86
103
|
<anti-patterns>
|
|
@@ -99,11 +116,28 @@ replace 3..3:
|
|
|
99
116
|
# RIGHT
|
|
100
117
|
replace 3..3:
|
|
101
118
|
+ return msg
|
|
119
|
+
|
|
120
|
+
# WRONG — a pure insertion done as a widened `replace`: you only want to add one line after 2,
|
|
121
|
+
# but you replace 2..4, retype the keepers in the body, and drop one (here line 4, `greet("world")`).
|
|
122
|
+
replace 2..4:
|
|
123
|
+
+ msg = "Hello, " + name
|
|
124
|
+
+ extra = compute(name)
|
|
125
|
+
+ print(msg)
|
|
126
|
+
# RIGHT — touch nothing you keep; the new line is the whole body.
|
|
127
|
+
insert after 2:
|
|
128
|
+
+ extra = compute(name)
|
|
129
|
+
|
|
130
|
+
# WRONG — `insert after block N:` anchored on a closing delimiter / last visible line. RIGHT: plain `insert after M:`
|
|
131
|
+
insert after block 3:
|
|
132
|
+
+after()
|
|
133
|
+
# RIGHT
|
|
134
|
+
insert after 3:
|
|
135
|
+
+after()
|
|
102
136
|
</anti-patterns>
|
|
103
137
|
|
|
104
138
|
<critical>
|
|
105
139
|
If you remember nothing else:
|
|
106
|
-
1. RE-GROUND AFTER EVERY EDIT.
|
|
107
|
-
2. RANGES ARE TIGHT
|
|
108
|
-
3. THE BODY IS THE FINAL CONTENT. Only `+TEXT` rows
|
|
140
|
+
1. RE-GROUND AFTER EVERY EDIT. Every apply mints a fresh `#TAG` and renumbers — take the next edit's numbers from the edit response or a fresh `read`. Stale tag or surprise? STOP, re-`read`.
|
|
141
|
+
2. RANGES ARE TIGHT. Cover only lines that change; a stale wide range shreds everything it spans. Whole construct → `replace block N`.
|
|
142
|
+
3. THE BODY IS THE FINAL CONTENT. Only `+TEXT` rows; never `-old`/context lines. The range does the deleting.
|
|
109
143
|
</critical>
|
package/src/snapshots.ts
CHANGED
|
@@ -62,12 +62,20 @@ export abstract class SnapshotStore {
|
|
|
62
62
|
|
|
63
63
|
const DEFAULT_MAX_PATHS = 30;
|
|
64
64
|
const DEFAULT_MAX_VERSIONS_PER_PATH = 4;
|
|
65
|
+
/** Global ceiling on retained snapshot text across all paths (UTF-16 code units). */
|
|
66
|
+
const DEFAULT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
65
67
|
|
|
66
68
|
export interface InMemorySnapshotStoreOptions {
|
|
67
69
|
/** Maximum number of distinct paths tracked at once (default 30). LRU eviction. */
|
|
68
70
|
maxPaths?: number;
|
|
69
71
|
/** Maximum full-file versions retained per path (default 4). Oldest dropped first. */
|
|
70
72
|
maxVersionsPerPath?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Global ceiling on retained snapshot text summed across every path's
|
|
75
|
+
* version history, measured in UTF-16 code units (default 64 MiB).
|
|
76
|
+
* Least-recently-used path histories are evicted to stay under it.
|
|
77
|
+
*/
|
|
78
|
+
maxTotalBytes?: number;
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
/**
|
|
@@ -85,7 +93,15 @@ export class InMemorySnapshotStore extends SnapshotStore {
|
|
|
85
93
|
|
|
86
94
|
constructor(options: InMemorySnapshotStoreOptions = {}) {
|
|
87
95
|
super();
|
|
88
|
-
this.#versions = new LRUCache<string, Snapshot[]>({
|
|
96
|
+
this.#versions = new LRUCache<string, Snapshot[]>({
|
|
97
|
+
max: options.maxPaths ?? DEFAULT_MAX_PATHS,
|
|
98
|
+
maxSize: options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
|
|
99
|
+
sizeCalculation: history => {
|
|
100
|
+
let total = 1;
|
|
101
|
+
for (const version of history) total += version.text.length;
|
|
102
|
+
return total;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
89
105
|
this.#maxVersionsPerPath = options.maxVersionsPerPath ?? DEFAULT_MAX_VERSIONS_PER_PATH;
|
|
90
106
|
}
|
|
91
107
|
|
package/src/tokenizer.ts
CHANGED
|
@@ -204,6 +204,7 @@ export type BlockTarget =
|
|
|
204
204
|
| { kind: "delete_block"; anchor: Anchor }
|
|
205
205
|
| { kind: "insert_before"; anchor: Anchor }
|
|
206
206
|
| { kind: "insert_after"; anchor: Anchor }
|
|
207
|
+
| { kind: "insert_after_block"; anchor: Anchor }
|
|
207
208
|
| { kind: "bof" }
|
|
208
209
|
| { kind: "eof" };
|
|
209
210
|
|
|
@@ -238,6 +239,16 @@ function scanInsertTarget(line: string, index: number, end: number): TargetScan
|
|
|
238
239
|
}
|
|
239
240
|
const afterEnd = scanKeyword(line, cursor, end, HL_INSERT_AFTER);
|
|
240
241
|
if (afterEnd !== null) {
|
|
242
|
+
// `insert after block N:` — resolve N to a tree-sitter block range at
|
|
243
|
+
// apply time and insert after its last line. Try the `block` sub-keyword
|
|
244
|
+
// before falling back to a literal `insert after N:` anchor.
|
|
245
|
+
const blockEnd = scanKeyword(line, skipWhitespace(line, afterEnd, end), end, HL_BLOCK_KEYWORD);
|
|
246
|
+
if (blockEnd !== null) {
|
|
247
|
+
const anchor = scanLineNumber(line, skipWhitespace(line, blockEnd, end), end);
|
|
248
|
+
if (anchor === null) return null;
|
|
249
|
+
const nextIndex = consumeOptionalColon(line, anchor.nextIndex, end);
|
|
250
|
+
return { target: { kind: "insert_after_block", anchor: { line: anchor.line } }, nextIndex };
|
|
251
|
+
}
|
|
241
252
|
const anchor = scanLineNumber(line, skipWhitespace(line, afterEnd, end), end);
|
|
242
253
|
if (anchor === null) return null;
|
|
243
254
|
const nextIndex = consumeOptionalColon(line, anchor.nextIndex, end);
|
package/src/types.ts
CHANGED
|
@@ -31,22 +31,32 @@ export type Edit =
|
|
|
31
31
|
lineNum: number;
|
|
32
32
|
index: number;
|
|
33
33
|
mode?: "replacement";
|
|
34
|
+
/**
|
|
35
|
+
* Present on inserts lowered from `insert after block N:`: the
|
|
36
|
+
* resolved block's first line. Lets the applier slide a body that
|
|
37
|
+
* claims a depth inside the block back across the block's trailing
|
|
38
|
+
* closer lines (never above this line).
|
|
39
|
+
*/
|
|
40
|
+
blockStart?: number;
|
|
34
41
|
}
|
|
35
42
|
| { kind: "delete"; anchor: Anchor; lineNum: number; index: number; oldAssertion?: string }
|
|
36
43
|
| {
|
|
37
44
|
/**
|
|
38
|
-
* Deferred block edit (`replace block N:` / `delete block N`
|
|
39
|
-
* line span is unknown at parse
|
|
40
|
-
* {@link resolveBlockEdits} once file text +
|
|
41
|
-
* available, then expanded into concrete edits:
|
|
42
|
-
* (from `replace block`) becomes
|
|
43
|
-
* that `replace start..end:`
|
|
44
|
-
*
|
|
45
|
+
* Deferred block edit (`replace block N:` / `delete block N` /
|
|
46
|
+
* `insert after block N:`). The exact line span is unknown at parse
|
|
47
|
+
* time — it is computed by {@link resolveBlockEdits} once file text +
|
|
48
|
+
* path (→ language) are available, then expanded into concrete edits:
|
|
49
|
+
* a non-empty `payloads` without `mode` (from `replace block`) becomes
|
|
50
|
+
* the same `replacement` inserts + deletes that `replace start..end:`
|
|
51
|
+
* produces; an empty `payloads` (from `delete block`) becomes a pure
|
|
52
|
+
* range deletion; `mode: "insert_after"` becomes plain `after_anchor`
|
|
53
|
+
* inserts at the block's last line. `applyEdits` never sees this
|
|
45
54
|
* variant.
|
|
46
55
|
*/
|
|
47
56
|
kind: "block";
|
|
48
57
|
anchor: Anchor;
|
|
49
58
|
payloads: string[];
|
|
59
|
+
mode?: "insert_after";
|
|
50
60
|
lineNum: number;
|
|
51
61
|
index: number;
|
|
52
62
|
};
|
|
@@ -59,6 +69,13 @@ export interface ApplyResult {
|
|
|
59
69
|
firstChangedLine?: number;
|
|
60
70
|
/** Diagnostic warnings collected by the parser, patcher, or recovery. */
|
|
61
71
|
warnings?: string[];
|
|
72
|
+
/**
|
|
73
|
+
* Resolved spans for each `replace block`/`delete block` op in this apply,
|
|
74
|
+
* in patch order. Present only when the apply matched the tagged content
|
|
75
|
+
* (the common no-drift path), so the line numbers line up with what the
|
|
76
|
+
* caller read. Absent when there were no block ops.
|
|
77
|
+
*/
|
|
78
|
+
blockResolutions?: BlockResolution[];
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
/** A parsed `[A..B]` line range. */
|
|
@@ -96,9 +113,11 @@ export interface CompactDiffPreview {
|
|
|
96
113
|
removedLines: number;
|
|
97
114
|
}
|
|
98
115
|
|
|
99
|
-
/** Optional knobs for {@link buildCompactDiffPreview}.
|
|
116
|
+
/** Optional knobs for {@link buildCompactDiffPreview}. */
|
|
100
117
|
export interface CompactDiffOptions {
|
|
101
|
-
/**
|
|
118
|
+
/** Added lines kept on each side of a long added-run elision (default 2). */
|
|
119
|
+
maxAddedRunContext?: number;
|
|
120
|
+
/** Back-compat alias for {@link maxAddedRunContext}. */
|
|
102
121
|
maxUnchangedRun?: number;
|
|
103
122
|
}
|
|
104
123
|
|
|
@@ -112,6 +131,24 @@ export interface BlockSpan {
|
|
|
112
131
|
end: number;
|
|
113
132
|
}
|
|
114
133
|
|
|
134
|
+
/**
|
|
135
|
+
* One `replace block N:` / `delete block N` / `insert after block N:` anchor
|
|
136
|
+
* resolved to its concrete line span. Surfaced on {@link ApplyResult} so the
|
|
137
|
+
* host can echo "block N → lines start..end" and let the model catch a wrong
|
|
138
|
+
* opener — e.g. a decorator or doc-comment that sits in a separate node
|
|
139
|
+
* outside the resolved block.
|
|
140
|
+
*/
|
|
141
|
+
export interface BlockResolution {
|
|
142
|
+
/** The 1-indexed line the block op was anchored on (the `N`). */
|
|
143
|
+
anchorLine: number;
|
|
144
|
+
/** First line of the resolved span (1-indexed, inclusive). */
|
|
145
|
+
start: number;
|
|
146
|
+
/** Last line of the resolved span (1-indexed, inclusive). */
|
|
147
|
+
end: number;
|
|
148
|
+
/** Which block op produced this resolution. */
|
|
149
|
+
op: "replace" | "delete" | "insert_after";
|
|
150
|
+
}
|
|
151
|
+
|
|
115
152
|
/** Request handed to a {@link BlockResolver} to resolve one `replace block N:` anchor. */
|
|
116
153
|
export interface BlockResolverRequest {
|
|
117
154
|
/** Target file path (used to infer language by extension). */
|