@oh-my-pi/hashline 15.10.12 → 15.11.1
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/messages.d.ts +11 -3
- package/package.json +1 -1
- package/src/block.ts +3 -1
- package/src/messages.ts +42 -7
- package/src/mismatch.ts +5 -25
- package/src/prompt.md +9 -1
package/dist/types/messages.d.ts
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
/** Lines of context shown either side of a hash mismatch. */
|
|
8
8
|
export declare const MISMATCH_CONTEXT = 2;
|
|
9
|
+
/**
|
|
10
|
+
* Render numbered `LINE:TEXT` context rows around `anchorLines`
|
|
11
|
+
* (±{@link MISMATCH_CONTEXT} lines each), `*`-marking the anchored lines and
|
|
12
|
+
* separating non-adjacent runs with `...`. Out-of-range anchors contribute no
|
|
13
|
+
* rows; returns an empty array when every anchor is out of range.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatAnchoredContext(anchorLines: readonly number[], fileLines: readonly string[]): string[];
|
|
9
16
|
/** Optional patch envelope start marker; silently consumed when present. */
|
|
10
17
|
export declare const BEGIN_PATCH_MARKER = "*** Begin Patch";
|
|
11
18
|
/** Optional patch envelope end marker; terminates parsing when encountered. */
|
|
@@ -32,10 +39,11 @@ export declare const EMPTY_BLOCK = "`replace block N:` needs at least one `+TEXT
|
|
|
32
39
|
* Error text emitted when a block-anchored op cannot be resolved to a
|
|
33
40
|
* syntactic block (unrecognized language, blank/out-of-range line, no node
|
|
34
41
|
* begins on line N such as a lone closing delimiter, or the resolved block has
|
|
35
|
-
* a syntax error). Names the offending line
|
|
36
|
-
* concrete-line form
|
|
42
|
+
* a syntax error). Names the offending line, steers back to an explicit
|
|
43
|
+
* concrete-line form, and — when `fileLines` is provided — appends a
|
|
44
|
+
* {@link formatAnchoredContext} preview of the file around the anchor line.
|
|
37
45
|
*/
|
|
38
|
-
export declare function blockUnresolvedMessage(line: number, op?: "replace" | "delete" | "insert_after"): string;
|
|
46
|
+
export declare function blockUnresolvedMessage(line: number, op?: "replace" | "delete" | "insert_after", fileLines?: readonly string[]): string;
|
|
39
47
|
/**
|
|
40
48
|
* Error text emitted when a block-anchored edit reaches a code path that
|
|
41
49
|
* has no {@link BlockResolver} wired in. Indicates a host-configuration bug
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/hashline",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.11.1",
|
|
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
|
@@ -69,7 +69,9 @@ export function resolveBlockEdits(
|
|
|
69
69
|
if (span === null) {
|
|
70
70
|
if (onUnresolved === "drop") continue;
|
|
71
71
|
throw new Error(
|
|
72
|
-
`line ${edit.lineNum}: ${
|
|
72
|
+
`line ${edit.lineNum}: ${
|
|
73
|
+
resolver ? blockUnresolvedMessage(edit.anchor.line, op, text.split("\n")) : BLOCK_RESOLVER_UNAVAILABLE
|
|
74
|
+
}`,
|
|
73
75
|
);
|
|
74
76
|
}
|
|
75
77
|
options.onResolved?.({
|
package/src/messages.ts
CHANGED
|
@@ -5,11 +5,37 @@
|
|
|
5
5
|
* them.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { HL_FILE_HASH_SEP, HL_FILE_PREFIX, HL_FILE_SUFFIX } from "./format";
|
|
8
|
+
import { formatNumberedLine, HL_FILE_HASH_SEP, HL_FILE_PREFIX, HL_FILE_SUFFIX } from "./format";
|
|
9
9
|
|
|
10
10
|
/** Lines of context shown either side of a hash mismatch. */
|
|
11
11
|
export const MISMATCH_CONTEXT = 2;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Render numbered `LINE:TEXT` context rows around `anchorLines`
|
|
15
|
+
* (±{@link MISMATCH_CONTEXT} lines each), `*`-marking the anchored lines and
|
|
16
|
+
* separating non-adjacent runs with `...`. Out-of-range anchors contribute no
|
|
17
|
+
* rows; returns an empty array when every anchor is out of range.
|
|
18
|
+
*/
|
|
19
|
+
export function formatAnchoredContext(anchorLines: readonly number[], fileLines: readonly string[]): string[] {
|
|
20
|
+
const displayLines = new Set<number>();
|
|
21
|
+
for (const line of anchorLines) {
|
|
22
|
+
if (line < 1 || line > fileLines.length) continue;
|
|
23
|
+
const lo = Math.max(1, line - MISMATCH_CONTEXT);
|
|
24
|
+
const hi = Math.min(fileLines.length, line + MISMATCH_CONTEXT);
|
|
25
|
+
for (let lineNum = lo; lineNum <= hi; lineNum++) displayLines.add(lineNum);
|
|
26
|
+
}
|
|
27
|
+
const anchorSet = new Set(anchorLines);
|
|
28
|
+
const rows: string[] = [];
|
|
29
|
+
let previous = -1;
|
|
30
|
+
for (const lineNum of [...displayLines].sort((a, b) => a - b)) {
|
|
31
|
+
if (previous !== -1 && lineNum > previous + 1) rows.push("...");
|
|
32
|
+
previous = lineNum;
|
|
33
|
+
const marker = anchorSet.has(lineNum) ? "*" : " ";
|
|
34
|
+
rows.push(`${marker}${formatNumberedLine(lineNum, fileLines[lineNum - 1] ?? "")}`);
|
|
35
|
+
}
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
/** Optional patch envelope start marker; silently consumed when present. */
|
|
14
40
|
export const BEGIN_PATCH_MARKER = "*** Begin Patch";
|
|
15
41
|
|
|
@@ -50,10 +76,15 @@ export const EMPTY_BLOCK =
|
|
|
50
76
|
* Error text emitted when a block-anchored op cannot be resolved to a
|
|
51
77
|
* syntactic block (unrecognized language, blank/out-of-range line, no node
|
|
52
78
|
* begins on line N such as a lone closing delimiter, or the resolved block has
|
|
53
|
-
* a syntax error). Names the offending line
|
|
54
|
-
* concrete-line form
|
|
79
|
+
* a syntax error). Names the offending line, steers back to an explicit
|
|
80
|
+
* concrete-line form, and — when `fileLines` is provided — appends a
|
|
81
|
+
* {@link formatAnchoredContext} preview of the file around the anchor line.
|
|
55
82
|
*/
|
|
56
|
-
export function blockUnresolvedMessage(
|
|
83
|
+
export function blockUnresolvedMessage(
|
|
84
|
+
line: number,
|
|
85
|
+
op: "replace" | "delete" | "insert_after" = "replace",
|
|
86
|
+
fileLines?: readonly string[],
|
|
87
|
+
): string {
|
|
57
88
|
const phrase =
|
|
58
89
|
op === "delete"
|
|
59
90
|
? `delete block ${line}`
|
|
@@ -66,11 +97,15 @@ export function blockUnresolvedMessage(line: number, op: "replace" | "delete" |
|
|
|
66
97
|
: op === "insert_after"
|
|
67
98
|
? `\`insert after M:\` with the block's explicit last line`
|
|
68
99
|
: `\`replace ${line}..M:\` with the block's explicit end line`;
|
|
69
|
-
|
|
100
|
+
let message =
|
|
70
101
|
`\`${phrase}\` could not resolve a syntactic block beginning on line ${line}. ` +
|
|
71
102
|
`The language may be unsupported, the line may be blank or a closing delimiter, or the block may not parse. ` +
|
|
72
|
-
`Use ${fallback} instead
|
|
73
|
-
)
|
|
103
|
+
`Use ${fallback} instead.`;
|
|
104
|
+
if (fileLines) {
|
|
105
|
+
const context = formatAnchoredContext([line], fileLines);
|
|
106
|
+
if (context.length > 0) message += `\n\n${context.join("\n")}`;
|
|
107
|
+
}
|
|
108
|
+
return message;
|
|
74
109
|
}
|
|
75
110
|
|
|
76
111
|
/**
|
package/src/mismatch.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* plus a couple of lines of surrounding context. The {@link MismatchError}
|
|
7
7
|
* formats this into a message at construction time.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { HL_FILE_HASH_EXAMPLES, HL_FILE_HASH_SEP, HL_FILE_PREFIX, HL_FILE_SUFFIX } from "./format";
|
|
10
|
+
import { formatAnchoredContext } from "./messages";
|
|
11
11
|
|
|
12
12
|
const LINE_REF_RE = /^\s*[>+\-*]*\s*(\d+)(?::.*)?\s*$/;
|
|
13
13
|
/** Format the required-shape diagnostic shown when a line reference is malformed. */
|
|
@@ -46,17 +46,6 @@ export interface MismatchDetails {
|
|
|
46
46
|
hashRecognized?: boolean;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function getMismatchDisplayLines(anchorLines: readonly number[], fileLines: string[]): number[] {
|
|
50
|
-
const displayLines = new Set<number>();
|
|
51
|
-
for (const line of anchorLines) {
|
|
52
|
-
if (line < 1 || line > fileLines.length) continue;
|
|
53
|
-
const lo = Math.max(1, line - MISMATCH_CONTEXT);
|
|
54
|
-
const hi = Math.min(fileLines.length, line + MISMATCH_CONTEXT);
|
|
55
|
-
for (let lineNum = lo; lineNum <= hi; lineNum++) displayLines.add(lineNum);
|
|
56
|
-
}
|
|
57
|
-
return [...displayLines].sort((a, b) => a - b);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
49
|
/**
|
|
61
50
|
* Raised when a hashline section's snapshot tag doesn't match the live file's
|
|
62
51
|
* content (and recovery, if configured, declined the merge). Carries the
|
|
@@ -113,19 +102,10 @@ export class MismatchError extends Error {
|
|
|
113
102
|
}
|
|
114
103
|
|
|
115
104
|
static formatMessage(details: MismatchDetails): string {
|
|
116
|
-
const anchorSet = new Set(details.anchorLines ?? []);
|
|
117
105
|
const lines = MismatchError.rejectionHeader(details);
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
lines.push("");
|
|
121
|
-
let previous = -1;
|
|
122
|
-
for (const lineNum of displayLines) {
|
|
123
|
-
if (previous !== -1 && lineNum > previous + 1) lines.push("...");
|
|
124
|
-
previous = lineNum;
|
|
125
|
-
const text = details.fileLines[lineNum - 1] ?? "";
|
|
126
|
-
const marker = anchorSet.has(lineNum) ? "*" : " ";
|
|
127
|
-
lines.push(`${marker}${formatNumberedLine(lineNum, text)}`);
|
|
128
|
-
}
|
|
106
|
+
const context = formatAnchoredContext(details.anchorLines ?? [], details.fileLines);
|
|
107
|
+
if (context.length === 0) return lines.join("\n");
|
|
108
|
+
lines.push("", ...context);
|
|
129
109
|
return lines.join("\n");
|
|
130
110
|
}
|
|
131
111
|
}
|
package/src/prompt.md
CHANGED
|
@@ -11,7 +11,7 @@ Every file section starts with `[PATH#TAG]`. `TAG` is the 4-hex snapshot tag fro
|
|
|
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 syntactic block that BEGINS on line N (
|
|
14
|
+
`insert after block N:` — insert the body rows after the END of the syntactic block that BEGINS on line N. Point N at the line that OPENS the construct (the `if`/`function`/`def`/`{`-bearing line), not the closing delimiter / last visible line; if you have the last line, use plain `insert after M:` instead.
|
|
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:`).
|
|
@@ -36,6 +36,7 @@ There is NO other body row kind. NEVER write `-old` or a bare/context line. To k
|
|
|
36
36
|
- Keep every range as tight as the change: a range covers 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. Tightness means excluding unchanged lines, not being short: a range where every line genuinely changes is correctly long. Tight ranges bound the blast radius of a stale number: a stale one-line range corrupts one line; a stale wide range shreds every line it spans. This applies to hand-counted `replace N..M` ranges; `replace block N` is exempt — tree-sitter fixes the end.
|
|
37
37
|
- `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`); glance at it to confirm you got what you meant. Use `replace N..M` to change specific lines inside a construct.
|
|
38
38
|
- The resolved span of `replace block N` is EXACTLY the node beginning on line N. A leading decorator, attribute, or doc-comment is a separate node and is NOT included; to take a decorated 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.
|
|
39
|
+
- `insert after block N` follows the same opener-only anchor rule as `replace block N`: N is the first line of the syntactic block, never a line inside it, its closing delimiter, or its last visible line. To append after a closing delimiter you can see, use plain `insert after M:`.
|
|
39
40
|
- 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.
|
|
40
41
|
- 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.
|
|
41
42
|
- 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.
|
|
@@ -125,6 +126,13 @@ replace 2..4:
|
|
|
125
126
|
# RIGHT — touch nothing you keep; the new line is the whole body.
|
|
126
127
|
insert after 2:
|
|
127
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()
|
|
128
136
|
</anti-patterns>
|
|
129
137
|
|
|
130
138
|
<critical>
|