jeopi-hashline 16.4.2 → 16.4.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/CHANGELOG.md +7 -0
- package/dist/types/messages.d.ts +17 -0
- package/package.json +1 -1
- package/src/apply.ts +51 -1
- package/src/messages.ts +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.4.3] - 2026-07-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Rejected ambiguous swaps that risk silent deletion of range boundaries: a one-sided boundary echo (payload restates the keeper line just outside the range) is only auto-repaired when the payload is long enough to be the widened range's full content — a shorter payload could instead mean the range itself shifted by the echo, which keeps the far boundary line(s) the repair would otherwise silently delete. The edit is now rejected with a message telling the author to re-issue it. Ported from oh-my-pi (upstream `e45796908`).
|
|
10
|
+
- Prevented ambiguous auto-repairing of structural closing lines when payload placement is unclear: sparing a deleted closer re-inserts it after the payload, which claims the payload belongs inside the block the closer terminates. That claim now requires evidence — the payload carries the closer's unmatched opener, or its indentation sits deeper than the closer — otherwise the edit is rejected instead of guessing whether the new content belongs before or after the closer. Ported from oh-my-pi (upstream `e45796908`).
|
|
11
|
+
|
|
5
12
|
## [16.2.14] - 2026-07-02
|
|
6
13
|
|
|
7
14
|
### Changed
|
package/dist/types/messages.d.ts
CHANGED
|
@@ -49,6 +49,23 @@ export declare function insertAfterBlockCloserLoweredWarning(line: number): stri
|
|
|
49
49
|
* applying with a warning beats failing the patch.
|
|
50
50
|
*/
|
|
51
51
|
export declare function insertAfterBlockUnresolvedLoweredWarning(line: number): string;
|
|
52
|
+
/**
|
|
53
|
+
* A one-sided boundary echo whose payload is too short to be the widened
|
|
54
|
+
* range's full content: dropping the echo deletes range line(s) the payload
|
|
55
|
+
* never restates (the "widened range" reading), while the "range shifted by
|
|
56
|
+
* the echo" reading keeps them. The readings produce different files, so the
|
|
57
|
+
* edit is rejected instead of repaired.
|
|
58
|
+
*/
|
|
59
|
+
export declare function ambiguousBoundaryEchoMessage(startLine: number, endLine: number, side: "leading" | "trailing", count: number): string;
|
|
60
|
+
/**
|
|
61
|
+
* A replacement range deletes trailing structural closer(s) the payload never
|
|
62
|
+
* restates, and nothing anchors the payload inside the block those closers
|
|
63
|
+
* terminate: the payload has no unmatched opener for them and its indentation
|
|
64
|
+
* is not deeper than the closer. Sparing the closer would have to guess
|
|
65
|
+
* whether the payload belongs before it (inside the block) or after it (a
|
|
66
|
+
* sibling), so the edit is rejected instead of repaired.
|
|
67
|
+
*/
|
|
68
|
+
export declare function ambiguousCloserSpareMessage(startLine: number, endLine: number, closerLine: number, count: number): string;
|
|
52
69
|
/**
|
|
53
70
|
* Internal invariant: `applyEdits` received an unresolved `replace_block N:`
|
|
54
71
|
* edit; `resolveBlockEdits` must run first. Wiring bug, not authored input.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "jeopi-hashline",
|
|
4
|
-
"version": "16.4.
|
|
4
|
+
"version": "16.4.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://github.com/akillness/jeopi",
|
|
7
7
|
"author": "Can Boluk",
|
package/src/apply.ts
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
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 {
|
|
10
|
+
import {
|
|
11
|
+
afterInsertLandingShiftWarning,
|
|
12
|
+
ambiguousBoundaryEchoMessage,
|
|
13
|
+
ambiguousCloserSpareMessage,
|
|
14
|
+
blockInsertLandingShiftWarning,
|
|
15
|
+
UNRESOLVED_BLOCK_INTERNAL,
|
|
16
|
+
} from "./messages";
|
|
11
17
|
import { cloneCursor } from "./tokenizer";
|
|
12
18
|
import type { Anchor, ApplyResult, Cursor, Edit } from "./types";
|
|
13
19
|
|
|
@@ -702,6 +708,12 @@ function describeBoundaryRepair(group: ReplacementGroup, action: string): string
|
|
|
702
708
|
* carries no delimiter-balance signal itself, such as a JSX `</section>` close.
|
|
703
709
|
* The dropped lines must keep the already-balanced result balanced, and must
|
|
704
710
|
* not consume the whole payload.
|
|
711
|
+
*
|
|
712
|
+
* A detected echo is only *repairable* when the payload is long enough to be
|
|
713
|
+
* the widened range's full content (`payload ≥ range + echo`). Shorter
|
|
714
|
+
* payloads are ambiguous — the echo may instead mean the range itself was
|
|
715
|
+
* shifted by the echo, which keeps the far boundary line(s) the repair would
|
|
716
|
+
* delete — and the caller rejects the edit instead of guessing.
|
|
705
717
|
*/
|
|
706
718
|
function findOneSidedBoundaryEcho(
|
|
707
719
|
group: ReplacementGroup,
|
|
@@ -800,6 +812,12 @@ function slotPatchDelta(slot: RepairSlot, fileLines: readonly string[]): Delimit
|
|
|
800
812
|
* deleted is only kept when the patch as a whole is missing it — never when
|
|
801
813
|
* another hunk already removed the matching opener. Returns the repaired edits
|
|
802
814
|
* plus one warning per repaired group.
|
|
815
|
+
*
|
|
816
|
+
* Repairs fire only when exactly one reading explains the mistake. When the
|
|
817
|
+
* evidence is ambiguous — a one-sided echo whose payload is too short for the
|
|
818
|
+
* widened range, or a spared closer the payload neither opens nor indents
|
|
819
|
+
* into — the function throws instead of guessing, so the author re-issues the
|
|
820
|
+
* edit rather than shipping silently corrupted content.
|
|
803
821
|
*/
|
|
804
822
|
function repairReplacementBoundaries(
|
|
805
823
|
edits: readonly AppliedEdit[],
|
|
@@ -842,6 +860,15 @@ function repairReplacementBoundaries(
|
|
|
842
860
|
if (balanceIsZero(delta)) {
|
|
843
861
|
const oneSided = findOneSidedBoundaryEcho(group, fileLines);
|
|
844
862
|
if (oneSided) {
|
|
863
|
+
// A payload shorter than range+echo cannot be the widened
|
|
864
|
+
// range's full content: the repair would delete range line(s)
|
|
865
|
+
// the payload never restates, while the "shifted range"
|
|
866
|
+
// reading keeps them. Reject rather than guess.
|
|
867
|
+
if (group.payload.length < group.deleteIndices.length + oneSided.count) {
|
|
868
|
+
throw new Error(
|
|
869
|
+
ambiguousBoundaryEchoMessage(group.startLine, group.endLine, oneSided.side, oneSided.count),
|
|
870
|
+
);
|
|
871
|
+
}
|
|
845
872
|
const trimmed =
|
|
846
873
|
oneSided.side === "leading"
|
|
847
874
|
? inserts.slice(oneSided.count)
|
|
@@ -934,6 +961,29 @@ function repairReplacementBoundaries(
|
|
|
934
961
|
insertedLineMaps,
|
|
935
962
|
);
|
|
936
963
|
if (droppedClosers) {
|
|
964
|
+
// Sparing a closer re-inserts it *after* the payload, which claims
|
|
965
|
+
// the payload lives inside the block the closer terminates. That
|
|
966
|
+
// claim needs evidence: the payload carries the closer's unmatched
|
|
967
|
+
// opener itself, or its indentation sits deeper than the closer.
|
|
968
|
+
// Without either, "before or after the closer" is a coin flip —
|
|
969
|
+
// reject rather than guess (e.g. a statement swapped onto a lone
|
|
970
|
+
// `}` at the closer's own depth belongs after the block).
|
|
971
|
+
const keptIndent = leadingIndent(fileLines[droppedClosers.startLine - 1] ?? "");
|
|
972
|
+
const payloadIndent = bodyTargetIndent(slot.group.payload);
|
|
973
|
+
const payloadOpens = balanceCovers(
|
|
974
|
+
computeDelimiterBalance(slot.group.payload),
|
|
975
|
+
balanceNegate(droppedClosers.balance),
|
|
976
|
+
);
|
|
977
|
+
if (!payloadOpens && !(payloadIndent !== undefined && isIndentDeeper(payloadIndent, keptIndent))) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
ambiguousCloserSpareMessage(
|
|
980
|
+
slot.group.startLine,
|
|
981
|
+
slot.group.endLine,
|
|
982
|
+
droppedClosers.startLine,
|
|
983
|
+
droppedClosers.count,
|
|
984
|
+
),
|
|
985
|
+
);
|
|
986
|
+
}
|
|
937
987
|
warnings.push(
|
|
938
988
|
describeBoundaryRepair(
|
|
939
989
|
slot.group,
|
package/src/messages.ts
CHANGED
|
@@ -108,6 +108,55 @@ export function insertAfterBlockCloserLoweredWarning(line: number): string {
|
|
|
108
108
|
export function insertAfterBlockUnresolvedLoweredWarning(line: number): string {
|
|
109
109
|
return `\`INS.BLK.POST ${line}:\` could not resolve a syntactic block on line ${line}, so it was applied as plain \`INS.POST ${line}:\`. Verify the landing line; anchor on a line that OPENS a construct.`;
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* A one-sided boundary echo whose payload is too short to be the widened
|
|
113
|
+
* range's full content: dropping the echo deletes range line(s) the payload
|
|
114
|
+
* never restates (the "widened range" reading), while the "range shifted by
|
|
115
|
+
* the echo" reading keeps them. The readings produce different files, so the
|
|
116
|
+
* edit is rejected instead of repaired.
|
|
117
|
+
*/
|
|
118
|
+
export function ambiguousBoundaryEchoMessage(
|
|
119
|
+
startLine: number,
|
|
120
|
+
endLine: number,
|
|
121
|
+
side: "leading" | "trailing",
|
|
122
|
+
count: number,
|
|
123
|
+
): string {
|
|
124
|
+
const where =
|
|
125
|
+
side === "leading"
|
|
126
|
+
? `opens by restating the ${count} line(s) just above the range`
|
|
127
|
+
: `ends by restating the ${count} line(s) just below the range`;
|
|
128
|
+
return (
|
|
129
|
+
`\`SWAP ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: the body ${where}, ` +
|
|
130
|
+
`but is too short to be the full final content of the widened range — applying it as-is or ` +
|
|
131
|
+
`auto-repairing would delete range line(s) the body never restates. ` +
|
|
132
|
+
`Re-issue with the range covering exactly the lines that change and the body as their complete ` +
|
|
133
|
+
`final content: drop the restated keeper from the body, or widen the range to consume it.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A replacement range deletes trailing structural closer(s) the payload never
|
|
139
|
+
* restates, and nothing anchors the payload inside the block those closers
|
|
140
|
+
* terminate: the payload has no unmatched opener for them and its indentation
|
|
141
|
+
* is not deeper than the closer. Sparing the closer would have to guess
|
|
142
|
+
* whether the payload belongs before it (inside the block) or after it (a
|
|
143
|
+
* sibling), so the edit is rejected instead of repaired.
|
|
144
|
+
*/
|
|
145
|
+
export function ambiguousCloserSpareMessage(
|
|
146
|
+
startLine: number,
|
|
147
|
+
endLine: number,
|
|
148
|
+
closerLine: number,
|
|
149
|
+
count: number,
|
|
150
|
+
): string {
|
|
151
|
+
const closers = count === 1 ? `line ${closerLine}` : `lines ${closerLine}-${closerLine + count - 1}`;
|
|
152
|
+
return (
|
|
153
|
+
`\`SWAP ${startLine}${HL_RANGE_SEP}${endLine}:\` rejected: the range deletes the closing-delimiter ` +
|
|
154
|
+
`${closers} but the body never restates it, and the body claims no position inside that block ` +
|
|
155
|
+
`(no unmatched opener, indentation not deeper than the closer) — whether the new content belongs ` +
|
|
156
|
+
`before or after the closer is ambiguous. Restate the closer in the body at the intended position, ` +
|
|
157
|
+
`or use \`INS.PRE ${closerLine}:\` / \`INS.POST ${closerLine}:\` instead.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
111
160
|
|
|
112
161
|
/**
|
|
113
162
|
* Internal invariant: `applyEdits` received an unresolved `replace_block N:`
|