refactor: restructured chunk region API and storage layer for consistency

- Renamed ChunkRegion::Inner variant to Body across Rust and TypeScript for clearer semantics.
- Renamed chunk edit tool parameter from target to sel and made op required instead of optional.
- Removed region aliases (prologue, epilogue, inner) and simplified region validation to accept only head, body, tail.
- Replaced db.exec() calls with db.run() for SQL execution in auth-storage, agent-storage, and history-storage.
- Enhanced chunk edit documentation with comprehensive examples, indentation rules, and CRC refresh guidance.
- Improved chunk statement merging logic to consolidate leaf statements into parent function bodies by default.
This commit is contained in:
can1357
2026-04-08 11:34:40 +02:00
parent 4c03bad90d
commit fee6b7858a
21 changed files with 581 additions and 193 deletions
+8 -6
View File
@@ -162,12 +162,14 @@ impl LangClassifier for PythonClassifier {
"except_clause" => Some(positional_candidate(node, ChunkKind::Except, source)),
"match_statement" => Some(positional_candidate(node, ChunkKind::Match, source)),
// ── Variables ──
"expression_statement" | "assignment" => {
Some(group_candidate(node, ChunkKind::Statements, source))
},
_ => None,
// ── Variables / simple statements ──
// Catch `expression_statement`, `assignment`, `return_statement`,
// `raise_statement`, `pass_statement`, `break_statement`,
// `continue_statement`, `delete_statement`, `assert_statement`,
// `nonlocal_statement`, `global_statement`, `type_alias_statement`,
// and any other leaf statements so they merge into the parent
// function body instead of becoming standalone addressable chunks.
_ => Some(group_candidate(node, ChunkKind::Statements, source)),
}
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ env_uint! {
/// Always recurse into named (non-group) chunks when children exist,
/// regardless of the leaf threshold. Enabled via `PI_CHUNK_ALWAYS_RECURSE=1`.
pub static ALWAYS_RECURSE: std::sync::LazyLock<bool> = std::sync::LazyLock::new(|| {
std::env::var("PI_CHUNK_ALWAYS_RECURSE").map_or(false, |v| !matches!(v.as_str(), "0" | "false"))
std::env::var("PI_CHUNK_ALWAYS_RECURSE").is_ok_and(|v| !matches!(v.as_str(), "0" | "false"))
});
/// Suppress `[/chunk#CRC]` closing tags in rendered output.
+219 -26
View File
@@ -305,11 +305,15 @@ fn resolve_edit_target(
};
let resolved =
resolve_chunk_with_crc(state, cleaned_selector.as_deref(), resolve_crc, warnings)?;
let region = operation.region.or(parsed_region);
let mut region = operation.region.or(parsed_region);
if !batch_auto_accepted {
validate_batch_crc(resolved.chunk, resolved.crc.as_deref(), requires_checksum)?;
}
let chunk = resolved.chunk.clone();
if chunk.prologue_end_byte.is_none() || chunk.epilogue_start_byte.is_none() {
region = None;
}
Ok(ResolvedEditTarget { chunk, region })
}
@@ -500,8 +504,12 @@ fn apply_insert(
file_indent_char,
file_indent_step,
)?;
let is_prepend_or_append = matches!(operation.op, ChunkEditOp::Prepend | ChunkEditOp::Append);
let spacing = compute_insert_spacing(state, &anchor, pos, is_prepend_or_append);
let suppress_chunk_adjacency =
matches!(operation.op, ChunkEditOp::Prepend | ChunkEditOp::Append)
&& !(matches!(operation.op, ChunkEditOp::Append)
&& pos == InsertPosition::After
&& owned_container_end_line(state, &anchor) > anchor.end_line);
let spacing = compute_insert_spacing(state, &anchor, pos, suppress_chunk_adjacency);
let content = operation.content.as_deref().unwrap_or_default();
let mut replacement = normalize_inserted_content(
content,
@@ -528,14 +536,14 @@ fn apply_insert(
&& anchor.children.iter().any(|child| child == "preamble")
{
return Err(
"Comment-only @inner.prepend on root is not allowed when the file has a preamble \
"Comment-only @body.prepend on root is not allowed when the file has a preamble \
chunk. Use replace on the preamble chunk instead."
.to_owned(),
);
}
if comment_only && !anchor.children.is_empty() {
warnings.push(
"Comment-only @inner.prepend can merge into the following chunk's first line; it is \
"Comment-only @body.prepend can merge into the following chunk's first line; it is \
not a separate named chunk."
.to_owned(),
);
@@ -602,7 +610,7 @@ fn validate_crc(chunk: &ChunkNode, crc: Option<&str>) -> Result<(), String> {
format!(
"Checksum required for {}. Re-read the chunk to get the current checksum, then include \
it in the selector. Hint: use target \"{}\" for container replacement, or append \
another region such as @inner.",
another region such as @body.",
chunk_path_opt(chunk),
selector
)
@@ -693,7 +701,7 @@ fn target_indent_for_region(
None | Some(ChunkRegion::Head | ChunkRegion::Tail) => {
anchor.indent_char.repeat(anchor.indent as usize)
},
Some(ChunkRegion::Inner) => {
Some(ChunkRegion::Body) => {
compute_insert_indent(state, anchor, true, file_indent_char, file_indent_step)
},
}
@@ -1039,7 +1047,7 @@ fn body_insertion_point(
indent: child_indent,
};
}
let (_, body_end) = chunk_region_range(anchor, ChunkRegion::Inner);
let (_, body_end) = chunk_region_range(anchor, ChunkRegion::Body);
return InsertionPoint { offset: body_end, indent };
}
@@ -1055,7 +1063,7 @@ fn body_insertion_point(
indent,
};
}
let (body_start, _) = chunk_region_range(anchor, ChunkRegion::Inner);
let (body_start, _) = chunk_region_range(anchor, ChunkRegion::Body);
InsertionPoint { offset: body_start, indent }
}
@@ -1078,13 +1086,13 @@ fn resolve_insertion_point(
Ok((after_chunk_insertion_point(state, anchor), InsertPosition::After))
},
// Inner first-child position
(Some(ChunkRegion::Inner), ChunkEditOp::Before | ChunkEditOp::Prepend)
(Some(ChunkRegion::Body), ChunkEditOp::Before | ChunkEditOp::Prepend)
| (Some(ChunkRegion::Head), ChunkEditOp::After | ChunkEditOp::Append) => Ok((
body_insertion_point(state, anchor, false, file_indent_char, file_indent_step),
InsertPosition::FirstChild,
)),
// Inner last-child position
(Some(ChunkRegion::Inner), ChunkEditOp::After | ChunkEditOp::Append)
(Some(ChunkRegion::Body), ChunkEditOp::After | ChunkEditOp::Append)
| (Some(ChunkRegion::Tail), ChunkEditOp::Before | ChunkEditOp::Prepend) => Ok((
body_insertion_point(state, anchor, true, file_indent_char, file_indent_step),
InsertPosition::LastChild,
@@ -1828,7 +1836,10 @@ mod tests {
let result = result.expect("line-number selector should auto-resolve");
assert!(
result.warnings.iter().any(|w| w.contains("Auto-resolved line target")),
result
.warnings
.iter()
.any(|w| w.contains("Auto-resolved line target")),
"should warn about auto-resolution: {:?}",
result.warnings
);
@@ -2143,7 +2154,7 @@ mod tests {
let result = apply_single_edit(&state, "test.ts", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("fn_main#{}@inner", chunk.checksum)),
sel: Some(format!("fn_main#{}@body", chunk.checksum)),
crc: None,
region: None,
content: Some("\treturn next();\n".to_owned()),
@@ -2161,7 +2172,7 @@ mod tests {
let result = apply_single_edit(&state, "test.rs", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("fn_main#{}@inner", chunk.checksum)),
sel: Some(format!("fn_main#{}@body", chunk.checksum)),
crc: None,
region: None,
content: Some("\tprintln!(\"new\");\n".to_owned()),
@@ -2179,7 +2190,7 @@ mod tests {
let result = apply_single_edit(&state, "test.go", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("fn_main#{}@inner", chunk.checksum)),
sel: Some(format!("fn_main#{}@body", chunk.checksum)),
crc: None,
region: None,
content: Some("\treturn\n".to_owned()),
@@ -2197,7 +2208,7 @@ mod tests {
let result = apply_single_edit(&state, "test.py", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("fn_run#{}@inner", chunk.checksum)),
sel: Some(format!("fn_run#{}@body", chunk.checksum)),
crc: None,
region: None,
content: Some("\treturn 2\n".to_owned()),
@@ -2243,7 +2254,7 @@ mod tests {
let body_state = state_for(source, "go");
let body_result = apply_single_edit(&body_state, "test.go", EditOperation {
op: ChunkEditOp::Append,
sel: Some("type_Server@inner".to_owned()),
sel: Some("type_Server@body".to_owned()),
crc: None,
region: None,
content: Some("\tPort int\n".to_owned()),
@@ -2297,6 +2308,30 @@ mod tests {
);
}
#[test]
fn go_type_container_append_after_receiver_methods_preserves_sibling_spacing() {
let source = "package main\n\ntype Server struct {}\n\nfunc (s *Server) Start() {}\nfunc (s \
*Server) Stop() {}\n";
let state = state_for(source, "go");
let result = apply_single_edit(&state, "test.go", EditOperation {
op: ChunkEditOp::Append,
sel: Some("type_Server".to_owned()),
crc: None,
region: None,
content: Some("func (s *Server) Restart() {}".to_owned()),
find: None,
});
assert!(
result
.diff_after
.contains("func (s *Server) Stop() {}\n\nfunc (s *Server) Restart() {}"),
"{}",
result.diff_after
);
}
#[test]
fn crc_mismatch_error_includes_fresh_chunk_context() {
let source = "class Foo {\n bar() {\n return 1;\n }\n}\n";
@@ -2437,7 +2472,7 @@ mod tests {
let result = apply_single_edit(&state, "test.ts", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("class_Server.fn_start#{}@inner", chunk.checksum)),
sel: Some(format!("class_Server.fn_start#{}@body", chunk.checksum)),
crc: None,
region: None,
content: Some("\treturn 42;\n".to_owned()),
@@ -2462,7 +2497,7 @@ mod tests {
let result = apply_single_edit(&state, "test.ts", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("class_Server.fn_start#{}@inner", chunk.checksum)),
sel: Some(format!("class_Server.fn_start#{}@body", chunk.checksum)),
crc: None,
region: None,
content: Some("\treturn 42;\n".to_owned()),
@@ -2488,7 +2523,7 @@ mod tests {
let result = apply_single_edit(&state, "test.ts", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("class_Server.fn_start#{}@inner", chunk.checksum)),
sel: Some(format!("class_Server.fn_start#{}@body", chunk.checksum)),
crc: None,
region: None,
content: Some("\t\tif (x) {\n\t\t\ty();\n\t\t}\n".to_owned()),
@@ -2504,14 +2539,14 @@ mod tests {
#[test]
fn body_append_inserts_inside_class() {
// Appending to @inner of a class should insert inside the body,
// Appending to @body of a class should insert inside the body,
// not after the closing brace.
let source = "class Foo {\n bar() {\n return 1;\n }\n}\n";
let state = state_for(source, "typescript");
let result = apply_single_edit(&state, "test.ts", EditOperation {
op: ChunkEditOp::Append,
sel: Some("class_Foo@inner".to_owned()),
sel: Some("class_Foo@body".to_owned()),
crc: None,
region: None,
content: Some("baz() {\n\treturn 2;\n}\n".to_owned()),
@@ -2535,14 +2570,14 @@ mod tests {
#[test]
fn body_prepend_inserts_after_opening_brace() {
// Prepending to @inner of an enum should insert after the opening brace,
// Prepending to @body of an enum should insert after the opening brace,
// not before doc comments.
let source = "/** My enum. */\nenum Color {\n Red,\n Green,\n Blue,\n}\n";
let state = state_for(source, "typescript");
let result = apply_single_edit(&state, "test.ts", EditOperation {
op: ChunkEditOp::Prepend,
sel: Some("enum_Color@inner".to_owned()),
sel: Some("enum_Color@body".to_owned()),
crc: None,
region: None,
content: Some("White,\n".to_owned()),
@@ -2620,7 +2655,8 @@ mod tests {
#[test]
fn python_body_append_preserves_indentation() {
let source = "class Server:\n def __init__(self):\n self.x = 1\n\n def start(self):\n pass\n";
let source = "class Server:\n def __init__(self):\n self.x = 1\n\n def \
start(self):\n pass\n";
let state = state_for(source, "python");
let result = apply_single_edit(&state, "test.py", EditOperation {
@@ -2635,9 +2671,166 @@ mod tests {
// The appended method should be at 4-space indent (class member level),
// with its body at 8-space indent.
assert!(
result.diff_after.contains(" def stop(self):\n pass"),
result
.diff_after
.contains(" def stop(self):\n pass"),
"appended method should have correct Python indentation: {}",
result.diff_after
);
}
#[test]
fn body_region_on_leaf_without_delimiters_falls_back_to_full_chunk() {
let source = "enum LogLevel {\n Debug,\n Info,\n Warn,\n Fatal,\n}\n";
let state = state_for(source, "rust");
let chunk = state
.inner()
.chunk("enum_LogLevel.variant_Info")
.expect("variant_Info should exist");
assert!(chunk.prologue_end_byte.is_none(), "leaf variant should not have prologue_end_byte");
for region_name in ["body", "head", "tail"] {
let sel = format!("enum_LogLevel.variant_Info#{}@{}", chunk.checksum, region_name);
let result = apply_edits(&state, &EditParams {
operations: vec![EditOperation {
op: ChunkEditOp::Replace,
sel: Some(sel),
crc: None,
region: None,
content: Some("Error,".to_owned()),
find: None,
}],
default_selector: None,
default_crc: None,
anchor_style: None,
cwd: ".".to_owned(),
file_path: "test.rs".to_owned(),
})
.expect("leaf region should fall back to full chunk");
assert!(
result.diff_after.contains("Debug,\n Error,\n Warn,"),
"@{region_name} should replace the full leaf chunk, got: {}",
result.diff_after
);
}
}
#[test]
fn rust_impl_method_head_replace_no_body_duplication() {
let source = concat!(
"struct Server {
",
" running: bool,
",
"}
",
"
",
"impl Server {
",
" /// Starts the server.
",
" pub fn start(&mut self) {
",
" self.running = true;
",
" println!(\"started\");
",
" }
",
"}
",
);
let state = state_for(source, "rust");
let chunk = state
.inner()
.chunk("impl_Server.fn_start")
.expect("impl_Server.fn_start should exist");
assert!(
chunk.prologue_end_byte.is_some(),
"fn_start should have prologue_end_byte, got: start_byte={}, end_byte={}, prologue_end_byte={:?}, epilogue_start_byte={:?}",
chunk.start_byte, chunk.end_byte, chunk.prologue_end_byte, chunk.epilogue_start_byte,
);
let result = apply_single_edit(&state, "test.rs", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("impl_Server.fn_start#{}@head", chunk.checksum)),
crc: None,
region: None,
content: Some(" /// Initializes and starts the server.
pub fn start(&mut self) {".to_owned()),
find: None,
});
let body_count = result.diff_after.matches("self.running = true;").count();
assert_eq!(
body_count, 1,
"body should appear exactly once after @head replace, got {} occurrences in:
{}",
body_count, result.diff_after
);
assert!(
result.diff_after.contains("/// Initializes and starts the server."),
"new doc comment should be in output:
{}", result.diff_after
);
assert!(
!result.diff_after.contains("/// Starts the server."),
"old doc comment should be removed:
{}", result.diff_after
);
}
#[test]
fn typescript_class_method_head_replace_no_body_duplication() {
let source = concat!(
"class Server {
",
" /** Starts the server. */
",
" start() {
",
" this.running = true;
",
" console.log(\"started\");
",
" }
",
"}
",
);
let state = state_for(source, "typescript");
let chunk = state
.inner()
.chunk("class_Server.fn_start")
.expect("class_Server.fn_start should exist");
assert!(
chunk.prologue_end_byte.is_some(),
"fn_start should have prologue_end_byte"
);
let result = apply_single_edit(&state, "test.ts", EditOperation {
op: ChunkEditOp::Replace,
sel: Some(format!("class_Server.fn_start#{}@head", chunk.checksum)),
crc: None,
region: None,
content: Some(" /** Initializes the server. */
start() {".to_owned()),
find: None,
});
let body_count = result.diff_after.matches("this.running = true;").count();
assert_eq!(
body_count, 1,
"body should appear exactly once after @head replace, got {} occurrences in:
{}",
body_count, result.diff_after
);
assert!(
result.diff_after.contains("/** Initializes the server. */"),
"new doc comment should be in output:
{}", result.diff_after
);
}
}
+1 -2
View File
@@ -201,8 +201,7 @@ pub fn reindent_inserted_block(
target_indent: &str,
file_indent_step: Option<usize>,
) -> String {
let lines = content.split('\n').collect::<Vec<_>>();
if lines.is_empty() {
if content.is_empty() {
return String::new();
}
+63 -1
View File
@@ -58,6 +58,7 @@ mod ast_vue;
use std::collections::HashMap;
use ast_grep_core::tree_sitter::LanguageExt;
use napi::{Error, Result};
use napi_derive::napi;
@@ -1727,7 +1728,7 @@ impl Config {
.expect("state should parse");
let result = state
.render_read(ReadRenderParams {
read_path: "sample.ts:fn_run@inner".to_string(),
read_path: "sample.ts:fn_run@body".to_string(),
display_path: "sample.ts".to_string(),
language_tag: Some("ts".to_string()),
omit_checksum: false,
@@ -2204,4 +2205,65 @@ end
tool.end_line,
);
}
#[test]
fn python_property_no_orphan_return_chunk() {
// Build a function large enough to trigger recursion (> LEAF_THRESHOLD)
// with multiple control-flow children that narrow scope enough to trigger
// recursion, plus a `return` statement that must NOT become a standalone
// chunk.
let mut body = String::new();
body.push_str("class Server:\n");
body.push_str(" @property\n");
body.push_str(" def address(self) -> str:\n");
body.push_str(" if self._host:\n");
for i in 0..20 {
let _ = writeln!(body, " x{i} = {i}");
}
body.push_str(" if self._port:\n");
for i in 0..20 {
let _ = writeln!(body, " y{i} = {i}");
}
body.push_str(" return f\"{{self._host}}:{{self._port}}\"\n");
let source = body;
let tree = build_chunk_tree(source.as_str(), "python").expect("tree should build");
// Dump all chunk paths for debugging
for chunk in &tree.chunks {
eprintln!(
"chunk: path={:?} kind={:?} leaf={} lines={}-{}",
chunk.path, chunk.kind, chunk.leaf, chunk.start_line, chunk.end_line
);
}
// The function body should recurse (it's large enough) but the return
// statement should NOT become a standalone addressable chunk.
let fn_chunk = tree
.chunks
.iter()
.find(|c| c.path == "class_Server.fn_address")
.expect("fn_address should exist");
let orphan_ret = tree.chunks.iter().find(|c| c.path.contains("ret"));
assert!(
orphan_ret.is_none(),
"return statement inside property method should not be a separate chunk, found: {:?}",
orphan_ret.map(|c| (&c.path, &c.kind))
);
// Verify the function actually recursed (has children)
assert!(
!fn_chunk.leaf,
"fn_address should recurse into children for this test to be meaningful"
);
}
#[test]
#[test]
#[test]
}
+12 -23
View File
@@ -14,18 +14,15 @@ pub struct ResolvedChunk<'a> {
fn parse_region_name(value: &str) -> Option<ChunkRegion> {
match value.trim() {
"head" | "prologue" => Some(ChunkRegion::Head),
"inner" | "body" => Some(ChunkRegion::Inner),
"tail" | "epilogue" => Some(ChunkRegion::Tail),
"head" => Some(ChunkRegion::Head),
"body" => Some(ChunkRegion::Body),
"tail" => Some(ChunkRegion::Tail),
_ => None,
}
}
fn is_known_region_name(value: &str) -> bool {
matches!(
value.trim(),
"outer" | "container" | "head" | "prologue" | "inner" | "body" | "tail" | "epilogue"
)
matches!(value.trim(), "head" | "body" | "tail")
}
/// Split a trailing `@region` suffix from a selector. Returns the selector
@@ -69,7 +66,7 @@ pub fn split_selector_crc_and_region(
(prefix, parsed_region)
} else if let Some((_, suffix)) = raw.rsplit_once('@') {
return Err(format!(
"Unknown chunk region \"{}\". Valid regions: head, inner, tail (or omit for the full \
"Unknown chunk region \"{}\". Valid regions: head, body, tail (or omit for the full \
chunk).",
suffix.trim()
));
@@ -201,7 +198,7 @@ pub fn chunk_region_range(chunk: &ChunkNode, region: ChunkRegion) -> (usize, usi
let epi_start = chunk.epilogue_start_byte.map_or(end, |b| b as usize);
match region {
ChunkRegion::Head => (start, pro_end),
ChunkRegion::Inner => (pro_end, epi_start),
ChunkRegion::Body => (pro_end, epi_start),
ChunkRegion::Tail => (epi_start, end),
}
}
@@ -226,20 +223,12 @@ fn resolve_chunk_selector_impl<'a>(
};
if is_line_number_selector(cleaned) {
if let Some(line) = parse_line_number(cleaned) {
if let Some(chunk_path) =
crate::chunk::line_to_chunk_path(state.tree(), line)
{
warnings.push(format!(
"Auto-resolved line target \"{cleaned}\" to chunk \"{chunk_path}\"."
));
return resolve_chunk_selector_impl(
state,
Some(&chunk_path),
crc,
warnings,
);
}
if let Some(line) = parse_line_number(cleaned)
&& let Some(chunk_path) = crate::chunk::line_to_chunk_path(state.tree(), line)
{
warnings
.push(format!("Auto-resolved line target \"{cleaned}\" to chunk \"{chunk_path}\"."));
return resolve_chunk_selector_impl(state, Some(&chunk_path), crc, warnings);
}
return Err(format!(
"Line target \"{cleaned}\" does not fall inside any chunk. Use chunk paths like \
+3 -3
View File
@@ -90,8 +90,8 @@ pub enum ChunkReadStatus {
pub enum ChunkRegion {
#[napi(value = "head")]
Head,
#[napi(value = "inner")]
Inner,
#[napi(value = "body")]
Body,
#[napi(value = "tail")]
Tail,
}
@@ -100,7 +100,7 @@ impl ChunkRegion {
pub const fn as_str(self) -> &'static str {
match self {
Self::Head => "head",
Self::Inner => "inner",
Self::Body => "body",
Self::Tail => "tail",
}
}
+13 -13
View File
@@ -2205,7 +2205,7 @@ export class AuthCredentialStore {
}
#initializeSchema(): void {
this.#db.exec(`
this.#db.run(`
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA busy_timeout=5000;
@@ -2276,7 +2276,7 @@ export class AuthCredentialStore {
}
#createAuthCredentialsTable(): void {
this.#db.exec(`
this.#db.run(`
CREATE TABLE IF NOT EXISTS auth_credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider TEXT NOT NULL,
@@ -2292,7 +2292,7 @@ export class AuthCredentialStore {
}
#createAuthCredentialIndexes(): void {
this.#db.exec(`
this.#db.run(`
CREATE INDEX IF NOT EXISTS idx_auth_provider ON auth_credentials(provider);
CREATE INDEX IF NOT EXISTS idx_auth_provider_identity ON auth_credentials(provider, identity_key) WHERE identity_key IS NOT NULL;
`);
@@ -2315,8 +2315,8 @@ export class AuthCredentialStore {
const v0Cols = this.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>;
const hasDisabled = v0Cols.some(col => col.name === "disabled");
this.#db.exec("ALTER TABLE auth_credentials RENAME TO auth_credentials_v0");
this.#db.exec(`
this.#db.run("ALTER TABLE auth_credentials RENAME TO auth_credentials_v0");
this.#db.run(`
CREATE TABLE auth_credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider TEXT NOT NULL,
@@ -2327,7 +2327,7 @@ export class AuthCredentialStore {
updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
);
`);
this.#db.exec(`
this.#db.run(`
INSERT INTO auth_credentials (id, provider, credential_type, data, disabled_cause, created_at, updated_at)
SELECT
id,
@@ -2339,16 +2339,16 @@ export class AuthCredentialStore {
updated_at
FROM auth_credentials_v0
`);
this.#db.exec("DROP TABLE auth_credentials_v0");
this.#db.run("DROP TABLE auth_credentials_v0");
});
migrate();
}
#migrateAuthSchemaV1OrV2ToV3(): void {
const migrate = this.#db.transaction(() => {
this.#db.exec("ALTER TABLE auth_credentials RENAME TO auth_credentials_legacy");
this.#db.run("ALTER TABLE auth_credentials RENAME TO auth_credentials_legacy");
this.#createAuthCredentialsTable();
this.#db.exec(`
this.#db.run(`
INSERT INTO auth_credentials (id, provider, credential_type, data, disabled_cause, identity_key, created_at, updated_at)
SELECT
id,
@@ -2361,16 +2361,16 @@ export class AuthCredentialStore {
updated_at
FROM auth_credentials_legacy
`);
this.#db.exec("DROP TABLE auth_credentials_legacy");
this.#db.run("DROP TABLE auth_credentials_legacy");
});
migrate();
}
#migrateAuthSchemaV3ToV4(): void {
const migrate = this.#db.transaction(() => {
this.#db.exec("ALTER TABLE auth_credentials RENAME TO auth_credentials_v3");
this.#db.run("ALTER TABLE auth_credentials RENAME TO auth_credentials_v3");
this.#createAuthCredentialsTable();
this.#db.exec(`
this.#db.run(`
INSERT INTO auth_credentials (id, provider, credential_type, data, disabled_cause, identity_key, created_at, updated_at)
SELECT
id,
@@ -2383,7 +2383,7 @@ export class AuthCredentialStore {
updated_at
FROM auth_credentials_v3
`);
this.#db.exec("DROP TABLE auth_credentials_v3");
this.#db.run("DROP TABLE auth_credentials_v3");
});
migrate();
}
@@ -556,12 +556,12 @@ describe("Tool argument coercion", () => {
id: "call-heal-esc-1",
name: "heal-esc-1",
arguments: {
edits: '[{"target": "fn_foo#ABCD@inner", "content": "return 1;\\n"}\\n]',
edits: '[{"target": "fn_foo#ABCD@body", "content": "return 1;\\n"}\\n]',
},
};
const result = validateToolArguments(tool, toolCall);
expect(result.edits).toEqual([{ target: "fn_foo#ABCD@inner", content: "return 1;\n" }]);
expect(result.edits).toEqual([{ target: "fn_foo#ABCD@body", content: "return 1;\n" }]);
});
it("heals stringified array with trailing junk after balanced container", () => {
+16 -2
View File
@@ -9,8 +9,8 @@
### Added
- Auto QA tool (`report_tool_issue`) for automated tracking of unexpected tool behavior; enabled via `PI_AUTO_QA=1` environment variable or `pi.autoqa` setting
- `pi.autoqa` setting to enable automated tool issue reporting for all agents
- Auto QA tool (`report_tool_issue`) for automated tracking of unexpected tool behavior; enabled via `PI_AUTO_QA=1` environment variable or `dev.autoqa` setting
- `dev.autoqa` setting to enable automated tool issue reporting for all agents
- System prompt guidance when `report_tool_issue` tool is available, encouraging agents to report tool behavior discrepancies
- LSP server discovery at startup via `discoverStartupLspServers()` to detect configured language servers without blocking initialization
- LSP startup event channel (`lsp:startup`) for asynchronous server warmup notifications with completion or failure status
@@ -72,6 +72,18 @@
### Changed
- Chunk edit tool schema: renamed `target` parameter to `sel` for consistency with read tool terminology
- Chunk edit tool: `op` parameter is now required (previously optional with `replace` default)
- Chunk edit documentation: updated all region references from `@inner` to `@body` for clearer semantics
- Chunk edit documentation: expanded with comprehensive real-world examples showing full read output, operation effects, and indentation rules
- Chunk edit documentation: simplified indentation guidance to write content at indent-level 0 with automatic re-indentation by the tool
- Chunk edit documentation: clarified that `@region` only works on container chunks, not leaf chunks
- Chunk edit documentation: emphasized that CRCs change after every edit and must be refreshed from latest responses
- Read chunk tool documentation: updated selector examples to use `@body` instead of `@inner`
- Chunk edit region terminology updated: `@inner` renamed to `@body` for clearer semantics in container chunks
- Chunk edit documentation restructured with comprehensive examples showing full read output, operation effects, and indentation rules
- Chunk edit indentation guidance simplified: content should be written at indent-level 0 and the tool automatically applies correct base indentation
- Chunk edit examples expanded with realistic TypeScript code samples demonstrating replace, insert, prepend, append, and delete operations
- Python tool description now dynamically reflects prelude documentation availability instead of static text
- Python tool now automatically warms the environment on first execution if prelude helpers are unavailable, ensuring documentation is loaded before use
- Tool creation now auto-injects `report_tool_issue` when auto QA is enabled, regardless of requested tool list
@@ -102,6 +114,8 @@
### Fixed
- Chunk edit parameter validation: corrected detection of chunk edit operations to check for `sel` field instead of `target`
- Chunk edit streaming previews: updated to reference `sel` parameter instead of `target`
- Python prelude introspection now respects execution timeout and signal options, preventing hangs during environment warmup
- Welcome banner LSP server status now updates in real-time when background startup warmup completes, eliminating stale connecting status displays
- Welcome banner LSP startup rows now re-render when background warmup finishes, use the pending status symbol while servers are still connecting, and no longer add a redundant `LSP ready` status line on successful startup
+5 -28
View File
@@ -120,10 +120,6 @@ function fileLanguageTag(filePath: string, language?: string): string | undefine
return ext.length > 0 ? ext : undefined;
}
function resolveChunkTarget(target: string): ParsedChunkTarget {
return { selector: target };
}
async function resolveChunkSourceContext(session: ToolSession, path: string): Promise<ChunkSourceContext> {
const resolvedPath = resolvePlanPath(session, path);
const sourceFile = Bun.file(resolvedPath);
@@ -314,11 +310,11 @@ export function missingChunkReadTarget(selector: string): ChunkReadTarget {
const CHUNK_OP_VALUES = ["replace", "after", "before", "prepend", "append"] as const;
export const chunkToolEditSchema = Type.Object({
target: Type.String({
op: StringEnum(CHUNK_OP_VALUES),
sel: Type.String({
description:
"Chunk selector. Format: 'path@region' for insertions, 'path#CRC@region' for replace. Omit @region to target the full chunk. Valid regions: head, inner, tail.",
"Chunk selector. Format: 'path@region' for insertions, 'path#CRC@region' for replace. Omit @region to target the full chunk. Valid regions: head, body, tail.",
}),
op: Type.Optional(StringEnum(CHUNK_OP_VALUES)),
content: Type.String({
description: "New content. Use \\t for indentation. Do NOT include the chunk's base padding.",
}),
@@ -355,31 +351,12 @@ export function isChunkParams(params: unknown): params is ChunkParams {
params.edits.length > 0 &&
typeof params.edits[0] === "object" &&
params.edits[0] !== null &&
"target" in params.edits[0]
"sel" in params.edits[0]
);
}
function parseChunkTarget(target: string): ParsedChunkTarget {
return resolveChunkTarget(target);
}
function normalizeChunkEditOperation(edit: ChunkToolEdit): ChunkEditOperation {
const { selector } = parseChunkTarget(edit.target);
const op = edit.op ?? "replace";
const content = edit.content;
return {
op,
sel: selector,
content,
};
}
function normalizeChunkEditOperations(edits: ChunkToolEdit[]): ChunkEditOperation[] {
const operations: ChunkEditOperation[] = [];
for (const edit of edits) {
operations.push(normalizeChunkEditOperation(edit));
}
return operations;
return edits as ChunkEditOperation[];
}
async function writeChunkResult(params: {
+2 -2
View File
@@ -182,7 +182,7 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
}
function isChunkStreamingEdit(edit: Partial<HashlineToolEdit | ChunkToolEdit>): edit is Partial<ChunkToolEdit> {
return "target" in edit;
return "sel" in edit;
}
function getStreamingEditContent(content: unknown): string {
@@ -226,7 +226,7 @@ function formatChunkStreamingEdit(edit: Partial<ChunkToolEdit>): FormattedStream
}
const contentLines = getStreamingEditContent(edit.content);
const target = edit.target ?? "?";
const target = edit.sel ?? "?";
const op = edit.op ?? "replace";
switch (op) {
@@ -1,66 +1,206 @@
Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. The edit target is a chunk selector, optionally qualified with a region.
Edits files via syntax-aware chunks. Run `read(path="file.ts")` first. The edit selector is a chunk path, optionally qualified with a region.
<rules>
- **MUST** `read` first. Never invent chunk paths or CRCs. Copy them from the latest `read` output (opening lines start with `+++`) or edit response.
- `target` format:
- `sel` format:
- insertions: `chunk` or `chunk@region`
- replacements: `chunk#CRC` or `chunk#CRC@region`
- Without a `@region` it defaults to the entire chunk. Valid regions: `head`, `inner`, `tail`.
- Without a `@region` it defaults to the entire chunk. Valid regions: `head`, `body`, `tail`.
- If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing.
- Use `\t` for indentation in `content`. Do **NOT** include the chunk's base indentation. Only indent relative to the chunk's opening level.
- Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `@body` of a method, write the body starting at column 0:
```
content: "if (x) {\n\treturn true;\n}"
```
The tool adds the correct base indent automatically. Never manually pad with the chunk's own indentation.
- `@region` only works on container chunks (classes, functions, impl blocks, sections). Do **not** use `@head`/`@body`/`@tail` on leaf chunks (enum variants, fields, single statements) — use the whole chunk instead.
- `replace` requires the current CRC. Insertions do not.
- Successful edits return refreshed chunk anchors. Use the latest selectors/CRCs for follow-up edits.
- **CRCs change after every edit.** Always use the selectors/CRCs from the most recent `read` or edit response. Never reuse a CRC from a previous edit.
</rules>
<regions>
- `@head` — attached trivia, header/signature, and opening delimiter.
- `@inner` — the editable interior only.
- `@body` — the editable interior only.
- `@tail` — the closing delimiter or trailing owned trailer.
For leaf chunks (fields, variants, single-line items), `@inner` falls back to the full chunk.
For leaf chunks (fields, variants, single-line items), `@body` falls back to the full chunk.
**Important:** `append`/`prepend` without a `@region` inserts *outside* the chunk. To add children *inside* a class, struct, enum, or function body, use `@inner`:
- `class_Foo@inner` + `append` → adds inside the class before `}`
- `class_Foo@inner` + `prepend` → adds inside the class after `{`
**Important:** `append`/`prepend` without a `@region` inserts _outside_ the chunk. To add children _inside_ a class, struct, enum, or function body, use `@body`:
- `class_Foo@body` + `append` → adds inside the class before `}`
- `class_Foo@body` + `prepend` → adds inside the class after `{`
- `class_Foo` + `append` → adds after the entire class (after `}`)
</regions>
</regions>
<ops>
|op|target form|effect|
|op|sel|effect|
|---|---|---|
|`replace`|`chunk#CRC` or `chunk#CRC@region`|rewrite the addressed region|
|`before`|`chunk` or `chunk@region`|insert before the region span|
|`after`|`chunk` or `chunk@region`|insert after the region span|
|`prepend`|`chunk` or `chunk@region`|insert at the start inside the region|
|`append`|`chunk` or `chunk@region`|insert at the end inside the region|
|`replace`|`chunk#CRC(@region)?`|rewrite the addressed region|
|`before`|`chunk(@region)?`|insert before the region span|
|`after`|`chunk(@region)?`|insert after the region span|
|`prepend`|`chunk(@region)?`|insert at the start inside the region|
|`append`|`chunk(@region)?`|insert at the end inside the region|
</ops>
<examples>
- Replace only a function body without touching the closing brace:
- `target: "fn_main#ABCD@inner"`
- `op: "replace"`
- `content: "\treturn compute();\n"`
- Insert a new top-level function after another top-level function:
- `target: "fn_prev"`
- `op: "after"`
- `content: "function next(): void {\n\twork();\n}\n"`
- Add a struct field:
- `target: "type_Server@inner"`
- `op: "append"`
- `content: "\tport int\n"`
- Add a Go receiver method owned by the type, not a struct field:
- `target: "type_Server"`
- `op: "append"`
- `content: "func (s *Server) Stop() error {\n\treturn nil\n}\n"`
- Edit a doc comment or header block:
- `target: "fn_foo#WXYZ@head"`
- `op: "replace"`
- `content: "/**\n * Updated docs.\n */\nfunction foo() {"`
Given this `read` output for `example.ts`:
~~~
| example.ts·34L·ts·#QBMH
|
| +++interface_Config#BWTR
1| interface Config {
| +++interface_Config.field_host#TTMN
2| host: string;
| +++interface_Config.field_port#QSMH
3| port: number;
| +++interface_Config.field_debug#JPRR
4| debug: boolean;
5| }
|
| +++class_Counter#HZHY
7| class Counter {
| +++class_Counter.field_value#QJBY
8| value: number = 0;
9|
| +++class_Counter.fn_increment#NQWY
10| increment(): void {
11| this.value += 1;
12| }
13|
| +++class_Counter.fn_decrement#PMBP
14| decrement(): void {
15| this.value -= 1;
16| }
17|
| +++class_Counter.fn_toString#ZQZP
18| toString(): string {
19| return `Counter(${this.value})`;
20| }
21| }
|
| +++enum_Status#HYQJ
23| enum Status {
| +++enum_Status.variant_Active#PQNS
24| Active = "ACTIVE",
| +++enum_Status.variant_Paused#HHNM
25| Paused = "PAUSED",
| +++enum_Status.variant_Stopped#NHTY
26| Stopped = "STOPPED",
27| }
|
| +++fn_createCounter#PQQY
29| function createCounter(initial: number): Counter {
30| const counter = new Counter();
31| counter.value = initial;
32| return counter;
33| }
~~~
**Replace a whole chunk** (rename a function):
~~~json
{ "sel": "fn_createCounter#PQQY", "op": "replace", "content": "function makeCounter(start: number): Counter {\n\tconst c = new Counter();\n\tc.value = start;\n\treturn c;\n}\n" }
~~~
Result — the entire chunk is rewritten:
~~~
function makeCounter(start: number): Counter {
const c = new Counter();
c.value = start;
return c;
}
~~~
**Replace a method body** (`@body`):
~~~json
{ "sel": "class_Counter.fn_increment#NQWY@body", "op": "replace", "content": "this.value += 1;\nconsole.log('incremented to', this.value);\n" }
~~~
Result — only the body changes, signature and braces are kept:
~~~
increment(): void {
this.value += 1;
console.log('incremented to', this.value);
}
~~~
**Replace a function header** (`@head` — signature and doc comment):
~~~json
{ "sel": "fn_createCounter#PQQY@head", "op": "replace", "content": "/** Creates a counter with the given start value. */\nfunction createCounter(initial: number, label?: string): Counter {\n" }
~~~
Result — adds a doc comment and updates the signature, body untouched:
~~~
/** Creates a counter with the given start value. */
function createCounter(initial: number, label?: string): Counter {
const counter = new Counter();
counter.value = initial;
return counter;
}
~~~
**Insert before a chunk** (`before`):
~~~json
{ "sel": "fn_createCounter", "op": "before", "content": "/** Factory function below. */\n" }
~~~
Result — a comment is inserted before the function:
~~~
/** Factory function below. */
function createCounter(initial: number): Counter {
~~~
**Insert after a chunk** (`after`):
~~~json
{ "sel": "enum_Status", "op": "after", "content": "\nfunction isActive(s: Status): boolean {\n\treturn s === Status.Active;\n}\n" }
~~~
Result — a new function appears after the enum:
~~~
enum Status {
Active = "ACTIVE",
Paused = "PAUSED",
Stopped = "STOPPED",
}
function isActive(s: Status): boolean {
return s === Status.Active;
}
function createCounter(initial: number): Counter {
~~~
**Prepend inside a container** (`@body` + `prepend`):
~~~json
{ "sel": "class_Counter@body", "op": "prepend", "content": "label: string = 'default';\n\n" }
~~~
Result — a new field is added at the top of the class body, before existing members:
~~~
class Counter {
label: string = 'default';
value: number = 0;
~~~
**Append inside a container** (`@body` + `append`):
~~~json
{ "sel": "class_Counter@body", "op": "append", "content": "\nreset(): void {\n\tthis.value = 0;\n}\n" }
~~~
Result — a new method is added at the end of the class body, before the closing `}`:
~~~
toString(): string {
return `Counter(${this.value})`;
}
reset(): void {
this.value = 0;
}
}
~~~
**Delete a chunk** (`replace` with empty content):
~~~json
{ "sel": "class_Counter.fn_toString#ZQZP", "op": "replace", "content": "" }
~~~
Result — the method is removed from the class.
- Indentation rules (important):
- Use `\t` for each indent level. The tool converts tabs to the file's actual style (2-space, 4-space, etc.).
- Do NOT include the chunk's base indentation — only indent relative to the region's opening level.
- For `@inner` of a function: `\t` = one level inside the body. Write `"\treturn x;\n"`, not `"\t\t\treturn x;\n"`.
- For `@head`: `\t` = one level at the chunk's own depth. A class member's head uses `"\t/** doc */\n\tstart(): void {"`.
- For `@body` of a function: write at column 0, e.g. `"return x;\n"`. The tool adds the correct base indent.
- For `@head`: write at the chunk's own depth. A class member's head uses `"/** doc */\nstart(): void {"`.
- For a top-level item: start at zero indent. Write `"function foo() {\n\treturn 1;\n}\n"`.
- The tool strips common leading indentation from your content as a safety net, so accidental over-indentation is corrected.
</examples>
@@ -2,7 +2,7 @@ Reads files using syntax-aware chunks.
<instruction>
- `path` — file path or URL; may include `:selector` suffix
- `sel` — optional selector: `class_Foo`, `class_Foo.fn_bar#ABCD@inner`, `?`, `L50`, `L50-L120`, or `raw`
- `sel` — optional selector: `class_Foo`, `class_Foo.fn_bar#ABCD@body`, `?`, `L50`, `L50-L120`, or `raw`
- `timeout` — seconds, for URLs only
Each opening anchor `+++ full.chunk.path#CCCC` in the default output identifies a chunk (with matching closers like `--- /full.chunk.path#CCCC`). Use `full.chunk.path#CCCC` as-is to read truncated chunks.
@@ -73,7 +73,7 @@ export class AgentStorage {
* AuthCredentialStore handles auth_credentials and cache tables.
*/
#initializeSchema(): void {
this.#db.exec(`
this.#db.run(`
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA busy_timeout=5000;
@@ -92,7 +92,7 @@ CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);
const hasValue = settingsInfo.some(column => column.name === "value");
if (!hasSettingsTable) {
this.#db.exec(`
this.#db.run(`
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
@@ -117,8 +117,8 @@ CREATE TABLE settings (
}
const migrate = this.#db.transaction((settings: Record<string, unknown> | null) => {
this.#db.exec("DROP TABLE settings");
this.#db.exec(`
this.#db.run("DROP TABLE settings");
this.#db.run(`
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
@@ -169,34 +169,34 @@ CREATE TABLE settings (
#migrateSchemaV4ToV5(): void {
const migrate = this.#db.transaction(() => {
this.#db.exec("ALTER TABLE settings RENAME TO settings_legacy");
this.#db.exec(`
this.#db.run("ALTER TABLE settings RENAME TO settings_legacy");
this.#db.run(`
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
);
`);
this.#db.exec(`
this.#db.run(`
INSERT INTO settings (key, value, updated_at)
SELECT key, value, updated_at
FROM settings_legacy
`);
this.#db.exec("DROP TABLE settings_legacy");
this.#db.run("DROP TABLE settings_legacy");
this.#db.exec("ALTER TABLE model_usage RENAME TO model_usage_legacy");
this.#db.exec(`
this.#db.run("ALTER TABLE model_usage RENAME TO model_usage_legacy");
this.#db.run(`
CREATE TABLE model_usage (
model_key TEXT PRIMARY KEY,
last_used_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
);
`);
this.#db.exec(`
this.#db.run(`
INSERT INTO model_usage (model_key, last_used_at)
SELECT model_key, last_used_at
FROM model_usage_legacy
`);
this.#db.exec("DROP TABLE model_usage_legacy");
this.#db.run("DROP TABLE model_usage_legacy");
});
migrate();
}
@@ -39,7 +39,7 @@ export class HistoryStorage {
const hasFts = this.#db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='history_fts'").get();
this.#db.exec(`
this.#db.run(`
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA busy_timeout=5000;
@@ -150,11 +150,11 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
#migrateHistorySchema(): void {
const migrate = this.#db.transaction(() => {
this.#db.exec("ALTER TABLE history RENAME TO history_legacy");
this.#db.exec("DROP INDEX IF EXISTS idx_history_created_at");
this.#db.exec("DROP TRIGGER IF EXISTS history_ai");
this.#db.exec("DROP TABLE IF EXISTS history_fts");
this.#db.exec(`
this.#db.run("ALTER TABLE history RENAME TO history_legacy");
this.#db.run("DROP INDEX IF EXISTS idx_history_created_at");
this.#db.run("DROP TRIGGER IF EXISTS history_ai");
this.#db.run("DROP TABLE IF EXISTS history_fts");
this.#db.run(`
CREATE TABLE history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prompt TEXT NOT NULL,
@@ -97,11 +97,7 @@ function getChecksum(source: string, chunkPath: string, language = "typescript")
return chunk.checksum;
}
function targetWithChecksum(
chunkPath: string,
checksum: string,
region?: "head" | "inner" | "tail",
): string {
function targetWithChecksum(chunkPath: string, checksum: string, region?: "head" | "body" | "tail"): string {
return `${chunkPath}#${checksum}${region ? `@${region}` : ""}`;
}
@@ -109,13 +105,13 @@ function currentTarget(
source: string,
chunkPath: string,
language = "typescript",
region?: "head" | "inner" | "tail",
region?: "head" | "body" | "tail",
): string {
return targetWithChecksum(chunkPath, getChecksum(source, chunkPath, language), region);
}
function bodyTarget(chunkPath: string): string {
return `${chunkPath}@inner`;
return `${chunkPath}@body`;
}
describe("applyChunkEdits", () => {
@@ -1084,9 +1080,9 @@ describe("prepend warnings", () => {
language: "go",
cwd: "/",
filePath: "main.go",
operations: [{ op: "prepend", sel: "@inner", content: "// AUTO-GENERATED\n" }],
operations: [{ op: "prepend", sel: "@body", content: "// AUTO-GENERATED\n" }],
});
expect(result.warnings.some(w => w.includes("Comment-only @inner.prepend"))).toBe(true);
expect(result.warnings.some(w => w.includes("Comment-only @body.prepend"))).toBe(true);
});
});
@@ -1147,9 +1143,9 @@ describe("prepend preamble guard", () => {
language: "javascript",
cwd: "/",
filePath: "index.js",
operations: [{ op: "prepend", sel: "@inner", content: "// AUTO-GENERATED\n" }],
operations: [{ op: "prepend", sel: "@body", content: "// AUTO-GENERATED\n" }],
}),
).toThrow(/Comment-only @inner.prepend on root is not allowed when the file has a preamble/);
).toThrow(/Comment-only @body.prepend on root is not allowed when the file has a preamble/);
});
});
@@ -218,7 +218,8 @@ describe("chunk mode tools", () => {
path: filePath,
edits: [
{
target: `class_Server.fn_handleError#${checksum}`,
sel: `class_Server.fn_handleError#${checksum}`,
op: "replace",
content: ` private handleError(err: Error): string {
return \`normalized:\${err.message}\`;
}
@@ -262,11 +263,13 @@ describe("chunk mode tools", () => {
path: filePath,
edits: [
{
target: `${chunkPath}#${checksum}`,
sel: `${chunkPath}#${checksum}`,
op: "replace",
content: buildHandleErrorMethod({ returnLine: " return err.message.toUpperCase() + total;" }),
},
{
target: `${chunkPath}#${checksum2}`,
sel: `${chunkPath}#${checksum2}`,
op: "replace",
content: buildHandleErrorMethod({
totalInitLine: " let total = 1;",
returnLine: " return err.message.toUpperCase() + total;",
@@ -310,7 +313,7 @@ describe("chunk mode tools", () => {
path: filePath,
edits: [
{
target: "class_Server@inner",
sel: "class_Server@body",
op: "append",
content: 'status(): string {\n return "ok";\n}\n',
},
@@ -332,7 +335,7 @@ describe("chunk mode tools", () => {
await editTool.execute("chunk-edit-empty-replace-delete", {
path: filePath,
edits: [{ target: `fn_main#${checksum}`, content: "" }],
edits: [{ sel: `fn_main#${checksum}`, op: "replace", content: "" }],
});
const updatedSource = await Bun.file(filePath).text();
@@ -366,8 +369,8 @@ describe("chunk mode tools", () => {
editTool.execute("chunk-edit-batch-rollback", {
path: filePath,
edits: [
{ target: "class_Server", op: "append", content: ' status(): string {\n return "ok";\n }' },
{ target: "class_Server.fn_handleError#ZZZZ", content: "" },
{ sel: "class_Server", op: "append", content: ' status(): string {\n return "ok";\n }' },
{ sel: "class_Server.fn_handleError#ZZZZ", op: "replace", content: "" },
],
}),
).rejects.toThrow(/No changes were saved/);
@@ -388,7 +391,8 @@ describe("chunk mode tools", () => {
path: filePath,
edits: [
{
target: `class_Server.fn_handleError#${checksum}`,
sel: `class_Server.fn_handleError#${checksum}`,
op: "replace",
content: " private handleError(err: Error): string {\n if (err) {\n",
},
],
@@ -410,11 +414,13 @@ describe("chunk mode tools", () => {
path: filePath,
edits: [
{
target: `class_Server.fn_handleError#${checksum}`,
sel: `class_Server.fn_handleError#${checksum}`,
op: "replace",
content: " private handleError(err: Error): string {\n return err.message;\n }",
},
{
target: `class_Server.fn_handleError#${checksum}`,
sel: `class_Server.fn_handleError#${checksum}`,
op: "replace",
content: " private handleError(err: Error): string {\n return err.message.toUpperCase();\n }",
},
],
@@ -436,7 +442,8 @@ describe("chunk mode tools", () => {
path: filePath,
edits: [
{
target: "class_Server.fn_handleError",
sel: "class_Server.fn_handleError",
op: "replace",
content: buildHandleErrorMethod({ totalInitLine: " let total = 1;" }),
},
],
@@ -456,7 +463,13 @@ describe("chunk mode tools", () => {
// Use bare "main" instead of "fn_main"
const _result = await editTool.execute("chunk-edit-prefix-resolve", {
path: filePath,
edits: [{ target: `main#${checksum}`, content: 'function main(): void {\n console.log("started");\n}\n' }],
edits: [
{
sel: `main#${checksum}`,
op: "replace",
content: 'function main(): void {\n console.log("started");\n}\n',
},
],
});
const updatedSource = await Bun.file(filePath).text();
expect(updatedSource).toContain('console.log("started")');
@@ -502,7 +515,8 @@ describe("chunk mode tools", () => {
path: filePath,
edits: [
{
target: `${building.path}#${building.checksum}`,
sel: `${building.path}#${building.checksum}`,
op: "replace",
content: "## Building and Testing\n\nUse `just verify` instead. It wraps cmake and ctest.\n",
},
],
+2
View File
@@ -1,8 +1,10 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Changed `ChunkRegion.Inner` enum value to `ChunkRegion.Body` to align with region semantics
- Changed `ChunkRegion` enum values from `Container`, `Prologue`, `Body`, `Epilogue` to `Head`, `Inner`, `Tail` with updated semantics for region targeting
- Replaced `ChunkEditOp` enum values — `AppendChild`, `PrependChild`, `AppendSibling`, `PrependSibling`, and `ReplaceBody` are now `Before`, `After`, `Prepend`, and `Append` with updated semantics for region-scoped operations
- Removed `ReplaceBody` operation — use `Replace` with `region: ChunkRegion.Body` to replace only chunk body content
+1 -1
View File
@@ -444,7 +444,7 @@ export interface ChunkReadTarget {
export declare enum ChunkRegion {
Head = 'head',
Inner = 'inner',
Body = 'body',
Tail = 'tail'
}
+1 -1
View File
@@ -259,7 +259,7 @@ exports.ChunkReadStatus = {
};
exports.ChunkRegion = {
Head: 'head',
Inner: 'inner',
Body: 'body',
Tail: 'tail',
};
exports.Ellipsis = {