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.
This commit is contained in:
@@ -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<typeof atomEditParamsSchema>;
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
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<Anchor> {
|
||||
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<number, string>();
|
||||
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;
|
||||
|
||||
@@ -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.
|
||||
</operations>
|
||||
|
||||
<examples>
|
||||
@@ -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"]}]}`
|
||||
</examples>
|
||||
|
||||
<critical>
|
||||
- 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.
|
||||
</critical>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user