@oh-my-pi/hashline 15.10.1 → 15.10.3
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/block.d.ts +8 -1
- package/dist/types/patcher.d.ts +7 -1
- package/dist/types/prefixes.d.ts +8 -0
- package/dist/types/types.d.ts +24 -0
- package/package.json +1 -1
- package/src/block.ts +14 -1
- package/src/parser.ts +31 -2
- package/src/patcher.ts +21 -6
- package/src/prefixes.ts +10 -0
- package/src/prompt.md +13 -3
- package/src/types.ts +25 -0
package/dist/types/block.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BlockResolver, Edit } from "./types";
|
|
1
|
+
import type { BlockResolution, BlockResolver, Edit } from "./types";
|
|
2
2
|
export interface ResolveBlockEditsOptions {
|
|
3
3
|
/**
|
|
4
4
|
* How to handle a block edit that cannot be resolved (missing resolver or a
|
|
@@ -8,6 +8,13 @@ export interface ResolveBlockEditsOptions {
|
|
|
8
8
|
* or transient parse error must not throw.
|
|
9
9
|
*/
|
|
10
10
|
onUnresolved?: "throw" | "drop";
|
|
11
|
+
/**
|
|
12
|
+
* Invoked once per successfully resolved block edit, in patch order, with
|
|
13
|
+
* the anchor line and the concrete span it resolved to. Lets the host echo
|
|
14
|
+
* the resolution back to the caller. Never fired for dropped/unresolvable
|
|
15
|
+
* edits.
|
|
16
|
+
*/
|
|
17
|
+
onResolved?: (resolution: BlockResolution) => void;
|
|
11
18
|
}
|
|
12
19
|
/** True when at least one edit is an unresolved `replace block N:` edit. */
|
|
13
20
|
export declare function hasBlockEdit(edits: readonly Edit[]): boolean;
|
package/dist/types/patcher.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ 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 { ApplyResult, BlockResolver } from "./types";
|
|
6
|
+
import type { ApplyResult, BlockResolution, BlockResolver } from "./types";
|
|
7
7
|
export interface PatcherOptions {
|
|
8
8
|
/** Storage backend used for all reads and writes. */
|
|
9
9
|
fs: Filesystem;
|
|
@@ -40,6 +40,12 @@ export interface PatchSectionResult {
|
|
|
40
40
|
firstChangedLine?: number;
|
|
41
41
|
/** Warnings collected by the parser, applier, and (optionally) recovery. */
|
|
42
42
|
warnings: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Resolved spans for any `replace block`/`delete block` ops, present when the
|
|
45
|
+
* apply matched the tagged content. Undefined for patches with no block ops
|
|
46
|
+
* (and for resolutions routed through drift recovery, where numbers shift).
|
|
47
|
+
*/
|
|
48
|
+
blockResolutions?: BlockResolution[];
|
|
43
49
|
}
|
|
44
50
|
export interface PatcherApplyResult {
|
|
45
51
|
sections: PatchSectionResult[];
|
package/dist/types/prefixes.d.ts
CHANGED
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
* common case for echoed file content, and erroneously echoed prefixes will
|
|
14
14
|
* otherwise turn every content line into a (malformed) op.
|
|
15
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* Single-pass variant of {@link stripLeadingHashlinePrefixes} that strips at
|
|
18
|
+
* most one leading hashline prefix (`N:`, `>>>N:`, `+N:` etc.) and does NOT
|
|
19
|
+
* loop. Use this when the input carries at most one snapshot prefix (e.g. a
|
|
20
|
+
* bare body row paste from `read` output) — recursive stripping would corrupt
|
|
21
|
+
* content whose own text starts with `digits:`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function stripOneLeadingHashlinePrefix(line: string): string;
|
|
16
24
|
/**
|
|
17
25
|
* Strip whichever prefix scheme the lines appear to be carrying:
|
|
18
26
|
* - hashline line-number prefixes (`123:`) when every content line has one
|
package/dist/types/types.d.ts
CHANGED
|
@@ -64,6 +64,13 @@ export interface ApplyResult {
|
|
|
64
64
|
firstChangedLine?: number;
|
|
65
65
|
/** Diagnostic warnings collected by the parser, patcher, or recovery. */
|
|
66
66
|
warnings?: string[];
|
|
67
|
+
/**
|
|
68
|
+
* Resolved spans for each `replace block`/`delete block` op in this apply,
|
|
69
|
+
* in patch order. Present only when the apply matched the tagged content
|
|
70
|
+
* (the common no-drift path), so the line numbers line up with what the
|
|
71
|
+
* caller read. Absent when there were no block ops.
|
|
72
|
+
*/
|
|
73
|
+
blockResolutions?: BlockResolution[];
|
|
67
74
|
}
|
|
68
75
|
/** A parsed `[A..B]` line range. */
|
|
69
76
|
export interface ParsedRange {
|
|
@@ -110,6 +117,23 @@ export interface BlockSpan {
|
|
|
110
117
|
/** Last line of the block (1-indexed, inclusive). */
|
|
111
118
|
end: number;
|
|
112
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* One `replace block N:` / `delete block N` anchor resolved to its concrete
|
|
122
|
+
* line span. Surfaced on {@link ApplyResult} so the host can echo
|
|
123
|
+
* "block N → lines start..end" and let the model catch a wrong opener — e.g. a
|
|
124
|
+
* decorator or doc-comment that sits in a separate node outside the resolved
|
|
125
|
+
* block.
|
|
126
|
+
*/
|
|
127
|
+
export interface BlockResolution {
|
|
128
|
+
/** The 1-indexed line the block op was anchored on (the `N`). */
|
|
129
|
+
anchorLine: number;
|
|
130
|
+
/** First line of the resolved span (1-indexed, inclusive). */
|
|
131
|
+
start: number;
|
|
132
|
+
/** Last line of the resolved span (1-indexed, inclusive). */
|
|
133
|
+
end: number;
|
|
134
|
+
/** True for `delete block N`; false for `replace block N:`. */
|
|
135
|
+
isDelete: boolean;
|
|
136
|
+
}
|
|
113
137
|
/** Request handed to a {@link BlockResolver} to resolve one `replace block N:` anchor. */
|
|
114
138
|
export interface BlockResolverRequest {
|
|
115
139
|
/** Target file path (used to infer language by extension). */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/hashline",
|
|
4
|
-
"version": "15.10.
|
|
4
|
+
"version": "15.10.3",
|
|
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",
|
package/src/block.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* remain, so {@link applyEdits} (and recovery) only ever see resolved edits.
|
|
11
11
|
*/
|
|
12
12
|
import { BLOCK_RESOLVER_UNAVAILABLE, blockUnresolvedMessage } from "./messages";
|
|
13
|
-
import type { BlockResolver, Cursor, Edit } from "./types";
|
|
13
|
+
import type { BlockResolution, BlockResolver, Cursor, Edit } from "./types";
|
|
14
14
|
|
|
15
15
|
export interface ResolveBlockEditsOptions {
|
|
16
16
|
/**
|
|
@@ -21,6 +21,13 @@ export interface ResolveBlockEditsOptions {
|
|
|
21
21
|
* or transient parse error must not throw.
|
|
22
22
|
*/
|
|
23
23
|
onUnresolved?: "throw" | "drop";
|
|
24
|
+
/**
|
|
25
|
+
* Invoked once per successfully resolved block edit, in patch order, with
|
|
26
|
+
* the anchor line and the concrete span it resolved to. Lets the host echo
|
|
27
|
+
* the resolution back to the caller. Never fired for dropped/unresolvable
|
|
28
|
+
* edits.
|
|
29
|
+
*/
|
|
30
|
+
onResolved?: (resolution: BlockResolution) => void;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
/** True when at least one edit is an unresolved `replace block N:` edit. */
|
|
@@ -61,6 +68,12 @@ export function resolveBlockEdits(
|
|
|
61
68
|
`line ${edit.lineNum}: ${resolver ? blockUnresolvedMessage(edit.anchor.line) : BLOCK_RESOLVER_UNAVAILABLE}`,
|
|
62
69
|
);
|
|
63
70
|
}
|
|
71
|
+
options.onResolved?.({
|
|
72
|
+
anchorLine: edit.anchor.line,
|
|
73
|
+
start: span.start,
|
|
74
|
+
end: span.end,
|
|
75
|
+
isDelete: edit.payloads.length === 0,
|
|
76
|
+
});
|
|
64
77
|
// Mirror the parser's `replace start..end:` expansion exactly: one
|
|
65
78
|
// `before_anchor` replacement insert per payload row at `span.start`,
|
|
66
79
|
// then one delete per line across `[span.start, span.end]`. An empty
|
package/src/parser.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
EMPTY_INSERT,
|
|
13
13
|
MINUS_ROW_REJECTED,
|
|
14
14
|
} from "./messages";
|
|
15
|
+
import { stripOneLeadingHashlinePrefix } from "./prefixes";
|
|
15
16
|
import { type BlockTarget, cloneCursor, type ParsedRange, type Token, Tokenizer } from "./tokenizer";
|
|
16
17
|
import type { Anchor, Cursor, Edit } from "./types";
|
|
17
18
|
|
|
@@ -81,7 +82,7 @@ interface PendingComment {
|
|
|
81
82
|
text: string;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
type PayloadRow = { kind: "literal"; text: string; lineNum: number };
|
|
85
|
+
type PayloadRow = { kind: "literal"; text: string; lineNum: number; bare?: boolean };
|
|
85
86
|
|
|
86
87
|
interface Pending {
|
|
87
88
|
target: BlockTarget;
|
|
@@ -220,7 +221,14 @@ export class Executor {
|
|
|
220
221
|
throw new Error(`line ${lineNum}: ${DELETE_BLOCK_TAKES_NO_BODY}`);
|
|
221
222
|
if (text.trimStart().charCodeAt(0) === 45 /* - */) throw new Error(`line ${lineNum}: ${MINUS_ROW_REJECTED}`);
|
|
222
223
|
if (!this.#warnings.includes(BARE_BODY_AUTO_PIPED_WARNING)) this.#warnings.push(BARE_BODY_AUTO_PIPED_WARNING);
|
|
223
|
-
|
|
224
|
+
// Defer read-output line-number stripping to #flushPending: a bare
|
|
225
|
+
// "N:text" row is only a copy-paste artifact from snapshot output
|
|
226
|
+
// when *every* bare row in the hunk carries that prefix. Stripping a
|
|
227
|
+
// row in isolation would corrupt a genuine body that merely starts
|
|
228
|
+
// with "digits:" (YAML ports "42:hello", timestamps "12:30") when it
|
|
229
|
+
// sits next to an unprefixed sibling. Rows with an explicit "+" go
|
|
230
|
+
// through #handleLiteralPayload and are never bare, never stripped.
|
|
231
|
+
this.#pending.payloads.push({ kind: "literal", text, lineNum, bare: true });
|
|
224
232
|
return;
|
|
225
233
|
}
|
|
226
234
|
if (text.trim().length === 0) return;
|
|
@@ -230,6 +238,26 @@ export class Executor {
|
|
|
230
238
|
);
|
|
231
239
|
}
|
|
232
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Strip a single read-output line-number prefix (`N:`) from every bare body
|
|
243
|
+
* row, but only when *all* bare rows carry one. A uniform set of prefixes is
|
|
244
|
+
* the signature of content pasted straight from `read`/`search` output; a
|
|
245
|
+
* mixed set means the `N:` is genuine payload content and must stay. Rows
|
|
246
|
+
* authored with an explicit `+` are not bare and are never touched.
|
|
247
|
+
*/
|
|
248
|
+
#stripBarePrefixesIfUniform(payloads: PayloadRow[]): void {
|
|
249
|
+
let sawBare = false;
|
|
250
|
+
for (const row of payloads) {
|
|
251
|
+
if (!row.bare) continue;
|
|
252
|
+
sawBare = true;
|
|
253
|
+
if (stripOneLeadingHashlinePrefix(row.text) === row.text) return;
|
|
254
|
+
}
|
|
255
|
+
if (!sawBare) return;
|
|
256
|
+
for (const row of payloads) {
|
|
257
|
+
if (row.bare) row.text = stripOneLeadingHashlinePrefix(row.text);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
233
261
|
#pushInsert(cursor: Cursor, text: string, lineNum: number, mode?: "replacement"): void {
|
|
234
262
|
this.#edits.push({
|
|
235
263
|
kind: "insert",
|
|
@@ -263,6 +291,7 @@ export class Executor {
|
|
|
263
291
|
const pending = this.#pending;
|
|
264
292
|
if (!pending) return;
|
|
265
293
|
const { target, lineNum, payloads } = pending;
|
|
294
|
+
this.#stripBarePrefixesIfUniform(payloads);
|
|
266
295
|
this.#pending = undefined;
|
|
267
296
|
if (target.kind === "delete") {
|
|
268
297
|
for (const anchor of expandRange(target.range)) this.#pushDelete(anchor, lineNum);
|
package/src/patcher.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { MismatchError } from "./mismatch";
|
|
|
33
33
|
import { detectLineEnding, type LineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
|
|
34
34
|
import { Recovery, type RecoveryResult } from "./recovery";
|
|
35
35
|
import type { SnapshotStore } from "./snapshots";
|
|
36
|
-
import type { ApplyResult, BlockResolver, Edit } from "./types";
|
|
36
|
+
import type { ApplyResult, BlockResolution, BlockResolver, Edit } from "./types";
|
|
37
37
|
|
|
38
38
|
export interface PatcherOptions {
|
|
39
39
|
/** Storage backend used for all reads and writes. */
|
|
@@ -72,6 +72,12 @@ export interface PatchSectionResult {
|
|
|
72
72
|
firstChangedLine?: number;
|
|
73
73
|
/** Warnings collected by the parser, applier, and (optionally) recovery. */
|
|
74
74
|
warnings: string[];
|
|
75
|
+
/**
|
|
76
|
+
* Resolved spans for any `replace block`/`delete block` ops, present when the
|
|
77
|
+
* apply matched the tagged content. Undefined for patches with no block ops
|
|
78
|
+
* (and for resolutions routed through drift recovery, where numbers shift).
|
|
79
|
+
*/
|
|
80
|
+
blockResolutions?: BlockResolution[];
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
export interface PatcherApplyResult {
|
|
@@ -300,6 +306,7 @@ export class Patcher {
|
|
|
300
306
|
fileHash,
|
|
301
307
|
header: formatHashlineHeader(section.path, fileHash),
|
|
302
308
|
firstChangedLine: applyResult.firstChangedLine,
|
|
309
|
+
blockResolutions: applyResult.blockResolutions,
|
|
303
310
|
warnings,
|
|
304
311
|
};
|
|
305
312
|
}
|
|
@@ -355,6 +362,7 @@ export class Patcher {
|
|
|
355
362
|
// resulting ranges flow through the 3-way-merge recovery below.
|
|
356
363
|
// When a block edit needs the tagged snapshot but it is unavailable, the
|
|
357
364
|
// range cannot be placed safely — reject with a MismatchError (re-read).
|
|
365
|
+
const blockResolutions: BlockResolution[] = [];
|
|
358
366
|
let resolved: readonly Edit[] = edits;
|
|
359
367
|
if (hasBlockEdit(edits)) {
|
|
360
368
|
const baseText =
|
|
@@ -362,13 +370,20 @@ export class Patcher {
|
|
|
362
370
|
if (baseText === undefined) {
|
|
363
371
|
throw this.#mismatchError(section, canonicalPath, normalized, expected ?? "", false);
|
|
364
372
|
}
|
|
365
|
-
resolved = resolveBlockEdits(edits, baseText, section.path, this.blockResolver, {
|
|
373
|
+
resolved = resolveBlockEdits(edits, baseText, section.path, this.blockResolver, {
|
|
374
|
+
onUnresolved: "throw",
|
|
375
|
+
onResolved: resolution => blockResolutions.push(resolution),
|
|
376
|
+
});
|
|
366
377
|
}
|
|
367
378
|
|
|
368
|
-
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
|
|
379
|
+
// No tag, or the tag still names the live content: an edit anchored at any
|
|
380
|
+
// line is safe to apply, and the resolved block spans line up with what
|
|
381
|
+
// the caller read, so echo them back. (A drifted file falls through to
|
|
382
|
+
// recovery below, where line numbers shift, so resolutions are dropped.)
|
|
383
|
+
if (expected === undefined || liveMatches) {
|
|
384
|
+
const result = applyEdits(normalized, resolved);
|
|
385
|
+
return blockResolutions.length > 0 ? { ...result, blockResolutions } : result;
|
|
386
|
+
}
|
|
372
387
|
// Head/tail-only inserts are position-stable: "start"/"end" cannot move
|
|
373
388
|
// with content drift, so a stale tag is non-fatal. Apply onto the live
|
|
374
389
|
// content and warn instead of hard-failing — unlike an anchored
|
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
|
@@ -6,7 +6,7 @@ Every file section starts with `[PATH#TAG]`. `TAG` is the 4-hex snapshot tag fro
|
|
|
6
6
|
|
|
7
7
|
<ops>
|
|
8
8
|
replace N..M: replace original lines N..M with the body rows below. CAUTION, IT IS INCLUSIVE! MAKE SURE YOU INTEND TO DELETE BOTH ENDS!
|
|
9
|
-
replace block N: replace the whole syntactic block that BEGINS on line N —
|
|
9
|
+
replace block N: replace the whole syntactic block that BEGINS on line N — header line through closing line — resolved with tree-sitter, so you never count the end. Body rows below. Reach for this to rewrite a whole construct (function/`if`/loop/class body): the end can't be mis-counted or clipped mid-block. Point N at the line that OPENS the construct (the `if`/`function`/`def`/`{`-bearing line), not a closing `}` or a blank line. The span is EXACTLY that node — a leading decorator/attribute/doc-comment is a separate node and is NOT swept in (see rules).
|
|
10
10
|
delete N..M delete original lines N..M. No body.
|
|
11
11
|
delete block N delete the whole syntactic block that BEGINS on line N.
|
|
12
12
|
insert before N: insert the body rows immediately before line N.
|
|
@@ -31,7 +31,8 @@ There is NO other body row kind. NEVER write `-old` or a bare/context line. To k
|
|
|
31
31
|
- An elided or partial read is NOT a read of the gap. A `…` (or any collapsed/truncated region) between two excerpts means those lines are UNSEEN — treat them exactly like lines you never opened. Never place a hunk on, or span a range across, an elided region; `read` that range explicitly first. Reconstructing it from memory of "what the code probably looks like" is how ranges drift off-by-N and shred neighboring blocks.
|
|
32
32
|
- On a stale-tag rejection — or any result you cannot fully account for — STOP and re-`read`. Never stack more line-numbered edits onto output you have not re-grounded; that compounds corruption.
|
|
33
33
|
- One hunk per range; the body is the final content, never an old/new pair.
|
|
34
|
-
- Keep every range as tight as the change: a range must cover ONLY lines whose content actually changes. Never widen it to swallow an unchanged signature, brace, or neighboring statement just to rewrite a few lines inside — change one line with `replace N..N`, not the whole block around it. (A range where every line genuinely changes is correctly long; tightness is about excluding unchanged lines, not about being short.) This bounds the blast radius if a number is off: a stale
|
|
34
|
+
- Keep every range as tight as the change: a range must cover ONLY lines whose content actually changes. Never widen it to swallow an unchanged signature, brace, or neighboring statement just to rewrite a few lines inside — change one line with `replace N..N`, not the whole block around it. (A range where every line genuinely changes is correctly long; tightness is about excluding unchanged lines, not about being short.) This bounds the blast radius if a number is off: a stale one-line range corrupts one line, while a stale wide range shreds every line it spans. (This is about hand-counted `replace N..M` ranges; the `replace block N` operator is the opposite — tree-sitter fixes the end, so it can't be mis-counted or clipped.)
|
|
35
|
+
- `replace block N` vs `replace N..M`: use `replace block N` to rewrite a WHOLE construct (function / `if` / loop / class body) — tree-sitter resolves its closing line, so a long body can't be mis-counted and a stale end can't clip it mid-block; the edit result echoes the span it matched (`replace block N → resolved lines A-B`), so glance at it to confirm you got what you meant. Use `replace N..M` to change specific lines inside a construct. The resolved span is EXACTLY the node beginning on line N: a leading decorator, attribute, or doc-comment is a separate node and is NOT included. To replace a decorated/annotated definition together with its decorator, point N at the FIRST decorator line (Python parses `@dec` + `def` as one block). A leading line-comment that parses as its own node (e.g. Rust `///`) is not captured by any single opener — use `replace N..M` spanning the comment and the construct.
|
|
35
36
|
- To change lines 2 and 5 while keeping 3–4, issue two hunks (`replace 2..2:` and `replace 5..5:`). Untouched lines are simply absent from every range.
|
|
36
37
|
- Pure additions use `insert`, never a widened `replace`. If the change only adds lines, `insert before/after` the spot and keep every existing line out of all ranges. Do NOT `replace` a span of keepers and retype them around the new line "to preserve" them — those retyped keepers are exactly what gets silently dropped when one is forgotten. A keeper that never enters your body cannot be lost. `replace` is only for lines whose own text changes.
|
|
37
38
|
- NEVER use this tool to format code — reordering imports, re-indenting, aligning columns, or any mechanical restyling. That is the project formatter's job; run it instead of hand-editing layout here.
|
|
@@ -84,6 +85,15 @@ replace block 1:
|
|
|
84
85
|
+def greet(name):
|
|
85
86
|
+ print(f"Hello, {name}")
|
|
86
87
|
```
|
|
88
|
+
|
|
89
|
+
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`:
|
|
90
|
+
```
|
|
91
|
+
[svc.py#C3D4]
|
|
92
|
+
replace block 1:
|
|
93
|
+
+@cache
|
|
94
|
+
+def load(key):
|
|
95
|
+
+ return store[key]
|
|
96
|
+
```
|
|
87
97
|
</example>
|
|
88
98
|
|
|
89
99
|
<anti-patterns>
|
|
@@ -117,6 +127,6 @@ insert after 2:
|
|
|
117
127
|
<critical>
|
|
118
128
|
If you remember nothing else:
|
|
119
129
|
1. RE-GROUND AFTER EVERY EDIT. Each applied edit mints a fresh `#TAG` and renumbers the file — the tag and line numbers you just used are now dead. Take the next edit's numbers from the edit response or a fresh `read`, never from pre-edit memory. On a stale-tag rejection or any unexpected result, STOP and re-`read`.
|
|
120
|
-
2. RANGES ARE TIGHT AND IN-BOUNDS. Cover only lines whose content actually changes; never widen a range to swallow an unchanged signature, brace, or statement, and never start or end a range mid-expression or mid-block. A stale
|
|
130
|
+
2. RANGES ARE TIGHT AND IN-BOUNDS. Cover only lines whose content actually changes; never widen a range to swallow an unchanged signature, brace, or statement, and never start or end a range mid-expression or mid-block. A stale one-line range corrupts one line; a stale wide range shreds everything it spans — to rewrite a whole construct, prefer `replace block N` so tree-sitter fixes the end.
|
|
121
131
|
3. THE BODY IS THE FINAL CONTENT. Only `+TEXT` rows under a `:` header — never `-old`/bare context lines, never an old/new pair. The range does the deleting.
|
|
122
132
|
</critical>
|
package/src/types.ts
CHANGED
|
@@ -59,6 +59,13 @@ export interface ApplyResult {
|
|
|
59
59
|
firstChangedLine?: number;
|
|
60
60
|
/** Diagnostic warnings collected by the parser, patcher, or recovery. */
|
|
61
61
|
warnings?: string[];
|
|
62
|
+
/**
|
|
63
|
+
* Resolved spans for each `replace block`/`delete block` op in this apply,
|
|
64
|
+
* in patch order. Present only when the apply matched the tagged content
|
|
65
|
+
* (the common no-drift path), so the line numbers line up with what the
|
|
66
|
+
* caller read. Absent when there were no block ops.
|
|
67
|
+
*/
|
|
68
|
+
blockResolutions?: BlockResolution[];
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/** A parsed `[A..B]` line range. */
|
|
@@ -112,6 +119,24 @@ export interface BlockSpan {
|
|
|
112
119
|
end: number;
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
/**
|
|
123
|
+
* One `replace block N:` / `delete block N` anchor resolved to its concrete
|
|
124
|
+
* line span. Surfaced on {@link ApplyResult} so the host can echo
|
|
125
|
+
* "block N → lines start..end" and let the model catch a wrong opener — e.g. a
|
|
126
|
+
* decorator or doc-comment that sits in a separate node outside the resolved
|
|
127
|
+
* block.
|
|
128
|
+
*/
|
|
129
|
+
export interface BlockResolution {
|
|
130
|
+
/** The 1-indexed line the block op was anchored on (the `N`). */
|
|
131
|
+
anchorLine: number;
|
|
132
|
+
/** First line of the resolved span (1-indexed, inclusive). */
|
|
133
|
+
start: number;
|
|
134
|
+
/** Last line of the resolved span (1-indexed, inclusive). */
|
|
135
|
+
end: number;
|
|
136
|
+
/** True for `delete block N`; false for `replace block N:`. */
|
|
137
|
+
isDelete: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
/** Request handed to a {@link BlockResolver} to resolve one `replace block N:` anchor. */
|
|
116
141
|
export interface BlockResolverRequest {
|
|
117
142
|
/** Target file path (used to infer language by extension). */
|