admin: add bulk-rename command, remove # from all keys
Add `poc-memory admin bulk-rename FROM TO [--apply]` for bulk key character replacement. Uses rename_node() per key for proper capnp log persistence. Collision detection, progress reporting, auto-fsck. Applied: renamed 13,042 keys from # to - separator. This fixes the Claude Bash tool's inability to pass # in command arguments (the model confabulates that quoting doesn't work and gives up). 7 collision pairs resolved by deleting the # version before rename. 209 orphan edges pruned by fsck. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
e74f403192
commit
f8221286da
1 changed files with 79 additions and 0 deletions
|
|
@ -608,6 +608,17 @@ enum AdminCmd {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
apply: bool,
|
apply: bool,
|
||||||
},
|
},
|
||||||
|
/// Bulk rename: replace a character in all keys
|
||||||
|
#[command(name = "bulk-rename")]
|
||||||
|
BulkRename {
|
||||||
|
/// Character to replace
|
||||||
|
from: String,
|
||||||
|
/// Replacement character
|
||||||
|
to: String,
|
||||||
|
/// Apply changes (default: dry run)
|
||||||
|
#[arg(long)]
|
||||||
|
apply: bool,
|
||||||
|
},
|
||||||
/// Brief metrics check (for cron/notifications)
|
/// Brief metrics check (for cron/notifications)
|
||||||
#[command(name = "daily-check")]
|
#[command(name = "daily-check")]
|
||||||
DailyCheck,
|
DailyCheck,
|
||||||
|
|
@ -809,6 +820,7 @@ fn main() {
|
||||||
AdminCmd::Health => cmd_health(),
|
AdminCmd::Health => cmd_health(),
|
||||||
AdminCmd::Fsck => cmd_fsck(),
|
AdminCmd::Fsck => cmd_fsck(),
|
||||||
AdminCmd::Dedup { apply } => cmd_dedup(apply),
|
AdminCmd::Dedup { apply } => cmd_dedup(apply),
|
||||||
|
AdminCmd::BulkRename { from, to, apply } => cmd_bulk_rename(&from, &to, apply),
|
||||||
AdminCmd::DailyCheck => cmd_daily_check(),
|
AdminCmd::DailyCheck => cmd_daily_check(),
|
||||||
AdminCmd::Import { files } => cmd_import(&files),
|
AdminCmd::Import { files } => cmd_import(&files),
|
||||||
AdminCmd::Export { files, all } => cmd_export(&files, all),
|
AdminCmd::Export { files, all } => cmd_export(&files, all),
|
||||||
|
|
@ -1014,6 +1026,73 @@ fn cmd_init() -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cmd_bulk_rename(from: &str, to: &str, apply: bool) -> Result<(), String> {
|
||||||
|
let mut store = store::Store::load()?;
|
||||||
|
|
||||||
|
// Find all keys that need renaming
|
||||||
|
let renames: Vec<(String, String)> = store.nodes.keys()
|
||||||
|
.filter(|k| k.contains(from))
|
||||||
|
.map(|k| (k.clone(), k.replace(from, to)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Check for collisions
|
||||||
|
let existing: std::collections::HashSet<&String> = store.nodes.keys().collect();
|
||||||
|
let mut collisions = 0;
|
||||||
|
for (old, new) in &renames {
|
||||||
|
if existing.contains(new) && old != new {
|
||||||
|
eprintln!("COLLISION: {} -> {} (target exists)", old, new);
|
||||||
|
collisions += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Bulk rename '{}' -> '{}'", from, to);
|
||||||
|
println!(" Keys to rename: {}", renames.len());
|
||||||
|
println!(" Collisions: {}", collisions);
|
||||||
|
|
||||||
|
if collisions > 0 {
|
||||||
|
return Err(format!("{} collisions — aborting", collisions));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apply {
|
||||||
|
// Show a sample
|
||||||
|
for (old, new) in renames.iter().take(10) {
|
||||||
|
println!(" {} -> {}", old, new);
|
||||||
|
}
|
||||||
|
if renames.len() > 10 {
|
||||||
|
println!(" ... and {} more", renames.len() - 10);
|
||||||
|
}
|
||||||
|
println!("\nDry run. Use --apply to execute.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply renames using rename_node() which properly appends to capnp logs.
|
||||||
|
// Process in batches to avoid holding the lock too long.
|
||||||
|
let mut renamed_count = 0;
|
||||||
|
let mut errors = 0;
|
||||||
|
let total = renames.len();
|
||||||
|
for (i, (old_key, new_key)) in renames.iter().enumerate() {
|
||||||
|
match store.rename_node(old_key, new_key) {
|
||||||
|
Ok(()) => renamed_count += 1,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" RENAME ERROR: {} -> {}: {}", old_key, new_key, e);
|
||||||
|
errors += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i + 1) % 1000 == 0 {
|
||||||
|
println!(" {}/{} ({} errors)", i + 1, total, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
store.save()?;
|
||||||
|
println!("Renamed {} nodes ({} errors).", renamed_count, errors);
|
||||||
|
|
||||||
|
// Run fsck to verify
|
||||||
|
println!("\nRunning fsck...");
|
||||||
|
drop(store);
|
||||||
|
cmd_fsck()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn install_default_file(data_dir: &std::path::Path, name: &str, content: &str) -> Result<(), String> {
|
fn install_default_file(data_dir: &std::path::Path, name: &str, content: &str) -> Result<(), String> {
|
||||||
let path = data_dir.join(name);
|
let path = data_dir.join(name);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue