@oh-my-pi/hashline 15.11.3 → 15.11.4
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/apply.d.ts +2 -0
- package/dist/types/block.d.ts +6 -0
- package/dist/types/messages.d.ts +15 -0
- package/dist/types/types.d.ts +7 -0
- package/package.json +1 -1
- package/src/apply.ts +94 -27
- package/src/block.ts +36 -3
- package/src/input.ts +14 -6
- package/src/messages.ts +29 -0
- package/src/patcher.ts +9 -3
- package/src/prompt.md +21 -21
- package/src/types.ts +7 -0
package/dist/types/apply.d.ts
CHANGED
package/dist/types/block.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export interface ResolveBlockEditsOptions {
|
|
|
15
15
|
* edits.
|
|
16
16
|
*/
|
|
17
17
|
onResolved?: (resolution: BlockResolution) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Invoked once per diagnostic produced while resolving — currently only the
|
|
20
|
+
* closer-anchor lowering of `insert after block N:`. Hosts should surface
|
|
21
|
+
* these on the apply result's `warnings`.
|
|
22
|
+
*/
|
|
23
|
+
onWarning?: (message: string) => void;
|
|
18
24
|
}
|
|
19
25
|
/** True when at least one edit is an unresolved deferred block edit. */
|
|
20
26
|
export declare function hasBlockEdit(edits: readonly Edit[]): boolean;
|
package/dist/types/messages.d.ts
CHANGED
|
@@ -50,6 +50,13 @@ export declare function blockUnresolvedMessage(line: number, op?: "replace" | "d
|
|
|
50
50
|
* rather than authored-input error.
|
|
51
51
|
*/
|
|
52
52
|
export declare const BLOCK_RESOLVER_UNAVAILABLE = "Block-anchored ops (`replace block N:`, `delete block N`, `insert after block N:`) are not available here (no tree-sitter block resolver is configured). Use a concrete line range instead.";
|
|
53
|
+
/**
|
|
54
|
+
* Warning emitted when an `insert after block N:` anchored on a pure
|
|
55
|
+
* closing-delimiter line is lowered to plain `insert after N:`. No block
|
|
56
|
+
* begins on a closer, but the closer IS the end of one — and inserting after
|
|
57
|
+
* the end of that block is exactly what the plain form does.
|
|
58
|
+
*/
|
|
59
|
+
export declare function insertAfterBlockCloserLoweredWarning(line: number): string;
|
|
53
60
|
/**
|
|
54
61
|
* Internal invariant error: `applyEdits` received an unresolved `replace block
|
|
55
62
|
* N:` edit. Block edits must be expanded by `resolveBlockEdits` before reaching
|
|
@@ -71,6 +78,14 @@ export declare const EMPTY_INSERT = "`insert` needs at least one `+TEXT` body ro
|
|
|
71
78
|
* I read" mistake.
|
|
72
79
|
*/
|
|
73
80
|
export declare function afterInsertLandingShiftWarning(anchorLine: number, landingLine: number, crossed: number): string;
|
|
81
|
+
/**
|
|
82
|
+
* Warning emitted when an `insert after block N:` body is indented deeper
|
|
83
|
+
* than the block's closing line and the landing was pulled back inside the
|
|
84
|
+
* block. `insert after block` places content AFTER the block at sibling
|
|
85
|
+
* depth; a deeper body almost always means "append inside the block's body"
|
|
86
|
+
* — the misuse this corrects.
|
|
87
|
+
*/
|
|
88
|
+
export declare function blockInsertLandingShiftWarning(blockStart: number, closerLine: number, landingLine: number): string;
|
|
74
89
|
/** Warning text emitted by `Recovery` when an external write fits a cached snapshot. */
|
|
75
90
|
export declare const RECOVERY_EXTERNAL_WARNING = "Recovered from a stale file hash using a previous read snapshot (file changed externally between read and edit).";
|
|
76
91
|
/** Warning text emitted by `Recovery` when a prior in-session edit advanced the hash. */
|
package/dist/types/types.d.ts
CHANGED
|
@@ -33,6 +33,13 @@ export type Edit = {
|
|
|
33
33
|
lineNum: number;
|
|
34
34
|
index: number;
|
|
35
35
|
mode?: "replacement";
|
|
36
|
+
/**
|
|
37
|
+
* Present on inserts lowered from `insert after block N:`: the
|
|
38
|
+
* resolved block's first line. Lets the applier slide a body that
|
|
39
|
+
* claims a depth inside the block back across the block's trailing
|
|
40
|
+
* closer lines (never above this line).
|
|
41
|
+
*/
|
|
42
|
+
blockStart?: number;
|
|
36
43
|
} | {
|
|
37
44
|
kind: "delete";
|
|
38
45
|
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.11.
|
|
4
|
+
"version": "15.11.4",
|
|
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/apply.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* which absorbs common model mistakes where a payload restates unchanged range
|
|
8
8
|
* boundaries or duplicates/drops structural closers.
|
|
9
9
|
*/
|
|
10
|
-
import { afterInsertLandingShiftWarning, UNRESOLVED_BLOCK_INTERNAL } from "./messages";
|
|
10
|
+
import { afterInsertLandingShiftWarning, blockInsertLandingShiftWarning, UNRESOLVED_BLOCK_INTERNAL } from "./messages";
|
|
11
11
|
import { cloneCursor } from "./tokenizer";
|
|
12
12
|
import type { Anchor, ApplyResult, Cursor, Edit } from "./types";
|
|
13
13
|
|
|
@@ -123,7 +123,7 @@ function bucketAnchorEditsByLine(edits: IndexedEdit[]): Map<number, IndexedEdit[
|
|
|
123
123
|
// wrapper" mistake.
|
|
124
124
|
|
|
125
125
|
/** A line that is nothing but closing delimiters: `}`, `)`, `];`, `})`, `},`. */
|
|
126
|
-
const STRUCTURAL_CLOSER_RE = /^\s*[)\]}]+[;,]?\s*$/;
|
|
126
|
+
export const STRUCTURAL_CLOSER_RE = /^\s*[)\]}]+[;,]?\s*$/;
|
|
127
127
|
|
|
128
128
|
interface DelimiterBalance {
|
|
129
129
|
paren: number;
|
|
@@ -511,20 +511,32 @@ function repairReplacementBoundaries(
|
|
|
511
511
|
//
|
|
512
512
|
// The body rows of an `insert after N:` hunk carry an implicit depth claim:
|
|
513
513
|
// their leading indentation says how deep the author expects the new lines
|
|
514
|
-
// to sit.
|
|
515
|
-
// inserting a sibling of some enclosing construct while anchored inside it —
|
|
516
|
-
// the common shape is anchoring on the last statement of a block and writing
|
|
517
|
-
// the body at the parent's depth. Sliding the landing point forward across
|
|
518
|
-
// the structural closer lines that follow (and nothing else — content lines
|
|
519
|
-
// are never crossed) places the body at the depth its indentation names.
|
|
514
|
+
// to sit. Two corrections share that claim, in opposite directions:
|
|
520
515
|
//
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
516
|
+
// Outward (any after-insert): when the depth is shallower than line N itself,
|
|
517
|
+
// the hunk is inserting a sibling of some enclosing construct while anchored
|
|
518
|
+
// inside it — the common shape is anchoring on the last statement of a block
|
|
519
|
+
// and writing the body at the parent's depth. Sliding the landing point
|
|
520
|
+
// forward across the structural closer lines that follow (and nothing else —
|
|
521
|
+
// content lines are never crossed) places the body at the depth its
|
|
522
|
+
// indentation names.
|
|
523
|
+
//
|
|
524
|
+
// Inward (block-lowered inserts only): `insert after block N:` anchors on the
|
|
525
|
+
// resolved block's closing line, but a body indented deeper than that closer
|
|
526
|
+
// claims a depth inside the block — the common misreading of the op as
|
|
527
|
+
// "append at the end of block N's body". Sliding the landing point backward
|
|
528
|
+
// across the block's trailing closer lines places the body inside, at its
|
|
529
|
+
// claimed depth. Scoped to block-lowered inserts because there the author
|
|
530
|
+
// named the opener and never saw the closer; a plain `insert after M:` on a
|
|
531
|
+
// closer line stays literal (the escape hatch for genuinely-after content
|
|
532
|
+
// such as method-chain continuations).
|
|
533
|
+
//
|
|
534
|
+
// Both shifts are deliberately conservative: they fire only when the body
|
|
535
|
+
// and anchor indentation are comparable (one is a prefix of the other),
|
|
536
|
+
// cross only pure closing-delimiter lines, stop as soon as depth matches the
|
|
537
|
+
// body's claim, and are abandoned when any other edit in the patch targets a
|
|
538
|
+
// crossed line. Every shift is reported as a warning so the author can
|
|
539
|
+
// re-issue when the original landing was intended.
|
|
528
540
|
|
|
529
541
|
/** Leading run of tabs and spaces. */
|
|
530
542
|
function leadingIndent(line: string): string {
|
|
@@ -547,6 +559,8 @@ interface AfterInsertGroup {
|
|
|
547
559
|
anchor: number;
|
|
548
560
|
/** Indices into the edit list, in patch order. */
|
|
549
561
|
members: number[];
|
|
562
|
+
/** First line of the resolved block when lowered from `insert after block N:`. */
|
|
563
|
+
blockStart?: number;
|
|
550
564
|
}
|
|
551
565
|
|
|
552
566
|
/**
|
|
@@ -603,10 +617,52 @@ function resolveShiftedLanding(
|
|
|
603
617
|
}
|
|
604
618
|
|
|
605
619
|
/**
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
620
|
+
* Resolve where a block-lowered after-insert anchored on the block's closing
|
|
621
|
+
* line should land given a body depth `target` deeper than that closer: just
|
|
622
|
+
* above the block's trailing run of closer lines, bounded below by
|
|
623
|
+
* `blockStart` (an empty block lands the body right after its opener).
|
|
624
|
+
* Returns `undefined` when the landing stays put.
|
|
625
|
+
*/
|
|
626
|
+
function resolveInwardLanding(
|
|
627
|
+
group: AfterInsertGroup,
|
|
628
|
+
target: string,
|
|
629
|
+
blockStart: number,
|
|
630
|
+
fileLines: readonly string[],
|
|
631
|
+
targetedLines: ReadonlySet<number>,
|
|
632
|
+
): number | undefined {
|
|
633
|
+
const anchorText = fileLines[group.anchor - 1];
|
|
634
|
+
if (anchorText === undefined || !hasNonWhitespace(anchorText)) return undefined;
|
|
635
|
+
// Fires only when the block ends in a pure closer the body out-indents.
|
|
636
|
+
// Blocks ending in content (indentation-only languages) already land the
|
|
637
|
+
// body inside the block — nothing to correct.
|
|
638
|
+
if (!STRUCTURAL_CLOSER_RE.test(anchorText)) return undefined;
|
|
639
|
+
if (!isIndentDeeper(target, leadingIndent(anchorText))) return undefined;
|
|
640
|
+
|
|
641
|
+
let landing = group.anchor;
|
|
642
|
+
for (let line = group.anchor; line > blockStart; line--) {
|
|
643
|
+
const text = fileLines[line - 1] ?? "";
|
|
644
|
+
if (!hasNonWhitespace(text)) {
|
|
645
|
+
landing = line - 1; // look past trailing blanks, never land after one
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (!STRUCTURAL_CLOSER_RE.test(text)) break; // content reached — land right after it
|
|
649
|
+
const indent = leadingIndent(text);
|
|
650
|
+
if (!isIndentDeeper(target, indent)) break; // closer at the body's depth — land after it
|
|
651
|
+
// Another hunk owns this closer (the group's own rows put the anchor
|
|
652
|
+
// itself in `targetedLines`; that one is ours to cross).
|
|
653
|
+
if (line !== group.anchor && targetedLines.has(line)) return undefined;
|
|
654
|
+
landing = line - 1;
|
|
655
|
+
}
|
|
656
|
+
return landing === group.anchor ? undefined : landing;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Slide mis-anchored after-insert hunks to the depth their body indentation
|
|
661
|
+
* claims: outward past the structural closer lines that follow the anchor
|
|
662
|
+
* when the body is shallower, or — for `insert after block N:` lowerings —
|
|
663
|
+
* inward across the block's trailing closers when the body is deeper than
|
|
664
|
+
* the block's closing line. Returns the corrected edit list plus one warning
|
|
665
|
+
* per shifted hunk.
|
|
610
666
|
*/
|
|
611
667
|
function repairAfterInsertLandings(
|
|
612
668
|
edits: readonly AppliedEdit[],
|
|
@@ -620,7 +676,8 @@ function repairAfterInsertLandings(
|
|
|
620
676
|
if (edit.cursor.kind !== "after_anchor") return;
|
|
621
677
|
const key = `${edit.cursor.anchor.line}:${edit.lineNum}`;
|
|
622
678
|
const group = groups.get(key);
|
|
623
|
-
if (group === undefined)
|
|
679
|
+
if (group === undefined)
|
|
680
|
+
groups.set(key, { anchor: edit.cursor.anchor.line, members: [idx], blockStart: edit.blockStart });
|
|
624
681
|
else group.members.push(idx);
|
|
625
682
|
});
|
|
626
683
|
if (groups.size === 0) return { edits, warnings: [] };
|
|
@@ -635,17 +692,27 @@ function repairAfterInsertLandings(
|
|
|
635
692
|
|
|
636
693
|
let out: AppliedEdit[] | undefined;
|
|
637
694
|
const warnings: string[] = [];
|
|
638
|
-
|
|
639
|
-
const target = bodyTargetIndent(group.members.map(idx => (edits[idx] as InsertEdit).text));
|
|
640
|
-
if (target === undefined) continue;
|
|
641
|
-
const landing = resolveShiftedLanding(group, target, fileLines, targetedLines);
|
|
642
|
-
if (landing === undefined) continue;
|
|
695
|
+
const retarget = (group: AfterInsertGroup, line: number): void => {
|
|
643
696
|
out ??= [...edits];
|
|
644
697
|
for (const idx of group.members) {
|
|
645
698
|
const edit = out[idx] as InsertEdit;
|
|
646
|
-
out[idx] = { ...edit, cursor: { kind: "after_anchor", anchor: { line
|
|
699
|
+
out[idx] = { ...edit, cursor: { kind: "after_anchor", anchor: { line } } };
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
for (const group of groups.values()) {
|
|
703
|
+
const target = bodyTargetIndent(group.members.map(idx => (edits[idx] as InsertEdit).text));
|
|
704
|
+
if (target === undefined) continue;
|
|
705
|
+
const outward = resolveShiftedLanding(group, target, fileLines, targetedLines);
|
|
706
|
+
if (outward !== undefined) {
|
|
707
|
+
retarget(group, outward.line);
|
|
708
|
+
warnings.push(afterInsertLandingShiftWarning(group.anchor, outward.line, outward.crossed));
|
|
709
|
+
continue;
|
|
647
710
|
}
|
|
648
|
-
|
|
711
|
+
if (group.blockStart === undefined) continue;
|
|
712
|
+
const inward = resolveInwardLanding(group, target, group.blockStart, fileLines, targetedLines);
|
|
713
|
+
if (inward === undefined) continue;
|
|
714
|
+
retarget(group, inward);
|
|
715
|
+
warnings.push(blockInsertLandingShiftWarning(group.blockStart, group.anchor, inward));
|
|
649
716
|
}
|
|
650
717
|
return { edits: out ?? edits, warnings };
|
|
651
718
|
}
|
package/src/block.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* it runs, no `block` edits remain, so {@link applyEdits} (and recovery) only
|
|
13
13
|
* ever see resolved edits.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { STRUCTURAL_CLOSER_RE } from "./apply";
|
|
16
|
+
import { BLOCK_RESOLVER_UNAVAILABLE, blockUnresolvedMessage, insertAfterBlockCloserLoweredWarning } from "./messages";
|
|
16
17
|
import type { BlockResolution, BlockResolver, Cursor, Edit } from "./types";
|
|
17
18
|
|
|
18
19
|
export interface ResolveBlockEditsOptions {
|
|
@@ -31,6 +32,12 @@ export interface ResolveBlockEditsOptions {
|
|
|
31
32
|
* edits.
|
|
32
33
|
*/
|
|
33
34
|
onResolved?: (resolution: BlockResolution) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Invoked once per diagnostic produced while resolving — currently only the
|
|
37
|
+
* closer-anchor lowering of `insert after block N:`. Hosts should surface
|
|
38
|
+
* these on the apply result's `warnings`.
|
|
39
|
+
*/
|
|
40
|
+
onWarning?: (message: string) => void;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
/** True when at least one edit is an unresolved deferred block edit. */
|
|
@@ -67,6 +74,22 @@ export function resolveBlockEdits(
|
|
|
67
74
|
const op = edit.mode === "insert_after" ? "insert_after" : edit.payloads.length === 0 ? "delete" : "replace";
|
|
68
75
|
const span = resolver ? resolver({ path, text, line: edit.anchor.line }) : null;
|
|
69
76
|
if (span === null) {
|
|
77
|
+
// `insert after block N` anchored on a pure closing-delimiter line:
|
|
78
|
+
// no block begins there, but line N IS the end of one — and "after
|
|
79
|
+
// the end of the block" is exactly plain `insert after N:`. Lower it
|
|
80
|
+
// instead of failing the patch; warn so the author learns the
|
|
81
|
+
// opener-only rule.
|
|
82
|
+
if (op === "insert_after" && resolver) {
|
|
83
|
+
const anchorText = text.split("\n")[edit.anchor.line - 1];
|
|
84
|
+
if (anchorText !== undefined && STRUCTURAL_CLOSER_RE.test(anchorText)) {
|
|
85
|
+
options.onWarning?.(insertAfterBlockCloserLoweredWarning(edit.anchor.line));
|
|
86
|
+
for (const payload of edit.payloads) {
|
|
87
|
+
const cursor: Cursor = { kind: "after_anchor", anchor: { line: edit.anchor.line } };
|
|
88
|
+
resolved.push({ kind: "insert", cursor, text: payload, lineNum: edit.lineNum, index: synthIndex++ });
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
70
93
|
if (onUnresolved === "drop") continue;
|
|
71
94
|
throw new Error(
|
|
72
95
|
`line ${edit.lineNum}: ${
|
|
@@ -82,10 +105,20 @@ export function resolveBlockEdits(
|
|
|
82
105
|
});
|
|
83
106
|
if (op === "insert_after") {
|
|
84
107
|
// Mirror the parser's `insert after N:` lowering: one `after_anchor`
|
|
85
|
-
// insert per payload row, anchored on the block's last line.
|
|
108
|
+
// insert per payload row, anchored on the block's last line. The
|
|
109
|
+
// `blockStart` tag lets the applier's landing correction slide a
|
|
110
|
+
// body that claims a depth inside the block back across the block's
|
|
111
|
+
// trailing closer lines.
|
|
86
112
|
for (const payload of edit.payloads) {
|
|
87
113
|
const cursor: Cursor = { kind: "after_anchor", anchor: { line: span.end } };
|
|
88
|
-
resolved.push({
|
|
114
|
+
resolved.push({
|
|
115
|
+
kind: "insert",
|
|
116
|
+
cursor,
|
|
117
|
+
text: payload,
|
|
118
|
+
lineNum: edit.lineNum,
|
|
119
|
+
index: synthIndex++,
|
|
120
|
+
blockStart: span.start,
|
|
121
|
+
});
|
|
89
122
|
}
|
|
90
123
|
continue;
|
|
91
124
|
}
|
package/src/input.ts
CHANGED
|
@@ -309,12 +309,16 @@ export class PatchSection {
|
|
|
309
309
|
*/
|
|
310
310
|
applyTo(text: string, blockResolver?: BlockResolver): ApplyResult {
|
|
311
311
|
const { edits, warnings } = this.parse();
|
|
312
|
-
const
|
|
312
|
+
const resolveWarnings: string[] = [];
|
|
313
|
+
const resolved = resolveBlockEdits(edits, text, this.path, blockResolver, {
|
|
314
|
+
onUnresolved: "throw",
|
|
315
|
+
onWarning: warning => resolveWarnings.push(warning),
|
|
316
|
+
});
|
|
313
317
|
const result = applyEdits(text, resolved);
|
|
314
318
|
// Preserve parse warnings so consumers don't need to call `parse()`
|
|
315
319
|
// separately.
|
|
316
|
-
const merged =
|
|
317
|
-
return merged
|
|
320
|
+
const merged = [...warnings, ...resolveWarnings, ...(result.warnings ?? [])];
|
|
321
|
+
return merged.length > 0
|
|
318
322
|
? { ...result, warnings: merged }
|
|
319
323
|
: { text: result.text, firstChangedLine: result.firstChangedLine };
|
|
320
324
|
}
|
|
@@ -332,10 +336,14 @@ export class PatchSection {
|
|
|
332
336
|
*/
|
|
333
337
|
applyPartialTo(text: string, blockResolver?: BlockResolver): ApplyResult {
|
|
334
338
|
const { edits, warnings } = parsePatchStreaming(this.diff);
|
|
335
|
-
const
|
|
339
|
+
const resolveWarnings: string[] = [];
|
|
340
|
+
const resolved = resolveBlockEdits(edits, text, this.path, blockResolver, {
|
|
341
|
+
onUnresolved: "drop",
|
|
342
|
+
onWarning: warning => resolveWarnings.push(warning),
|
|
343
|
+
});
|
|
336
344
|
const result = applyEdits(text, resolved);
|
|
337
|
-
const merged =
|
|
338
|
-
return merged
|
|
345
|
+
const merged = [...warnings, ...resolveWarnings, ...(result.warnings ?? [])];
|
|
346
|
+
return merged.length > 0
|
|
339
347
|
? { ...result, warnings: merged }
|
|
340
348
|
: { text: result.text, firstChangedLine: result.firstChangedLine };
|
|
341
349
|
}
|
package/src/messages.ts
CHANGED
|
@@ -116,6 +116,19 @@ export function blockUnresolvedMessage(
|
|
|
116
116
|
export const BLOCK_RESOLVER_UNAVAILABLE =
|
|
117
117
|
"Block-anchored ops (`replace block N:`, `delete block N`, `insert after block N:`) are not available here (no tree-sitter block resolver is configured). Use a concrete line range instead.";
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Warning emitted when an `insert after block N:` anchored on a pure
|
|
121
|
+
* closing-delimiter line is lowered to plain `insert after N:`. No block
|
|
122
|
+
* begins on a closer, but the closer IS the end of one — and inserting after
|
|
123
|
+
* the end of that block is exactly what the plain form does.
|
|
124
|
+
*/
|
|
125
|
+
export function insertAfterBlockCloserLoweredWarning(line: number): string {
|
|
126
|
+
return (
|
|
127
|
+
`\`insert after block ${line}:\` anchors on a closing-delimiter line; no block begins there, so it was applied as plain \`insert after ${line}:\`. ` +
|
|
128
|
+
"Anchor `insert after block` on the line that OPENS the construct."
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
119
132
|
/**
|
|
120
133
|
* Internal invariant error: `applyEdits` received an unresolved `replace block
|
|
121
134
|
* N:` edit. Block edits must be expanded by `resolveBlockEdits` before reaching
|
|
@@ -150,6 +163,22 @@ export function afterInsertLandingShiftWarning(anchorLine: number, landingLine:
|
|
|
150
163
|
);
|
|
151
164
|
}
|
|
152
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Warning emitted when an `insert after block N:` body is indented deeper
|
|
168
|
+
* than the block's closing line and the landing was pulled back inside the
|
|
169
|
+
* block. `insert after block` places content AFTER the block at sibling
|
|
170
|
+
* depth; a deeper body almost always means "append inside the block's body"
|
|
171
|
+
* — the misuse this corrects.
|
|
172
|
+
*/
|
|
173
|
+
export function blockInsertLandingShiftWarning(blockStart: number, closerLine: number, landingLine: number): string {
|
|
174
|
+
return (
|
|
175
|
+
`insert after block ${blockStart}: the body is indented deeper than the block's closing line ${closerLine}, ` +
|
|
176
|
+
`so it was placed inside the block, after line ${landingLine}. ` +
|
|
177
|
+
"`insert after block` always lands AFTER the block at sibling depth — if you meant that, " +
|
|
178
|
+
`re-issue as plain \`insert after ${closerLine}:\` with the body indented to match line ${closerLine}.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
153
182
|
/** Warning text emitted by `Recovery` when an external write fits a cached snapshot. */
|
|
154
183
|
export const RECOVERY_EXTERNAL_WARNING =
|
|
155
184
|
"Recovered from a stale file hash using a previous read snapshot (file changed externally between read and edit).";
|
package/src/patcher.ts
CHANGED
|
@@ -380,6 +380,7 @@ export class Patcher {
|
|
|
380
380
|
// When a block edit needs the tagged snapshot but it is unavailable, the
|
|
381
381
|
// range cannot be placed safely — reject with a MismatchError (re-read).
|
|
382
382
|
const blockResolutions: BlockResolution[] = [];
|
|
383
|
+
const resolveWarnings: string[] = [];
|
|
383
384
|
let resolved: readonly Edit[] = edits;
|
|
384
385
|
if (hasBlockEdit(edits)) {
|
|
385
386
|
const baseText =
|
|
@@ -390,8 +391,13 @@ export class Patcher {
|
|
|
390
391
|
resolved = resolveBlockEdits(edits, baseText, section.path, this.blockResolver, {
|
|
391
392
|
onUnresolved: "throw",
|
|
392
393
|
onResolved: resolution => blockResolutions.push(resolution),
|
|
394
|
+
onWarning: warning => resolveWarnings.push(warning),
|
|
393
395
|
});
|
|
394
396
|
}
|
|
397
|
+
const withResolveWarnings = (result: ApplyResult): ApplyResult =>
|
|
398
|
+
resolveWarnings.length === 0
|
|
399
|
+
? result
|
|
400
|
+
: { ...result, warnings: [...resolveWarnings, ...(result.warnings ?? [])] };
|
|
395
401
|
|
|
396
402
|
// No tag, or the tag still names the live content: an edit anchored at any
|
|
397
403
|
// line is safe to apply, and the resolved block spans line up with what
|
|
@@ -399,7 +405,7 @@ export class Patcher {
|
|
|
399
405
|
// recovery below, where line numbers shift, so resolutions are dropped.)
|
|
400
406
|
if (expected === undefined || liveMatches) {
|
|
401
407
|
const result = applyEdits(normalized, resolved);
|
|
402
|
-
return blockResolutions.length > 0 ? { ...result, blockResolutions } : result;
|
|
408
|
+
return withResolveWarnings(blockResolutions.length > 0 ? { ...result, blockResolutions } : result);
|
|
403
409
|
}
|
|
404
410
|
// Head/tail-only inserts are position-stable: "start"/"end" cannot move
|
|
405
411
|
// with content drift, so a stale tag is non-fatal. Apply onto the live
|
|
@@ -407,7 +413,7 @@ export class Patcher {
|
|
|
407
413
|
// mismatch, which cannot be safely relocated and must reject.
|
|
408
414
|
if (!hasAnchorScopedEdit(resolved)) {
|
|
409
415
|
const result = applyEdits(normalized, resolved);
|
|
410
|
-
return { ...result, warnings: [HEADTAIL_DRIFT_WARNING, ...(result.warnings ?? [])] };
|
|
416
|
+
return withResolveWarnings({ ...result, warnings: [HEADTAIL_DRIFT_WARNING, ...(result.warnings ?? [])] });
|
|
411
417
|
}
|
|
412
418
|
// File drifted: try to replay the edit against the version the tag
|
|
413
419
|
// names and 3-way-merge it onto the live content.
|
|
@@ -417,7 +423,7 @@ export class Patcher {
|
|
|
417
423
|
fileHash: expected,
|
|
418
424
|
edits: resolved,
|
|
419
425
|
});
|
|
420
|
-
if (recovered) return recoveryToApplyResult(recovered);
|
|
426
|
+
if (recovered) return withResolveWarnings(recoveryToApplyResult(recovered));
|
|
421
427
|
const hashRecognized = this.snapshots.byHash(canonicalPath, expected) !== null;
|
|
422
428
|
throw this.#mismatchError(section, canonicalPath, normalized, expected, hashRecognized);
|
|
423
429
|
}
|
package/src/prompt.md
CHANGED
|
@@ -5,13 +5,13 @@ 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:` — replace original lines N..M with the body rows below.
|
|
9
|
-
`replace block N:` — replace the whole syntactic block that BEGINS on line N
|
|
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
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.
|
|
13
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
|
|
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
15
|
`insert head:` — insert the body rows at the very start of the file.
|
|
16
16
|
`insert tail:` — insert the body rows at the very end of the file.
|
|
17
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:`).
|
|
@@ -24,22 +24,22 @@ There is NO other body row kind. NEVER write `-old` or a bare/context line. To k
|
|
|
24
24
|
</body-rows>
|
|
25
25
|
|
|
26
26
|
<rules>
|
|
27
|
-
- Line numbers
|
|
28
|
-
- Numbers refer to the ORIGINAL file
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
- On a stale-tag rejection
|
|
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.
|
|
35
35
|
- One hunk per range; the body is the final content, never an old/new pair.
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
- `insert after block N
|
|
40
|
-
-
|
|
41
|
-
- Pure additions use `insert`, never a widened `replace
|
|
42
|
-
- NEVER
|
|
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.
|
|
43
43
|
</rules>
|
|
44
44
|
|
|
45
45
|
<example>
|
|
@@ -137,7 +137,7 @@ insert after 3:
|
|
|
137
137
|
|
|
138
138
|
<critical>
|
|
139
139
|
If you remember nothing else:
|
|
140
|
-
1. RE-GROUND AFTER EVERY EDIT.
|
|
141
|
-
2. RANGES ARE TIGHT
|
|
142
|
-
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.
|
|
143
143
|
</critical>
|
package/src/types.ts
CHANGED
|
@@ -31,6 +31,13 @@ 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
|
| {
|