feat(coding-agent): introduced keyed union format for chunk operations

- Changed chunk edit operation format from flat `op` field to nested keyed union structure with operation name as object key.
- Added parseKeyedChunkOp() function to normalize both keyed and legacy flat operation formats for backward compatibility.
- Enhanced chunk selector sanitization with filename prefix stripping and case-insensitive CRC checksum matching.
- Updated chunk edit prompt documentation and test suite to reflect new nested operation JSON structure.
This commit is contained in:
can1357
2026-04-06 18:50:08 +02:00
parent 266aabf29f
commit 79e2f8b2b1
6 changed files with 281 additions and 173 deletions
+1 -1
View File
@@ -1978,4 +1978,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
## [0.9.4] - 2025-11-26
Initial release with multi-provider LLM support.
Initial release with multi-provider LLM support.
+3 -1
View File
@@ -1,7 +1,6 @@
# Changelog
## [Unreleased]
### Added
- Added `new_segment` option to `init_experiment` to force a new segment even when contract fields match, enabling re-initialization with unchanged parameters
@@ -15,6 +14,9 @@
### Changed
- Changed chunk edit operation format from flat `{ "op": "splice", ... }` to keyed `{ "splice": { ... } }` format for improved clarity and schema validation
- Updated chunk edit schema to use discriminated union types with operation-specific field requirements (insert ops no longer require CRC, mutation ops require CRC)
- Improved chunk selector sanitization to strip filename prefixes and normalize checksums to uppercase, handling model output variations
- Changed `log_experiment` revert behavior to only restore run-modified files instead of reverting entire working tree, preserving pre-existing uncommitted changes
- Changed `init_experiment` error message for pending unlogged runs to include command, metric, and pass/fail status for better context
- Changed `init_experiment` to detect when contract fields match current state and skip re-initialization (no-op) unless `new_segment=true`
+123 -45
View File
@@ -104,45 +104,84 @@ const patchEditSchema = Type.Object({
diff: Type.Optional(Type.String({ description: "Diff hunks (update) or full content (create)" })),
});
const CHUNK_EDIT_OPS_BASE = [
"append_child",
"prepend_child",
"append_sibling",
"prepend_sibling",
"replace",
"delete",
] as const;
const chunkEditOperationSchemaBaseProps = {
sel: Type.Optional(Type.String({ description: "Chunk path selector" })),
crc: Type.Optional(Type.String({ description: "Chunk checksum for staleness validation" })),
const chunkEditSelProp = { sel: Type.Optional(Type.String({ description: "Chunk path selector" })) } as const;
const chunkEditCrcProp = { crc: Type.String({ description: "Chunk checksum for staleness validation" }) } as const;
const chunkEditContentProp = {
content: Type.Union([Type.String(), Type.Array(Type.String())], { description: "Inserted or replacement content" }),
} as const;
const chunkEditOptionalContentProp = {
content: Type.Optional(
Type.Union([Type.String(), Type.Array(Type.String())], { description: "Inserted or replacement content" }),
Type.Union([Type.String(), Type.Array(Type.String())], {
description: "Replacement content (empty string to delete lines)",
}),
),
} as const;
const chunkEditOperationSchemaWithoutSplice = Type.Object({
op: StringEnum(CHUNK_EDIT_OPS_BASE, { description: "Chunk edit operation" }),
...chunkEditOperationSchemaBaseProps,
});
const chunkEditOperationSchemaWithSplice = Type.Object({
op: StringEnum([...CHUNK_EDIT_OPS_BASE, "splice"], { description: "Chunk edit operation" }),
...chunkEditOperationSchemaBaseProps,
const chunkEditBegEndProps = {
beg: Type.Integer({
description:
"Start line: absolute file line (1-indexed, same as read gutter). With end, inclusive when beg ≤ end; when beg = end + 1, zero-width insertion.",
}),
end: Type.Integer({
description:
"End line: absolute file line (1-indexed). Inclusive when beg ≤ end; for zero-width insertion use end = beg − 1.",
}),
} as const;
const chunkEditOptionalBegEndProps = {
beg: Type.Optional(
Type.Integer({
description:
"Start line: absolute file line (1-indexed, same as read gutter). Used by splice and replace with line range. With end, inclusive when beg \u2264 end; when beg = end + 1, zero-width insertion.",
}),
),
end: Type.Optional(
Type.Integer({
description:
"End line: absolute file line (1-indexed). Inclusive when beg \u2264 end; for zero-width insertion use end = beg \u2212 1.",
"Start line for line-scoped replace. When both beg and end are provided, replaces only those file lines (same as splice).",
}),
),
end: Type.Optional(Type.Integer({ description: "End line for line-scoped replace." })),
} as const;
// Insert ops (no CRC)
const appendChildOp = Type.Object({ append_child: Type.Object({ ...chunkEditSelProp, ...chunkEditContentProp }) });
const prependChildOp = Type.Object({ prepend_child: Type.Object({ ...chunkEditSelProp, ...chunkEditContentProp }) });
const appendSiblingOp = Type.Object({ append_sibling: Type.Object({ ...chunkEditSelProp, ...chunkEditContentProp }) });
const prependSiblingOp = Type.Object({
prepend_sibling: Type.Object({ ...chunkEditSelProp, ...chunkEditContentProp }),
});
// Mutation ops (CRC required)
const replaceOp = Type.Object({
replace: Type.Object({
...chunkEditSelProp,
...chunkEditCrcProp,
...chunkEditContentProp,
...chunkEditOptionalBegEndProps,
}),
});
const deleteOp = Type.Object({ delete: Type.Object({ ...chunkEditSelProp, ...chunkEditCrcProp }) });
const spliceOp = Type.Object({
splice: Type.Object({
...chunkEditSelProp,
...chunkEditCrcProp,
...chunkEditBegEndProps,
...chunkEditOptionalContentProp,
}),
});
const chunkEditOperationSchemaWithoutSplice = Type.Union([
appendChildOp,
prependChildOp,
appendSiblingOp,
prependSiblingOp,
replaceOp,
deleteOp,
]);
const chunkEditOperationSchemaWithSplice = Type.Union([
appendChildOp,
prependChildOp,
appendSiblingOp,
prependSiblingOp,
replaceOp,
deleteOp,
spliceOp,
]);
const chunkEditOperationSchema = chunkSplicesEnabled()
? chunkEditOperationSchemaWithSplice
: chunkEditOperationSchemaWithoutSplice;
@@ -382,6 +421,44 @@ function isChunkParams(params: ReplaceParams | PatchParams | HashlineParams | Ch
return "operations" in params;
}
const CHUNK_OP_KEYS = [
"append_child",
"prepend_child",
"append_sibling",
"prepend_sibling",
"replace",
"delete",
"splice",
] as const;
type ChunkOpKey = (typeof CHUNK_OP_KEYS)[number];
interface ParsedChunkOp {
op: ChunkOpKey;
sel?: string;
crc?: string;
content?: string | string[];
beg?: number;
end?: number;
}
/** Normalize keyed `{ splice: { ... } }` format to flat internal format. Also tolerates legacy `{ op: "splice", ... }`. */
function parseKeyedChunkOp(raw: Record<string, unknown>): ParsedChunkOp {
// Legacy flat format: { op: "splice", sel: "...", ... }
if (typeof raw.op === "string" && CHUNK_OP_KEYS.includes(raw.op as ChunkOpKey)) {
return raw as unknown as ParsedChunkOp;
}
// Keyed format: { splice: { sel: "...", ... } }
for (const key of CHUNK_OP_KEYS) {
if (key in raw && typeof raw[key] === "object" && raw[key] !== null) {
const inner = raw[key] as Record<string, unknown>;
return { op: key, ...inner } as unknown as ParsedChunkOp;
}
}
throw new Error(`Unknown chunk edit operation shape: ${JSON.stringify(Object.keys(raw))}`);
}
/**
* Edit tool implementation.
*
@@ -535,9 +612,10 @@ export class EditTool implements AgentTool<TInput> {
: undefined;
const normalizedOperations: ChunkEditOperation[] = operations.map(operation => {
const anchorSelector = operation.sel ?? defaultSelector;
const anchorCrc = operation.crc ?? (operation.sel === undefined ? defaultCrc : undefined);
const content = normalizeChunkContentInput(operation.content);
const opEntry = parseKeyedChunkOp(operation as Record<string, unknown>);
const anchorSelector = opEntry.sel ?? defaultSelector;
const anchorCrc = opEntry.crc ?? (opEntry.sel === undefined ? defaultCrc : undefined);
const content = normalizeChunkContentInput(opEntry.content);
const requireChecksum = (op: string): void => {
if (anchorCrc) return;
@@ -559,31 +637,31 @@ export class EditTool implements AgentTool<TInput> {
);
};
switch (operation.op) {
switch (opEntry.op) {
case "append_child":
case "prepend_child":
case "append_sibling":
case "prepend_sibling":
if (!content) {
throw new Error(`Content required for ${operation.op} on ${anchorSelector ?? "<root>"}`);
throw new Error(`Content required for ${opEntry.op} on ${anchorSelector ?? "<root>"}`);
}
return { op: operation.op, sel: operation.sel, crc: operation.crc, content } as ChunkEditOperation;
return { op: opEntry.op, sel: opEntry.sel, crc: opEntry.crc, content } as ChunkEditOperation;
case "replace":
requireChecksum("replace");
if (content.length === 0) {
return { op: "delete", sel: operation.sel, crc: anchorCrc } as ChunkEditOperation;
return { op: "delete", sel: opEntry.sel, crc: anchorCrc } as ChunkEditOperation;
}
return {
op: "replace",
sel: operation.sel,
sel: opEntry.sel,
crc: anchorCrc,
content,
beg: (operation as { beg?: number }).beg,
end: (operation as { end?: number }).end,
beg: opEntry.beg,
end: opEntry.end,
} as ChunkEditOperation;
case "delete":
requireChecksum("delete");
return { op: "delete", sel: operation.sel, crc: anchorCrc } as ChunkEditOperation;
return { op: "delete", sel: opEntry.sel, crc: anchorCrc } as ChunkEditOperation;
case "splice":
if (!chunkSplicesEnabled()) {
throw new Error(
@@ -591,19 +669,19 @@ export class EditTool implements AgentTool<TInput> {
);
}
requireChecksum("splice");
if (operation.beg == null || operation.end == null) {
if (opEntry.beg == null || opEntry.end == null) {
throw new Error(`beg and end required for splice on ${anchorSelector}`);
}
return {
op: "splice",
sel: operation.sel,
sel: opEntry.sel,
crc: anchorCrc,
beg: operation.beg,
end: operation.end,
beg: opEntry.beg,
end: opEntry.end,
content,
} as ChunkEditOperation;
default:
throw new Error(`Unknown chunk edit operation: ${(operation as { op: string }).op}`);
throw new Error(`Unknown chunk edit operation: ${opEntry.op}`);
}
});
@@ -4,7 +4,7 @@ Read the file first with `read(path="file.ts")`. Use chunk paths exactly as show
Successful edit responses already include the updated root chunk rendering for that file. Do not immediately re-read the file just to refresh checksums unless the file may have changed externally.
**Checksum scope:** Each chunk has its own CRC over that chunk’s source span. Editing non-overlapping lines elsewhere in the file does not change unrelated chunks’ checksums. There is no separate whole-file guard.
**Checksum scope:** Each chunk has its own CRC over that chunk's source span. Editing non-overlapping lines elsewhere in the file does not change unrelated chunks' checksums. There is no separate whole-file guard.
<operations>
{{#if chunkSplices}}
@@ -15,43 +15,42 @@ Successful edit responses already include the updated root chunk rendering for t
- `splice` is almost always the right choice for small, targeted fixes
- `replace` with `beg`/`end` also works for line-level fixes (same behavior as `splice`)
{{/if}}
|op|effect|
|---|---|
|`append_child`|insert as last child inside a container (before closing delimiter)|
|`prepend_child`|insert as first child inside a container (after opening delimiter)|
|`append_sibling`|insert after the selected chunk|
|`prepend_sibling`|insert before the selected chunk|
|`replace`|without `beg`/`end`: rewrite the **entire** chunk from first line to last line; supply full replacement content. With `beg`/`end`: replace only those **file lines** within the chunk (same as `splice` but semantically a replacement). `splice` is almost always the right choice for small, targeted fixes|
|`delete`|remove the entire chunk|
|operation|format|effect|
|---|---|---|
|`append_child`|`{ "append_child": { "sel": "…", "content": "…" } }`|insert as last child inside a container (before closing delimiter)|
|`prepend_child`|`{ "prepend_child": { "sel": "…", "content": "…" } }`|insert as first child inside a container (after opening delimiter)|
|`append_sibling`|`{ "append_sibling": { "sel": "…", "content": "…" } }`|insert after the selected chunk|
|`prepend_sibling`|`{ "prepend_sibling": { "sel": "…", "content": "…" } }`|insert before the selected chunk|
|`replace`|`{ "replace": { "sel": "…", "crc": "…", "content": "…" } }`|without `beg`/`end`: rewrite the **entire** chunk from first line to last line; supply full replacement content. With `beg`/`end`: replace only those **file lines** within the chunk (same as `splice` but semantically a replacement). `splice` is almost always the right choice for small, targeted fixes|
|`delete`|`{ "delete": { "sel": "…", "crc": "…" } }`|remove the entire chunk|
{{#if chunkSplices}}
|`splice`|when `beg` ≤ `end`, replace **file** lines `beg`–`end` (inclusive) within the selected chunk; empty `content` deletes them. When `beg` = `end` + 1, **zero-width splice**: insert in the gap after file line `end` and before file line `beg` (no lines removed)|
|`splice`|`{ "splice": { "sel": "…", "crc": "…", "beg": N, "end": N } }`|when `beg` ≤ `end`, replace **file** lines `beg`–`end` (inclusive) within the selected chunk; empty `content` deletes them. When `beg` = `end` + 1, **zero-width splice**: insert in the gap after file line `end` and before file line `beg` (no lines removed)|
{{/if}}
- `op` is a single keyword only (e.g. `"splice"`). Do not put other fields like `sel` inside `op`.
- `sel` is the chunk path (a separate field from `op`)
- `sel` is the chunk path
- `crc` is the chunk checksum used for staleness validation on `replace`, `delete`{{#if chunkSplices}}, and `splice`{{/if}}; it guards the selected chunk, not unrelated file changes elsewhere in the file
{{#if chunkSplices}}
- `beg`/`end` are **absolute file line numbers** (same as the gutter in `read`). They must fall within the selected chunk’s file line span (or form a valid zero-width gap on its boundary).
- `beg`/`end` are **absolute file line numbers** (same as the gutter in `read`). They must fall within the selected chunk's file line span (or form a valid zero-width gap on its boundary).
{{/if}}
- `path="file.ts:chunk_path"` sets a default `sel` for operations that omit it
- top-level `crc` sets the default checksum for the path-level selector
- **Auto-indent (flat):** The tool adds the target insertion column’s base indent to every **non-empty** line of your `content`. It does not parse or understand block nesting; put any deeper relative indentation in the `content` yourself (extra leading spaces/tabs on inner lines).
- **Auto-indent (flat):** The tool adds the target insertion column's base indent to every **non-empty** line of your `content`. It does not parse or understand block nesting; put any deeper relative indentation in the `content` yourself (extra leading spaces/tabs on inner lines).
- Chunk paths are always fully qualified: `class_Server.fn_start`, not bare `fn_start`
- Batch ops observe earlier edits in the same request. If op 1 changes a chunk’s checksum, span, or path, op 2 must use the **post-op-1** checksum/span/path.
- Batch ops observe earlier edits in the same request. If op 1 changes a chunk's checksum, span, or path, op 2 must use the **post-op-1** checksum/span/path.
- `replace`/`delete` operate on the selected chunk range, including leading comments or attributes that the parser attached to that chunk.
</operations>
{{#if chunkSplices}}
<splice>
**Zero-width `splice` (insert only, `beg` = `end` + 1), using file line numbers `S` = chunk’s first line, `E` = chunk’s last line:**
**Zero-width `splice` (insert only, `beg` = `end` + 1), using file line numbers `S` = chunk's first line, `E` = chunk's last line:**
Use zero-width splice for separator gaps between neighboring chunks. Gap edits are independent from either chunk’s checksum: inserting into the gap does not update the following chunk’s `crc`, and later `replace`/`delete` operations only affect that chunk’s own selected range.
Use zero-width splice for separator gaps between neighboring chunks. Gap edits are independent from either chunk's checksum: inserting into the gap does not update the following chunk's `crc`, and later `replace`/`delete` operations only affect that chunk's own selected range.
|Goal|`beg`|`end`|
|---|---|---|
|Insert before file line L|L|L − 1|
|Insert after file line L|L + 1|L|
|Insert before the chunk’s first line S|S|S − 1|
|Insert after the chunk’s last line E|E + 1|E|
|Insert before the chunk's first line S|S|S − 1|
|Insert after the chunk's last line E|E + 1|E|
</splice>
{{/if}}
@@ -97,10 +96,11 @@ All examples reference this `read` output (`beg`/`end` match the gutter):
"path": "server.ts",
"operations": [
{
"op": "replace",
"sel": "class_Server.fn_start",
"crc": "HTST",
"content": "start(): void {\n log(\"starting\");\n this.tryBind();\n}"
"replace": {
"sel": "class_Server.fn_start",
"crc": "HTST",
"content": "start(): void {\n log(\"starting\");\n this.tryBind();\n}"
}
}
]
```
@@ -112,12 +112,13 @@ All examples reference this `read` output (`beg`/`end` match the gutter):
"path": "server.ts",
"operations": [
{
"op": "splice",
"sel": "class_Server.fn_start",
"crc": "HTST",
"beg": 13,
"end": 13,
"content": " warn(\"booting on \" + this.port);"
"splice": {
"sel": "class_Server.fn_start",
"crc": "HTST",
"beg": 13,
"end": 13,
"content": " warn(\"booting on \" + this.port);"
}
}
]
```
@@ -128,12 +129,13 @@ All examples reference this `read` output (`beg`/`end` match the gutter):
"path": "server.ts",
"operations": [
{
"op": "splice",
"sel": "class_Server.fn_start",
"crc": "HTST",
"beg": 13,
"end": 12,
"content": "const startedAt = Date.now();"
"splice": {
"sel": "class_Server.fn_start",
"crc": "HTST",
"beg": 13,
"end": 12,
"content": "const startedAt = Date.now();"
}
}
]
```
@@ -144,9 +146,10 @@ All examples reference this `read` output (`beg`/`end` match the gutter):
"path": "server.ts",
"operations": [
{
"op": "delete",
"sel": "class_Server.fn_tryBind",
"crc": "VNWR"
"delete": {
"sel": "class_Server.fn_tryBind",
"crc": "VNWR"
}
}
]
```
@@ -158,17 +161,19 @@ All examples reference this `read` output (`beg`/`end` match the gutter):
"path": "server.ts",
"operations": [
{
"op": "splice",
"sel": "class_Server.fn_start",
"crc": "HTST",
"beg": 13,
"end": 13,
"content": " warn(\"booting on \" + this.port);"
"splice": {
"sel": "class_Server.fn_start",
"crc": "HTST",
"beg": 13,
"end": 13,
"content": " warn(\"booting on \" + this.port);"
}
},
{
"op": "delete",
"sel": "class_Server.fn_tryBind",
"crc": "VNWR"
"delete": {
"sel": "class_Server.fn_tryBind",
"crc": "VNWR"
}
}
]
```
@@ -179,15 +184,17 @@ All examples reference this `read` output (`beg`/`end` match the gutter):
"path": "server.ts",
"operations": [
{
"op": "replace",
"sel": "class_Server.fn_start",
"crc": "HTST",
"content": "start(): void {\n warn(\"booting on \" + this.port);\n for (let i = 0; i < MAX_RETRIES; i++) {\n this.tryBind();\n }\n}"
"replace": {
"sel": "class_Server.fn_start",
"crc": "HTST",
"content": "start(): void {\n warn(\"booting on \" + this.port);\n for (let i = 0; i < MAX_RETRIES; i++) {\n this.tryBind();\n }\n}"
}
},
{
"op": "delete",
"sel": "class_Server.fn_tryBind",
"crc": "VNWR"
"delete": {
"sel": "class_Server.fn_tryBind",
"crc": "VNWR"
}
}
]
```
@@ -205,8 +212,8 @@ All examples reference this `read` output (`beg`/`end` match the gutter):
{{#if chunkSplices}}
- For `splice`, copy `beg`/`end` from the **file line numbers** in `read` output (the same values as elision `sel=L…` ranges).
- **Do NOT batch multiple `splice` operations on the same chunk** in one edit call. Each splice changes the chunk's checksum, and you cannot predict the new checksum. Instead: either combine the changes into a single splice with a wider `beg`/`end` range covering all affected lines, or make separate edit calls (one splice per call).
- Each `splice` operation requires its own `crc` field (not just a top-level `crc`). Put `"crc": "XXXX"` inside the operation object.
{{/if}}
- When restoring a deleted statement, check surrounding lines carefully — insert at the exact position relative to existing code. Do not duplicate adjacent lines.
- Use only chunk paths that appear in the `read` output. If you need deeper chunks, `read` the parent chunk first to discover its children.
</critical>
</output>
@@ -725,6 +725,7 @@ function resolveAnchorChunk(state: MutableChunkState, selector: string | undefin
/**
* Normalize a chunk selector from model output:
* - Strip `filename.ext:` prefix (model puts `path:chunk` format in `sel`)
* - Strip leading `:` (model artifact)
* - Strip trailing `#XXXX` hash suffix (model confuses checksum with path)
* - Convert literal `"null"` string to empty
@@ -733,10 +734,14 @@ function sanitizeChunkSelector(selector: string | undefined): string | undefined
if (selector === undefined || selector === null) return undefined;
let s = typeof selector === "string" ? selector : String(selector);
if (s === "null" || s === "undefined") return undefined;
// Strip filename prefix: "pirate.ts:expression_2" -> "expression_2"
// Models sometimes put the path:chunk format in the sel field instead of path.
const colonIdx = chunkReadPathSeparatorIndex(s);
if (colonIdx !== -1) s = s.slice(colonIdx + 1);
// Strip leading colon (":error" -> "error")
if (s.startsWith(":")) s = s.slice(1);
// Strip trailing #XXXX checksum suffix ("imports#YKXY" -> "imports")
s = s.replace(/#[A-Z]{4}$/, "");
s = s.replace(/#[A-Z]{4}$/i, "");
return s.trim() || undefined;
}
@@ -754,11 +759,12 @@ function resolveAnchorWithCrc(
* Normalize a CRC value from model output:
* - Convert literal `"null"` / `"undefined"` strings to undefined
* - Treat empty strings as undefined
* - Normalize to uppercase (models sometimes emit lowercase checksums)
*/
function sanitizeCrc(crc: string | undefined): string | undefined {
if (crc === undefined || crc === null) return undefined;
if (crc === "null" || crc === "undefined" || crc === "") return undefined;
return crc;
return crc.toUpperCase();
}
function validateCrc(chunk: ChunkNode, crc: string | undefined): void {
@@ -208,14 +208,15 @@ describe("chunk mode tools", () => {
crc: checksum,
operations: [
{
op: "replace",
content: ` private handleError(err: Error): string {
replace: {
content: ` private handleError(err: Error): string {
return \`normalized:\${err.message}\`;
}
`,
},
},
],
});
} as never);
const editText = getText(editResult);
const updatedSource = await Bun.file(filePath).text();
@@ -256,20 +257,22 @@ describe("chunk mode tools", () => {
crc: checksum,
operations: [
{
op: "splice",
beg: 63,
end: 63,
content: "return err.message.toUpperCase() + total;",
splice: {
beg: 63,
end: 63,
content: "return err.message.toUpperCase() + total;",
},
},
{
op: "splice",
crc: checksum2,
beg: 3,
end: 3,
content: "let total = 1;",
splice: {
crc: checksum2,
beg: 3,
end: 3,
content: "let total = 1;",
},
},
],
});
} as never);
const updatedSource = await Bun.file(filePath).text();
expect(updatedSource).toContain("let total = 1;");
@@ -306,9 +309,10 @@ describe("chunk mode tools", () => {
path: filePath,
operations: [
{
op: "append_child",
sel: "class_Server",
content: 'status(): string {\n return "ok";\n}\n',
append_child: {
sel: "class_Server",
content: 'status(): string {\n return "ok";\n}\n',
},
},
],
} as never);
@@ -350,20 +354,22 @@ describe("chunk mode tools", () => {
path: filePath,
operations: [
{
op: "splice",
sel: chunkPath,
crc: checksum,
beg: 4,
end: 3,
content: "const end = Date.now();",
splice: {
sel: chunkPath,
crc: checksum,
beg: 4,
end: 3,
content: "const end = Date.now();",
},
},
{
op: "splice",
sel: chunkPath,
crc: checksum2,
beg: 3,
end: 2,
content: "const start = Date.now();",
splice: {
sel: chunkPath,
crc: checksum2,
beg: 3,
end: 2,
content: "const start = Date.now();",
},
},
],
} as never);
@@ -386,10 +392,11 @@ describe("chunk mode tools", () => {
path: filePath,
operations: [
{
op: "replace",
sel: "fn_main",
crc: checksum,
content: "",
replace: {
sel: "fn_main",
crc: checksum,
content: "",
},
},
],
});
@@ -427,12 +434,13 @@ describe("chunk mode tools", () => {
path: filePath,
operations: [
{
op: "splice",
sel: "class_Server.fn_handleError",
crc: checksum,
beg: 5,
end: 2,
content: " let total = 1;",
splice: {
sel: "class_Server.fn_handleError",
crc: checksum,
beg: 5,
end: 2,
content: " let total = 1;",
},
},
],
}),
@@ -453,14 +461,16 @@ describe("chunk mode tools", () => {
path: filePath,
operations: [
{
op: "append_child",
sel: "class_Server",
content: ' status(): string {\n return "ok";\n }',
append_child: {
sel: "class_Server",
content: ' status(): string {\n return "ok";\n }',
},
},
{
op: "delete",
sel: "class_Server.fn_handleError",
crc: "ZZZZ",
delete: {
sel: "class_Server.fn_handleError",
crc: "ZZZZ",
},
},
],
}),
@@ -482,10 +492,11 @@ describe("chunk mode tools", () => {
path: filePath,
operations: [
{
op: "replace",
sel: "class_Server.fn_handleError",
crc: checksum,
content: " private handleError(err: Error): string {\n if (err) {\n",
replace: {
sel: "class_Server.fn_handleError",
crc: checksum,
content: " private handleError(err: Error): string {\n if (err) {\n",
},
},
],
}),
@@ -507,18 +518,20 @@ describe("chunk mode tools", () => {
path: filePath,
operations: [
{
op: "replace",
sel: "class_Server.fn_handleError",
crc: checksum,
content: " private handleError(err: Error): string {\n return err.message;\n }",
replace: {
sel: "class_Server.fn_handleError",
crc: checksum,
content: " private handleError(err: Error): string {\n return err.message;\n }",
},
},
{
op: "splice",
sel: "class_Server.fn_handleError",
crc: checksum,
beg: 63,
end: 63,
content: " return err.message.toUpperCase();",
splice: {
sel: "class_Server.fn_handleError",
crc: checksum,
beg: 63,
end: 63,
content: " return err.message.toUpperCase();",
},
},
],
}),
@@ -541,13 +554,14 @@ describe("chunk mode tools", () => {
path: filePath,
operations: [
{
op: "splice",
sel: "class_Server.fn_handleError",
// no crc!
beg: 3,
end: 3,
content: " let total = 1;",
},
splice: {
sel: "class_Server.fn_handleError",
// no crc!
beg: 3,
end: 3,
content: " let total = 1;",
},
} as never,
],
}),
).rejects.toThrow(/Checksum required/);
@@ -567,10 +581,11 @@ describe("chunk mode tools", () => {
path: filePath,
operations: [
{
op: "replace",
sel: "main",
crc: checksum,
content: 'function main(): void {\n console.log("started");\n}\n',
replace: {
sel: "main",
crc: checksum,
content: 'function main(): void {\n console.log("started");\n}\n',
},
},
],
});