@oh-my-pi/hashline 17.2.14 → 17.3.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/CHANGELOG.md +17 -0
- package/dist/types/apply.d.ts +13 -15
- package/dist/types/format.d.ts +5 -0
- package/dist/types/messages.d.ts +25 -27
- package/dist/types/syntax.d.ts +8 -7
- package/package.json +3 -3
- package/src/apply.ts +541 -966
- package/src/format.ts +14 -2
- package/src/messages.ts +52 -52
- package/src/prompt.md +33 -31
- package/src/syntax.ts +33 -7
package/src/format.ts
CHANGED
|
@@ -139,8 +139,20 @@ export function formatNumberedLine(lineNumber: number, line: string): string {
|
|
|
139
139
|
return `${lineNumber}${HL_LINE_BODY_SEP}${line}`;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Split LF-delimited file text into lines hashline anchors can address.
|
|
144
|
+
* A terminal newline terminates the preceding line; it is not content.
|
|
145
|
+
*/
|
|
146
|
+
export function splitAddressableFileLines(text: string): string[] {
|
|
147
|
+
const lines = text.split("\n");
|
|
148
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
149
|
+
return lines;
|
|
150
|
+
}
|
|
151
|
+
|
|
142
152
|
/** Format file text with hashline-mode line-number prefixes for display. */
|
|
143
153
|
export function formatNumberedLines(text: string, startLine = 1): string {
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
return text
|
|
155
|
+
.split("\n")
|
|
156
|
+
.map((line, i) => formatNumberedLine(startLine + i, line))
|
|
157
|
+
.join("\n");
|
|
146
158
|
}
|
package/src/messages.ts
CHANGED
|
@@ -281,11 +281,10 @@ export function pasteAfterBlockUnresolvedLoweredWarning(line: number): string {
|
|
|
281
281
|
return unresolvedLoweredWarning(`PUT >${line}*`, line, `PUT >${line}`);
|
|
282
282
|
}
|
|
283
283
|
/**
|
|
284
|
-
* A one-sided boundary echo
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
* edit is rejected instead of repaired.
|
|
284
|
+
* A one-sided exact boundary echo cannot cover the selected range after the
|
|
285
|
+
* duplicated body rows are removed. Applying or dropping it would lose
|
|
286
|
+
* distinct range content, so the edit is rejected unless a parse-restoring
|
|
287
|
+
* boundary combination proves another reading.
|
|
289
288
|
*/
|
|
290
289
|
export function ambiguousBoundaryEchoMessage(
|
|
291
290
|
startLine: number,
|
|
@@ -299,70 +298,67 @@ export function ambiguousBoundaryEchoMessage(
|
|
|
299
298
|
: `ends by restating the ${count} line(s) just below the range`;
|
|
300
299
|
return (
|
|
301
300
|
`\`PUT ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: the body ${where}, ` +
|
|
302
|
-
`but is too short to be the full final content of the
|
|
303
|
-
`
|
|
304
|
-
`Re-issue with the range covering exactly the lines that change and the body as their complete ` +
|
|
305
|
-
`final content: drop the restated keeper from the body, or widen the range to consume it.`
|
|
301
|
+
`but is too short to be the full final content of the selected range. ` +
|
|
302
|
+
`Re-issue with the range covering exactly the lines that change and the body as their complete final content.`
|
|
306
303
|
);
|
|
307
304
|
}
|
|
308
305
|
|
|
309
306
|
/**
|
|
310
|
-
* A
|
|
311
|
-
*
|
|
312
|
-
* terminate: the payload has no unmatched opener for them and its indentation
|
|
313
|
-
* is not deeper than the closer. Sparing the closer would have to guess
|
|
314
|
-
* whether the payload belongs before it (inside the block) or after it (a
|
|
315
|
-
* sibling), so the edit is rejected instead of repaired.
|
|
307
|
+
* A syntax-essential selected edge can be retained on either side of the
|
|
308
|
+
* payload, but indentation does not establish which placement was intended.
|
|
316
309
|
*/
|
|
317
|
-
export function
|
|
318
|
-
startLine: number,
|
|
319
|
-
endLine: number,
|
|
320
|
-
closerLine: number,
|
|
321
|
-
count: number,
|
|
322
|
-
): string {
|
|
323
|
-
const closers = count === 1 ? `line ${closerLine}` : `lines ${closerLine}-${closerLine + count - 1}`;
|
|
310
|
+
export function ambiguousBoundaryPlacementMessage(startLine: number, endLine: number): string {
|
|
324
311
|
return (
|
|
325
|
-
`\`PUT ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected:
|
|
326
|
-
|
|
327
|
-
`
|
|
328
|
-
`before or after the closer is ambiguous. Restate the closer in the body at the intended position, ` +
|
|
329
|
-
`or use \`PUT <${closerLine}:\` / \`PUT >${closerLine}:\` instead.`
|
|
312
|
+
`\`PUT ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: a selected boundary row is required for the file to parse, ` +
|
|
313
|
+
`but the body indentation does not establish whether it belongs before or after that row. ` +
|
|
314
|
+
`Re-read the region and re-issue with a range that excludes every unchanged boundary row.`
|
|
330
315
|
);
|
|
331
316
|
}
|
|
317
|
+
|
|
332
318
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* the construct above" mistake — but the payload's indentation claims a depth
|
|
336
|
-
* inside the block those closers terminate, so whether the new content
|
|
337
|
-
* belongs before or after the spared closer is ambiguous. Rejected instead of
|
|
338
|
-
* repaired; the at-or-above-depth reading is auto-repaired by sparing the
|
|
339
|
-
* closer ahead of the payload.
|
|
319
|
+
* Exact-text boundary rows were removed because the remaining payload covers
|
|
320
|
+
* the selected range and the same rows already survive immediately outside it.
|
|
340
321
|
*/
|
|
341
|
-
export function
|
|
342
|
-
const
|
|
322
|
+
export function textualBoundaryEchoWarning(startLine: number, leading: number, trailing: number): string {
|
|
323
|
+
const parts: string[] = [];
|
|
324
|
+
if (leading > 0) parts.push(`${leading} leading`);
|
|
325
|
+
if (trailing > 0) parts.push(`${trailing} trailing`);
|
|
343
326
|
return (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
`closer terminates — whether the new content belongs before or after the closer is ambiguous. ` +
|
|
347
|
-
`Start the range on the first line that actually changes, or restate the closer in the body at the intended position.`
|
|
327
|
+
`Auto-repaired a replacement boundary echo at line ${startLine}: dropped ${parts.join(" and ")} body line(s) ` +
|
|
328
|
+
`already present outside the range. Issue the body as final content for the selected range only.`
|
|
348
329
|
);
|
|
349
330
|
}
|
|
350
331
|
|
|
351
332
|
/**
|
|
352
|
-
* A replacement range
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* and opener/closer text shape cannot prove a syntactic block (the braces may
|
|
357
|
-
* be literal prose), so the edit applies as authored and the author decides.
|
|
333
|
+
* A replacement range's boundary disposition was corrected by the
|
|
334
|
+
* syntax-probe-judged search: syntax-essential source boundary rows were
|
|
335
|
+
* retained, or exact body echoes of surviving outside rows were removed. The
|
|
336
|
+
* authored result did not parse and the selected result does.
|
|
358
337
|
*/
|
|
359
|
-
export function
|
|
338
|
+
export function boundaryVariantRepairWarning(startLine: number, kept: number, dropped: number): string {
|
|
339
|
+
const keptPart = kept === 0 ? "" : `retained ${kept} syntax-essential source boundary row(s) selected by the range`;
|
|
340
|
+
const droppedPart = dropped === 0 ? "" : `dropped ${dropped} body row(s) duplicated just outside the range`;
|
|
341
|
+
const action = [keptPart, droppedPart].filter(Boolean).join(" and ");
|
|
360
342
|
return (
|
|
361
|
-
|
|
362
|
-
`
|
|
363
|
-
`
|
|
364
|
-
|
|
365
|
-
|
|
343
|
+
`Auto-repaired replacement boundaries at line ${startLine}: ${action}. ` +
|
|
344
|
+
`The result was verified by the syntax probe — re-issue with the range covering exactly the changed ` +
|
|
345
|
+
`lines and the body as their complete final content.`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The applied result no longer parses while the pre-edit content did: the
|
|
351
|
+
* patch introduced a syntax error. Advisory, never a rejection — the applier
|
|
352
|
+
* honors the authored edit — but the breakage is machine-confirmed by
|
|
353
|
+
* tree-sitter and surfaced in the same response instead of waiting for a
|
|
354
|
+
* compiler pass.
|
|
355
|
+
*/
|
|
356
|
+
export function editBrokeParseWarning(firstChangedLine: number | undefined): string {
|
|
357
|
+
const at = firstChangedLine === undefined ? "" : ` near line ${firstChangedLine}`;
|
|
358
|
+
return (
|
|
359
|
+
`This edit introduced a syntax error${at}: the file parsed before the patch and no longer does. ` +
|
|
360
|
+
`It was applied exactly as written, so a line number or range endpoint is likely wrong — ` +
|
|
361
|
+
`re-read the touched region and re-issue a correcting edit.`
|
|
366
362
|
);
|
|
367
363
|
}
|
|
368
364
|
|
|
@@ -373,6 +369,10 @@ export function midBlockRangeWarning(startLine: number, endLine: number, orphane
|
|
|
373
369
|
export const UNRESOLVED_BLOCK_INTERNAL =
|
|
374
370
|
"internal error: unresolved block edit reached the applier (resolveBlockEdits was not run).";
|
|
375
371
|
|
|
372
|
+
/** Internal invariant: clipboard edits must be concrete before application. */
|
|
373
|
+
export const UNRESOLVED_CLIPBOARD_INTERNAL =
|
|
374
|
+
"internal error: unresolved clipboard edit reached the applier (resolveClipboardEdits was not run).";
|
|
375
|
+
|
|
376
376
|
/** `REM` received a body row or coexists with line edits. */
|
|
377
377
|
export const REM_TAKES_NO_BODY =
|
|
378
378
|
"`REM` deletes the whole file and takes no body rows or line ops. Issue it alone under the header.";
|
package/src/prompt.md
CHANGED
|
@@ -1,38 +1,40 @@
|
|
|
1
|
-
Line-anchored patch language: name original lines/gaps to replace, insert, cut, or paste
|
|
1
|
+
Line-anchored patch language: name original lines/gaps to replace, insert, cut, or paste; then give new content. `:` headers take `+` body rows; colonless paste `PUT`, `CUT`, `REM`, `MV` take none.
|
|
2
2
|
|
|
3
3
|
<headers>
|
|
4
|
-
|
|
4
|
+
Section: `[PATH#TAG]`; `TAG`: 4-hex snapshot from latest `read`/`search`, REQUIRED each section. New files: `write`; hashline edits existing files only.
|
|
5
5
|
</headers>
|
|
6
6
|
|
|
7
7
|
<ops>
|
|
8
|
-
`PUT N.=M
|
|
9
|
-
`PUT N
|
|
10
|
-
`PUT <N:`
|
|
11
|
-
`PUT >N
|
|
12
|
-
`PUT
|
|
13
|
-
`
|
|
14
|
-
`
|
|
15
|
-
|
|
8
|
+
`PUT N.=M:`: replace original inclusive lines N–M with body.
|
|
9
|
+
`PUT N*:`: replace syntactic block beginning N; closing line resolved.
|
|
10
|
+
`PUT <N:` insert body rows before line N (`PUT <1:` = file head).
|
|
11
|
+
`PUT >N:` insert body rows after line N (`PUT >$:` = file tail).
|
|
12
|
+
`PUT >N*:`: insert after block N's end, at sibling depth. Append inside block: `PUT >M:`.
|
|
13
|
+
`PUT <N @name` / `PUT >N @name` paste register `@name` at the gap before/after line N; omit `@name` for the anonymous register.
|
|
14
|
+
`PUT N.=M @name` / `PUT N* @name` paste `@name` over the range / resolved block; `@name` required here.
|
|
15
|
+
`CUT N.=M` / `CUT N*`: delete and capture inclusive lines N–M / block N; anonymous or given `@name`.
|
|
16
|
+
`REM`: delete section file. `MV DEST`: move/rename (quote paths with spaces); prior edits apply to source, final content to `DEST`.
|
|
17
|
+
Single line: `PUT N.=N:` / `CUT N.=N`. Ranges name original inclusive touched lines; body length irrelevant.
|
|
16
18
|
</ops>
|
|
17
19
|
|
|
18
20
|
<body-rows>
|
|
19
|
-
Only
|
|
21
|
+
Only below `:` headers. Row: verbatim `+TEXT` (leading whitespace preserved); `+`: blank. NEVER `-old`, bare, or context rows: range deletes; body is final content. Keep line: exclude it from every range. Literal initial `-`/`+`: `- item` → `+- item`; `+ item` → `++ item`.
|
|
20
22
|
</body-rows>
|
|
21
23
|
|
|
22
24
|
<rules>
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
- Touch
|
|
26
|
-
-
|
|
27
|
-
- NEVER start
|
|
28
|
-
- Ranges
|
|
29
|
-
- Whole construct
|
|
30
|
-
- `PUT N*:` resolves
|
|
31
|
-
- Block ops
|
|
32
|
-
- Markdown
|
|
33
|
-
- Pure
|
|
34
|
-
- Move
|
|
35
|
-
- NEVER format/restyle
|
|
25
|
+
- Numbers and `#TAG`: latest `read`/`search` `LINE:TEXT`; numbers are original, never shifted by hunks.
|
|
26
|
+
- Each edit renumbers and changes `#TAG` → next numbers from edit response or fresh `read`.
|
|
27
|
+
- Touch displayed lines only; undisplayed hunks REJECTED. Far from read window: re-`read`; confirm construct.
|
|
28
|
+
- Elisions UNSEEN: `…`, `..`, collapsed `N-M:` rows. NEVER hunk in/across one; `read` first.
|
|
29
|
+
- NEVER start/end range mid-expression or mid-block.
|
|
30
|
+
- Ranges: changed lines only; NEVER widen over keepers. Non-adjacent changes: separate hunks.
|
|
31
|
+
- Whole construct: `PUT N*:`; internal lines: `PUT N.=M:`.
|
|
32
|
+
- `PUT N*:` resolves exactly node N. Leading decorators/attributes/doc-comments are separate nodes: point N at first decorator to include both. Standalone line-comments never swept: use `PUT N.=M:`.
|
|
33
|
+
- Block ops: opening line of multi-line construct, NEVER closer, last line, bare inner statement. One statement: plain `PUT N.=N:` / `CUT N.=N` / `PUT >N:`. At closer: `PUT >M:`.
|
|
34
|
+
- Markdown headings are block openers. Block op on `##`/`###`: whole section through deeper headings to next same/higher heading. After section `PUT >N*:`: end body with blank line to separate next heading.
|
|
35
|
+
- Pure addition: `PUT <N:` / `PUT >N:`, NEVER widened `PUT N.=M:`.
|
|
36
|
+
- Move: `CUT`+`PUT`; `CUT 5.=9 @fn` → `@fn`, `PUT >40 @fn` pastes. Single call-local move: unlabeled `CUT` + `PUT >40`. Named registers persist across edit calls.
|
|
37
|
+
- NEVER format/restyle with this tool; run project formatter.
|
|
36
38
|
</rules>
|
|
37
39
|
|
|
38
40
|
<example>
|
|
@@ -54,7 +56,7 @@ PUT 1.=3:
|
|
|
54
56
|
MV lib/greet.py
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
Markdown bullets —
|
|
59
|
+
Markdown bullets — file receives `- task`:
|
|
58
60
|
```
|
|
59
61
|
[PLAN.md#A1B2]
|
|
60
62
|
PUT >2:
|
|
@@ -62,7 +64,7 @@ PUT >2:
|
|
|
62
64
|
+ - nested task
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
Move `greet` to
|
|
67
|
+
Move `greet` to sibling file via named register; flows across sections:
|
|
66
68
|
```
|
|
67
69
|
[greet.py#A1B2]
|
|
68
70
|
CUT 1* @fn
|
|
@@ -70,7 +72,7 @@ CUT 1* @fn
|
|
|
70
72
|
PUT <1 @fn
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
`PUT 1*:` resolves lines 1–3 (`def`
|
|
75
|
+
`PUT 1*:` resolves lines 1–3 (`def` through `print(msg)`); line 4 separate, remains:
|
|
74
76
|
```
|
|
75
77
|
[greet.py#A1B2]
|
|
76
78
|
PUT 1*:
|
|
@@ -78,7 +80,7 @@ PUT 1*:
|
|
|
78
80
|
+ print(f"Hello, {name}")
|
|
79
81
|
```
|
|
80
82
|
|
|
81
|
-
Decorator/doc-comment
|
|
83
|
+
Decorator/doc-comment separate block: point N at decorator to include both; anchoring `def` line 2 orphans `@cache`:
|
|
82
84
|
```
|
|
83
85
|
[svc.py#C3D4]
|
|
84
86
|
PUT 1*:
|
|
@@ -127,7 +129,7 @@ PUT >20 @fn:
|
|
|
127
129
|
</anti-patterns>
|
|
128
130
|
|
|
129
131
|
<critical>
|
|
130
|
-
1. RE-GROUND AFTER EVERY EDIT
|
|
131
|
-
2. RANGES
|
|
132
|
-
3. BODY
|
|
132
|
+
1. RE-GROUND AFTER EVERY EDIT: edits renumber and change `#TAG`; take next numbers from edit response or fresh `read`. Stale tag/surprise: STOP; re-`read`.
|
|
133
|
+
2. RANGES TIGHT: changed lines only. Whole construct: `PUT N*:`.
|
|
134
|
+
3. BODY FINAL CONTENT: every row starts `+`; Markdown bullet: `+- item`, not `- item`.
|
|
133
135
|
</critical>
|
package/src/syntax.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Syntax probe for candidate edit results, via the native tree-sitter parser.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* heuristics as the only available evidence.
|
|
4
|
+
* Replacement-boundary repair uses parsing as its semantic filter: exact
|
|
5
|
+
* outside-row equality may justify dropping a duplicated payload edge, while
|
|
6
|
+
* retaining a selected source boundary additionally requires source-range
|
|
7
|
+
* structure, indentation, or a narrow pure-closer shape. An unrecognized
|
|
8
|
+
* language yields no structural proof, so only evidence-complete textual
|
|
9
|
+
* normalization remains available.
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
12
|
import { enclosingBlockBoundaries } from "@oh-my-pi/pi-natives";
|
|
@@ -16,6 +15,33 @@ import { enclosingBlockBoundaries } from "@oh-my-pi/pi-natives";
|
|
|
16
15
|
const parseCache = new Map<string, boolean>();
|
|
17
16
|
const PARSE_CACHE_MAX = 256;
|
|
18
17
|
|
|
18
|
+
const boundaryCache = new Map<string, readonly number[]>();
|
|
19
|
+
|
|
20
|
+
/** Syntactic node boundaries outside a visible source range. */
|
|
21
|
+
export function enclosingBoundaries(
|
|
22
|
+
lines: readonly string[],
|
|
23
|
+
path: string,
|
|
24
|
+
startLine: number,
|
|
25
|
+
endLine: number,
|
|
26
|
+
): readonly number[] {
|
|
27
|
+
const text = lines.join("\n");
|
|
28
|
+
const key = `${Bun.hash(text).toString(36)}:${text.length}:${path}:${startLine}:${endLine}`;
|
|
29
|
+
const cached = boundaryCache.get(key);
|
|
30
|
+
if (cached !== undefined) return cached;
|
|
31
|
+
let boundaries: readonly number[];
|
|
32
|
+
try {
|
|
33
|
+
boundaries = enclosingBlockBoundaries({ code: text, path, ranges: [{ startLine, endLine }] }) ?? [];
|
|
34
|
+
} catch {
|
|
35
|
+
boundaries = [];
|
|
36
|
+
}
|
|
37
|
+
if (boundaryCache.size >= PARSE_CACHE_MAX) {
|
|
38
|
+
const oldest = boundaryCache.keys().next().value;
|
|
39
|
+
if (oldest !== undefined) boundaryCache.delete(oldest);
|
|
40
|
+
}
|
|
41
|
+
boundaryCache.set(key, boundaries);
|
|
42
|
+
return boundaries;
|
|
43
|
+
}
|
|
44
|
+
|
|
19
45
|
/**
|
|
20
46
|
* `true` when `text` parses without a syntax error under the language inferred
|
|
21
47
|
* from `path`. `false` covers "does not parse" and "cannot tell" alike — no
|