From 09e54075393faadaac5ea9097df00d0c00c7ea9f Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 26 Apr 2026 11:16:52 +0200 Subject: [PATCH] refactor(coding-agent): renamed atom edit verb from set to splice - Renamed atom tool schema fields and internal `AtomEdit` operation from `set` to `splice` in edit parsing and application. - Adjusted resolver and conflict checks to use `splice` semantics, including file-loc rejection, blank-line deletion handling, and sed precedence. - Updated atom edit documentation and tests to describe and verify the new `splice`-based API. --- packages/coding-agent/src/edit/modes/atom.ts | 46 ++++++------- .../coding-agent/src/prompts/tools/atom.md | 54 ++++++++++------ packages/coding-agent/test/core/atom.test.ts | 64 +++++++++---------- 3 files changed, 88 insertions(+), 76 deletions(-) diff --git a/packages/coding-agent/src/edit/modes/atom.ts b/packages/coding-agent/src/edit/modes/atom.ts index dc9e427d0..92f400615 100644 --- a/packages/coding-agent/src/edit/modes/atom.ts +++ b/packages/coding-agent/src/edit/modes/atom.ts @@ -1,20 +1,20 @@ /** * * Flat locator + verb edit mode backed by hashline anchors. Each entry carries - * one shared `loc` selector plus one or more verbs (`pre`, `set`, `post`). + * one shared `loc` selector plus one or more verbs (`pre`, `splice`, `post`). * The runtime resolves those verbs into internal anchor-scoped edits and still * reuses hashline's staleness scheme (`computeLineHash`) verbatim. * * External shapes (one entry): - * { path, loc: "5th", set: ["..."] } + * { path, loc: "5th", splice: ["..."] } * { path, loc: "5th", pre: ["..."] } * { path, loc: "5th", post: ["..."] } - * { path, loc: "5th", pre: [...], set: [...], post: [...] } + * { path, loc: "5th", pre: [...], splice: [...], post: [...] } * { path, loc: "$", pre: [...] } // prepend to file * { path, loc: "$", post: [...] } // append to file * { path, loc: "$", sed: "s/foo/bar/" } // sed on every line * - * `set: []` on a single-anchor locator deletes that line. `set:[""]` preserves + * `splice: []` on a single-anchor locator deletes that line. `splice:[""]` preserves * a blank line. Line ranges are not supported. * in the same entry. * @@ -62,7 +62,7 @@ export const atomEditSchema = Type.Object( description: 'edit location: "1ab", "$", or path override like "a.ts:1ab"', examples: ["1ab", "$", "src/foo.ts:1ab"], }), - set: Type.Optional(textSchema), + splice: Type.Optional(textSchema), pre: Type.Optional(textSchema), post: Type.Optional(textSchema), sed: Type.Optional( @@ -91,7 +91,7 @@ export type AtomParams = Static; // ═══════════════════════════════════════════════════════════════════════════ export type AtomEdit = - | { op: "set"; pos: Anchor; lines: string[] } + | { op: "splice"; pos: Anchor; lines: string[] } | { op: "pre"; pos: Anchor; lines: string[] } | { op: "post"; pos: Anchor; lines: string[] } | { op: "del"; pos: Anchor } @@ -112,7 +112,7 @@ export interface SedSpec { // Param guards // ═══════════════════════════════════════════════════════════════════════════ -const ATOM_VERB_KEYS = ["set", "pre", "post", "sed"] as const; +const ATOM_VERB_KEYS = ["splice", "pre", "post", "sed"] as const; type AtomOptionalKey = "loc" | (typeof ATOM_VERB_KEYS)[number]; const ATOM_OPTIONAL_KEYS = ["loc", ...ATOM_VERB_KEYS] as const satisfies readonly AtomOptionalKey[]; @@ -394,8 +394,8 @@ function resolveAtomToolEdit(edit: AtomToolEdit, editIndex = 0): AtomEdit[] { const resolved: AtomEdit[] = []; if (loc.kind === "file") { - if (entry.set !== undefined) { - throw new Error(`Edit ${editIndex}: loc "$" supports pre, post, and sed (not set).`); + if (entry.splice !== undefined) { + throw new Error(`Edit ${editIndex}: loc "$" supports pre, post, and sed (not splice).`); } if (entry.pre !== undefined) { resolved.push({ op: "prepend_file", lines: hashlineParseText(entry.pre) }); @@ -413,28 +413,28 @@ function resolveAtomToolEdit(edit: AtomToolEdit, editIndex = 0): AtomEdit[] { if (entry.pre !== undefined) { resolved.push({ op: "pre", pos: loc.pos, lines: hashlineParseText(entry.pre) }); } - if (entry.set !== undefined) { - if (Array.isArray(entry.set) && entry.set.length === 0) { - // Models often default `set: []` alongside other verbs (notably `sed`). + if (entry.splice !== undefined) { + if (Array.isArray(entry.splice) && entry.splice.length === 0) { + // Models often default `splice: []` alongside other verbs (notably `sed`). // Treating that combination as an explicit `del` produces a confusing // `Conflicting ops` error. When another mutating verb is present, drop - // the empty `set` instead of treating it as a deletion. + // the empty `splice` instead of treating it as a deletion. if (entry.sed === undefined) { resolved.push({ op: "del", pos: loc.pos }); } } else { - resolved.push({ op: "set", pos: loc.pos, lines: hashlineParseText(entry.set) }); + resolved.push({ op: "splice", pos: loc.pos, lines: hashlineParseText(entry.splice) }); } } if (entry.post !== undefined) { resolved.push({ op: "post", pos: loc.pos, lines: hashlineParseText(entry.post) }); } if (entry.sed !== undefined) { - const setIsExplicitReplacement = Array.isArray(entry.set) && entry.set.length > 0; - // Models often duplicate intent by sending both an explicit `set` and a + const spliceIsExplicitReplacement = Array.isArray(entry.splice) && entry.splice.length > 0; + // Models often duplicate intent by sending both an explicit `splice` and a // matching `sed`. The explicit replacement wins; the redundant `sed` would // otherwise trigger a confusing `Conflicting ops` rejection. - if (!setIsExplicitReplacement) { + if (!spliceIsExplicitReplacement) { const spec = parseSedExpression(entry.sed, editIndex); resolved.push({ op: "sed", pos: loc.pos, spec, expression: entry.sed }); } @@ -448,7 +448,7 @@ function resolveAtomToolEdit(edit: AtomToolEdit, editIndex = 0): AtomEdit[] { function* getAtomAnchors(edit: AtomEdit): Iterable { switch (edit.op) { - case "set": + case "splice": case "pre": case "post": case "del": @@ -524,16 +524,16 @@ function validateAtomAnchors(edits: AtomEdit[], fileLines: string[], warnings: s } function validateNoConflictingAnchorOps(edits: AtomEdit[]): void { - // For each anchor line, at most one mutating op (set/del). + // For each anchor line, at most one mutating op (splice/del). // `pre`/`post` (insert ops) may coexist with them — they don't mutate the anchor line. const mutatingPerLine = new Map(); for (const edit of edits) { - if (edit.op !== "set" && edit.op !== "del" && edit.op !== "sed") continue; + if (edit.op !== "splice" && edit.op !== "del" && edit.op !== "sed") continue; const existing = mutatingPerLine.get(edit.pos.line); if (existing) { throw new Error( `Conflicting ops on anchor line ${edit.pos.line}: \`${existing}\` and \`${edit.op}\`. ` + - `At most one of set/del/sed is allowed per anchor.`, + `At most one of splice/del/sed is allowed per anchor.`, ); } mutatingPerLine.set(edit.pos.line, edit.op); @@ -548,7 +548,7 @@ function maybeAutocorrectEscapedTabIndentation(edits: AtomEdit[], warnings: stri const enabled = Bun.env.PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS !== "0"; if (!enabled) return; for (const edit of edits) { - if (edit.op !== "set" && edit.op !== "pre" && edit.op !== "post") continue; + if (edit.op !== "splice" && edit.op !== "pre" && edit.op !== "post") continue; if (edit.lines.length === 0) continue; const hasEscapedTabs = edit.lines.some(line => line.includes("\\t")); if (!hasEscapedTabs) continue; @@ -666,7 +666,7 @@ export function applyAtomEdits( replacementSet = true; anchorDeleted = true; break; - case "set": + case "splice": replacement = edit.lines.length === 0 ? [""] : [...edit.lines]; replacementSet = true; anchorMutated = true; diff --git a/packages/coding-agent/src/prompts/tools/atom.md b/packages/coding-agent/src/prompts/tools/atom.md index 1fa1340c7..74079cc3b 100644 --- a/packages/coding-agent/src/prompts/tools/atom.md +++ b/packages/coding-agent/src/prompts/tools/atom.md @@ -11,18 +11,18 @@ Each entry has one shared locator plus one or more verbs: - `loc: "a.ts:160sr"` — cross-file override inside the locator Verbs: -- `set: ["…"]` — replace the anchor line -- `pre: ["…"]` — insert before the anchor line (or at BOF when `loc:"^"`) -- `post: ["…"]` — insert after the anchor line (or at EOF when `loc:"$"`) -- `sed: "s/foo/bar/"` — sed-style substitution applied to the anchor line. **Prefer this over `set` for token-level changes** +- `splice: […]`: lines are spliced in at the anchor. +- `pre: […]`: prepend before the anchor line (or at BOF if `loc=$`) +- `post: […]`: append after the anchor line (or at EOF if `loc=$`) +- `sed: "s/foo/bar/"` — sed-style substitution applied to the anchor line. **Prefer this over `splice` for token-level changes** Flags: `g` (all occurrences), `i` (case-insensitive), `F` (literal/fixed-string, no regex). Delimiter is whatever character follows `s`. You **MUST** keep the pattern as short as possible. Combination rules: -- On a single-anchor `loc`, you may combine `pre`, `set`, and `post` in the same entry. -- `set: []` on a single-anchor `loc` deletes that line. -- `set:[""]` is **not** delete — it replaces the line with a blank line. +- On a single-anchor `loc`, you may combine `pre`, `splice`, and `post` in the same entry. +- `splice: []` on a single-anchor `loc` deletes that line. +- `splice:[""]` is **not** delete — it replaces the line with a blank line. @@ -39,17 +39,17 @@ All examples below reference the same file: {{hline 8 "}"}} ``` -# Replace a line with `set` -`{path:"a.ts",edits:[{loc:{{href 1 "const tag = \"BAD\";"}},set:["const tag = \"OK\";"]}]}` +# Replace a line with `splice` +`{path:"a.ts",edits:[{loc:{{href 1 "const tag = \"BAD\";"}},splice:["const tag = \"OK\";"]}]}` -# Combine `pre` + `set` + `post` in one entry -`{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},pre:["\tvalidate();"],set:["\tif (!x) {"],post:["\t\tlog();"]}]}` +# Combine `pre` + `splice` + `post` in one entry +`{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},pre:["\tvalidate();"],splice:["\tif (!x) {"],post:["\t\tlog();"]}]}` -# Delete a line with `set: []` -`{path:"a.ts",edits:[{loc:{{href 7 "\treturn null;"}},set:[]}]}` +# Delete a line with `splice: []` +`{path:"a.ts",edits:[{loc:{{href 7 "\treturn null;"}},splice:[]}]}` -# Preserve a blank line with `set:[""]` -`{path:"a.ts",edits:[{loc:{{href 2 ""}},set:[""]}]}` +# Preserve a blank line with `splice:[""]` +`{path:"a.ts",edits:[{loc:{{href 2 ""}},splice:[""]}]}` # Insert before / after a line `{path:"a.ts",edits:[{loc:{{href 3 "function beta(x) {"}},pre:["function gamma() {","\tvalidate();","}",""]}]}` @@ -67,20 +67,32 @@ Use the `F` flag to disable regex; the delimiter can be any non-alphanumeric cha `{path:"a.ts",edits:[{loc:"$",post:["","export const VERSION = \"1.0.0\";"]}]}` # Cross-file override inside `loc` -`{path:"a.ts",edits:[{loc:"b.ts:{{href 1 "const tag = \"BAD\";"}}",set:["const tag = \"OK\";"]}]}` +`{path:"a.ts",edits:[{loc:"b.ts:{{href 1 "const tag = \"BAD\";"}}",splice:["const tag = \"OK\";"]}]}` + +# WRONG: retyping unchanged neighbors inside `splice` duplicates them +`{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},splice:["\tif (x && ready) {","\t\treturn parse(data) ?? fallback;","\t\t//unreachable"]}]}` +The 2nd array element matches existing line 5, which is **not** overwritten, it shifts, so return statement ends up duplicated. + +# RIGHT: split into separate edits +- `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},sed:"s/x/x \\&\\& ready/"},{loc:{{href 5 "\t\treturn parse(data) ?? fallback;"}},post:["\t\t//unreachable"]}]}` +OR +- `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},splice:["\tif (x && ready) {"]},{loc:{{href 5 "\t\treturn parse(data) ?? fallback;"}},splice:["\t\treturn parse(data) ?? fallback;","\t\t//unreachable"]}]}` - Make the minimum exact edit. - Copy the full anchors exactly as shown by `read/grep` (for example `160sr`, not just `sr`). - `loc` chooses the target. Verbs describe what to do there. -- On a single-anchor `loc`, you may combine `pre`, `set`, and `post`. +- On a single-anchor `loc`, you may combine `pre`, `splice`, and `post`. - `loc:"$"` operates on the whole file: `pre` prepends, `post` appends, `sed` runs across every line. -- `set: []` deletes the anchored line. `set:[""]` preserves a blank line. +- `splice: []` deletes the anchored line. `splice:[""]` preserves a blank line. - Within a single request you may submit edits in any order — the runtime applies them bottom-up so they don't shift each other. After any request that mutates a file, anchors below the mutation are stale on disk; re-read before issuing more edits to that file. -- `set` operations target the current file content only. Do not try to reference old line text after the file has changed. -- For token-level edits, prefer `sed` over `set`. The `loc` anchor already pins the line — repeating the entire line in a `set` array invites hallucinated content. Use the smallest `sed` pattern that uniquely identifies the change on that line; do not pad it with surrounding text just to feel safe. -- When you do use `set`, re-read the anchored line first and copy it verbatim, changing only the required token(s). Anchor identity does not verify line content, so a hallucinated replacement will silently corrupt the file. +- `splice` operations target the current file content only. Do not try to reference old line text after the file has changed. +- For **small** in-line edits (renaming a token, flipping an operator, tweaking a literal), prefer `sed` over `splice`. The `loc` anchor already pins the line — repeating the entire line in a `splice` array invites hallucinated content. Use the smallest `sed` pattern that uniquely identifies the change on that line; do not pad it with surrounding text just to feel safe. For multi-line restructuring (wrapping logic, adding new branches, inserting blocks), use `splice`/`pre`/`post` — do **not** stretch `sed` into a rewrite tool. +- When you do use `splice`, re-read the anchored line first and copy it verbatim, changing only the required token(s). Anchor identity does not verify line content, so a hallucinated replacement will silently corrupt the file. +- Anchors are pin points, not region markers. One anchor pins exactly one line. If your change touches N distinct source lines, that is N edits with N anchors — not one big `splice` array intended to cover the whole region. `splice` cannot "replace lines 4 through 7"; it can only splice content in at one anchor. +- You **MUST NOT** include lines in `splice`/`pre`/`post` that already exist immediately adjacent to the anchor in the current file. `splice` does not overwrite the lines below — they shift down — so any neighbor you re-type in your array becomes a duplicate. If your intended replacement contains content that is already on neighboring source lines, split into multiple edits at each real change site instead of one fat `splice`. +- Before issuing a multi-line `splice`, mentally diff each array element against the current file lines at and just below the anchor. Any element that matches a line within ~5 lines of the anchor will become a duplicate after the splice. If you find a match, drop that element and use a separate edit (or `pre`/`post`) at the real change point. - Text content must be literal file content with matching indentation. If the file uses tabs, use real tabs. - You **MUST NOT** use this tool to reformat or clean up unrelated code. diff --git a/packages/coding-agent/test/core/atom.test.ts b/packages/coding-agent/test/core/atom.test.ts index 25d3d0e60..a675edb65 100644 --- a/packages/coding-agent/test/core/atom.test.ts +++ b/packages/coding-agent/test/core/atom.test.ts @@ -16,10 +16,10 @@ function tag(line: number, content: string): Anchor { return { line, hash: computeLineHash(line, content) }; } -describe("applyAtomEdits — set", () => { +describe("applyAtomEdits — splice", () => { it("replaces a single line", () => { const content = "aaa\nbbb\nccc"; - const edits: AtomEdit[] = [{ op: "set", pos: tag(2, "bbb"), lines: ["BBB"] }]; + const edits: AtomEdit[] = [{ op: "splice", pos: tag(2, "bbb"), lines: ["BBB"] }]; const result = applyAtomEdits(content, edits); expect(result.lines).toBe("aaa\nBBB\nccc"); expect(result.firstChangedLine).toBe(2); @@ -27,14 +27,14 @@ describe("applyAtomEdits — set", () => { it("expands one line into many", () => { const content = "aaa\nbbb\nccc"; - const edits: AtomEdit[] = [{ op: "set", pos: tag(2, "bbb"), lines: ["X", "Y", "Z"] }]; + const edits: AtomEdit[] = [{ op: "splice", pos: tag(2, "bbb"), lines: ["X", "Y", "Z"] }]; const result = applyAtomEdits(content, edits); expect(result.lines).toBe("aaa\nX\nY\nZ\nccc"); }); it("rejects on stale hash", () => { const content = "aaa\nbbb\nccc"; - const edits: AtomEdit[] = [{ op: "set", pos: { line: 2, hash: "ZZ" }, lines: ["BBB"] }]; + const edits: AtomEdit[] = [{ op: "splice", pos: { line: 2, hash: "ZZ" }, lines: ["BBB"] }]; expect(() => applyAtomEdits(content, edits)).toThrow(HashlineMismatchError); }); }); @@ -73,11 +73,11 @@ describe("applyAtomEdits — pre/post", () => { expect(result.lines).toBe("aaa\nbbb\nNEW\nccc"); }); - it("pre + post on same anchor coexist with set", () => { + it("pre + post on same anchor coexist with splice", () => { const content = "aaa\nbbb\nccc"; const edits: AtomEdit[] = [ { op: "pre", pos: tag(2, "bbb"), lines: ["B"] }, - { op: "set", pos: tag(2, "bbb"), lines: ["BBB"] }, + { op: "splice", pos: tag(2, "bbb"), lines: ["BBB"] }, { op: "post", pos: tag(2, "bbb"), lines: ["A"] }, ]; const result = applyAtomEdits(content, edits); @@ -139,36 +139,36 @@ describe("resolveAtomToolEdit — loc syntax", () => { expect(result.lines).toBe("PRE\naaa\nBBB\nPOST"); }); - it('loc:"$" rejects set', () => { - expect(() => resolveAtomToolEdit({ loc: "$", set: ["X"] })).toThrow(/supports pre, post, and sed/); + it('loc:"$" rejects splice', () => { + expect(() => resolveAtomToolEdit({ loc: "$", splice: ["X"] })).toThrow(/supports pre, post, and sed/); }); it('loc:"^" is no longer supported', () => { expect(() => resolveAtomToolEdit({ loc: "^", pre: ["ZZZ"] })).toThrow(); }); - it("expands pre + set + post from one entry", () => { + it("expands pre + splice + post from one entry", () => { const content = "aaa\nbbb\nccc"; const loc = `2${computeLineHash(2, "bbb")}`; - const resolved = resolveAtomToolEdit({ loc, pre: ["B"], set: ["BBB"], post: ["A"] }); + const resolved = resolveAtomToolEdit({ loc, pre: ["B"], splice: ["BBB"], post: ["A"] }); const result = applyAtomEdits(content, resolved); expect(result.lines).toBe("aaa\nB\nBBB\nA\nccc"); }); - it("set: [] deletes the anchor line", () => { + it("splice: [] deletes the anchor line", () => { const content = "aaa\nbbb\nccc"; const loc = `2${computeLineHash(2, "bbb")}`; - const resolved = resolveAtomToolEdit({ loc, set: [] }); + const resolved = resolveAtomToolEdit({ loc, splice: [] }); expect(resolved[0]?.op).toBe("del"); const result = applyAtomEdits(content, resolved); expect(result.lines).toBe("aaa\nccc"); }); - it('set:[""] preserves a blank line', () => { + it('splice: [""] preserves a blank line', () => { const content = "aaa\nbbb\nccc"; const loc = `2${computeLineHash(2, "bbb")}`; - const resolved = resolveAtomToolEdit({ loc, set: [""] }); - expect(resolved[0]?.op).toBe("set"); + const resolved = resolveAtomToolEdit({ loc, splice: [""] }); + expect(resolved[0]?.op).toBe("splice"); const result = applyAtomEdits(content, resolved); expect(result.lines).toBe("aaa\n\nccc"); }); @@ -176,16 +176,16 @@ describe("resolveAtomToolEdit — loc syntax", () => { it("ignores null optional verb fields", () => { const content = "aaa\nbbb\nccc"; const loc = `2${computeLineHash(2, "bbb")}`; - const toolEdit = { loc, pre: null, set: "BBB", post: null } as unknown as AtomToolEdit; + const toolEdit = { loc, pre: null, splice: "BBB", post: null } as unknown as AtomToolEdit; const resolved = resolveAtomToolEdit(toolEdit); - expect(resolved).toEqual([{ op: "set", pos: tag(2, "bbb"), lines: ["BBB"] }]); + expect(resolved).toEqual([{ op: "splice", pos: tag(2, "bbb"), lines: ["BBB"] }]); const result = applyAtomEdits(content, resolved); expect(result.lines).toBe("aaa\nBBB\nccc"); }); it("supports path override inside loc", () => { - const resolved = resolveAtomEntryPaths([{ loc: "a.ts:1ab", set: ["X"] }], undefined); + const resolved = resolveAtomEntryPaths([{ loc: "a.ts:1ab", splice: ["X"] }], undefined); expect(resolved[0]?.path).toBe("a.ts"); expect(resolved[0]?.loc).toBe("1ab"); }); @@ -194,7 +194,7 @@ describe("resolveAtomToolEdit — loc syntax", () => { describe("applyAtomEdits — out of range", () => { it("rejects line beyond file length", () => { const content = "aaa\nbbb"; - const edits: AtomEdit[] = [{ op: "set", pos: { line: 99, hash: "ZZ" }, lines: ["x"] }]; + const edits: AtomEdit[] = [{ op: "splice", pos: { line: 99, hash: "ZZ" }, lines: ["x"] }]; expect(() => applyAtomEdits(content, edits)).toThrow(/does not exist/); }); }); @@ -203,7 +203,7 @@ describe("parseAnchor (atom tolerant) + applyAtomEdits", () => { it("surfaces correct anchor + content when the model invents an out-of-alphabet hash", () => { const content = "alpha\nbravo\ncharlie"; // `XG` is not in the alphabet; should be rejected with the actual anchor exposed. - const toolEdit = { path: "a.ts", loc: "2XG", set: ["BRAVO"] }; + const toolEdit = { path: "a.ts", loc: "2XG", splice: ["BRAVO"] }; const resolved = resolveAtomToolEdit(toolEdit); expect(() => applyAtomEdits(content, resolved)).toThrow(HashlineMismatchError); try { @@ -218,26 +218,26 @@ describe("parseAnchor (atom tolerant) + applyAtomEdits", () => { it("surfaces correct anchor + content when the model omits the hash entirely", () => { const content = "alpha\nbravo\ncharlie"; - const toolEdit = { path: "a.ts", loc: "2", set: ["BRAVO"] }; + const toolEdit = { path: "a.ts", loc: "2", splice: ["BRAVO"] }; const resolved = resolveAtomToolEdit(toolEdit); expect(() => applyAtomEdits(content, resolved)).toThrow(HashlineMismatchError); }); it("surfaces correct anchor when the model uses pipe-separator (LINE|content) form", () => { const content = "alpha\nbravo\ncharlie"; - const toolEdit = { path: "a.ts", loc: "2|bravo", set: ["BRAVO"] }; + const toolEdit = { path: "a.ts", loc: "2|bravo", splice: ["BRAVO"] }; const resolved = resolveAtomToolEdit(toolEdit); expect(() => applyAtomEdits(content, resolved)).toThrow(HashlineMismatchError); }); it("throws a usage-style error when no line number can be extracted", () => { - const toolEdit = { path: "a.ts", loc: " if (!x) return;", set: ["x"] }; + const toolEdit = { path: "a.ts", loc: " if (!x) return;", splice: ["x"] }; expect(() => resolveAtomToolEdit(toolEdit)).toThrow(/Could not find a line number/); }); }); describe("atom range locators", () => { - it("resolveAtomToolEdit rejects range loc with set", () => { - expect(() => resolveAtomToolEdit({ loc: "1xx-4yy", set: ["X"] })).toThrow(/does not support line ranges/); + it("resolveAtomToolEdit rejects range loc with splice", () => { + expect(() => resolveAtomToolEdit({ loc: "1xx-4yy", splice: ["X"] })).toThrow(/does not support line ranges/); }); it("resolveAtomToolEdit rejects range loc even when the verb would otherwise be valid", () => { @@ -245,7 +245,7 @@ describe("atom range locators", () => { }); it("resolveAtomEntryPaths still peels off a path override before range validation", () => { - const [resolved] = resolveAtomEntryPaths([{ loc: "a.ts:1xx-4yy", set: ["X"] }], undefined); + const [resolved] = resolveAtomEntryPaths([{ loc: "a.ts:1xx-4yy", splice: ["X"] }], undefined); expect(resolved?.path).toBe("a.ts"); expect(resolved?.loc).toBe("1xx-4yy"); expect(() => resolveAtomToolEdit(resolved!)).toThrow(/does not support line ranges/); @@ -257,7 +257,7 @@ describe("atom range locators", () => { // must not be mistaken for range syntax. const content = "alpha\nbravo\ncharlie"; const loc = `2${computeLineHash(2, "bravo")}| for (let i = 0; i--; ...) {`; - const resolved = resolveAtomToolEdit({ loc, set: ["BRAVO"] }); + const resolved = resolveAtomToolEdit({ loc, splice: ["BRAVO"] }); const result = applyAtomEdits(content, resolved); expect(result.lines).toBe("alpha\nBRAVO\ncharlie"); }); @@ -323,11 +323,11 @@ describe("applyAtomEdits — sed", () => { expect(result.lines).toBe("aaa\nBEFORE\nFOO\nAFTER\nccc"); }); - it("prefers set when sed is also present on the same anchor", () => { + it("prefers splice when sed is also present on the same anchor", () => { const content = "aaa\nfoo\nccc"; const loc = `2${computeLineHash(2, "foo")}`; - const resolved = resolveAtomToolEdit({ loc, set: ["X"], sed: "s/foo/Y/" }); - // Models sometimes duplicate intent on the same line; the explicit `set` + const resolved = resolveAtomToolEdit({ loc, splice: ["X"], sed: "s/foo/Y/" }); + // Models sometimes duplicate intent on the same line; the explicit `splice` // wins and the redundant `sed` is dropped silently. const result = applyAtomEdits(content, resolved); expect(result.lines).toBe("aaa\nX\nccc"); @@ -376,10 +376,10 @@ describe("applyAtomEdits — sed", () => { expect(() => applyAtomEdits(content, resolved)).toThrow(/failed to compile/); }); - it("treats empty `set: []` as no-op when paired with sed", () => { + it("treats empty `splice: []` as no-op when paired with sed", () => { const content = "aaa\nfoo\nccc"; const loc = `2${computeLineHash(2, "foo")}`; - const resolved = resolveAtomToolEdit({ loc, set: [], sed: "s/foo/FOO/" }); + const resolved = resolveAtomToolEdit({ loc, splice: [], sed: "s/foo/FOO/" }); const result = applyAtomEdits(content, resolved); expect(result.lines).toBe("aaa\nFOO\nccc"); });