fix(hashline): restored balance-validated boundary repair in applyEdits

- Replacement hunks now drop duplicated closing delimiters restated in the payload but surviving just outside the range.
- Ranges that swallow a structural closer the payload omits now spare that closer instead of deleting it.
- Each repair surfaces a `delimiter-balance` warning through `ApplyResult.warnings`.
- Brackets inside strings, template literals, and comments are skipped to avoid false positives.
This commit is contained in:
can1357
2026-05-29 16:20:57 +02:00
parent b8538faaa2
commit 0bc83f04ff
6 changed files with 498 additions and 5 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Restored automatic repair of `edit` range hunks that break bracket balance — the failure class that previously left a duplicated closing line (a `</>` / `);` / `}` echoed just below the range) or dropped one (the range swallowed a `});` the payload never restated), leaving the file syntactically broken until a follow-up edit. The hashline applier now normalizes each replacement so its payload preserves the deleted region's delimiter balance, dropping a duplicated bordering closer or sparing a deleted one, and surfaces a warning on the tool result. Always on and balance-validated (no `edit.hashlineAutoDropPureInsertDuplicates` setting); see `@oh-my-pi/hashline` for the contract.
## [15.5.12] - 2026-05-29
### Added
@@ -313,14 +313,20 @@ describe("hashline parser — range-anchor syntax", () => {
expect(applyDiff(suffixSource, suffixDiff)).toBe(["new();", "// one", "// two", "// one", "// two"].join("\n"));
});
it("keeps duplicated structural replacement boundaries literal", () => {
it("de-duplicates structural replacement boundaries (balance-validated)", () => {
// `1 1` replaces `old();` but the payload also restates the `};` that
// survives at line 2 — a duplicate close that would unbalance braces.
const suffixSource = ["old();", "};"].join("\n");
const suffixDiff = [`${sameLineRange(tag(1, "old();"))}`, repl("new();"), repl("};")].join("\n");
expect(applyDiff(suffixSource, suffixDiff)).toBe(["new();", "};", "};"].join("\n"));
expect(applyDiff(suffixSource, suffixDiff)).toBe(["new();", "};"].join("\n"));
// Mirror case at the leading edge.
const prefixSource = ["};", "old();"].join("\n");
const prefixDiff = [`${sameLineRange(tag(2, "old();"))}`, repl("};"), repl("new();")].join("\n");
expect(applyDiff(prefixSource, prefixDiff)).toBe(["};", "};", "new();"].join("\n"));
expect(applyDiff(prefixSource, prefixDiff)).toBe(["};", "new();"].join("\n"));
const result = applyHashlineEdits(suffixSource, parseHashline(suffixDiff).edits);
expect(result.warnings?.some(w => /delimiter-balance/.test(w))).toBe(true);
});
it("keeps duplicated single non-structural replacement boundaries literal", () => {