fix(mcp): backfill 按落盘路径归并——同路径重复登记互相覆盖丢笔记(真机实锤丢 20 条)
All checks were successful
Push & PR Check / check (push) Successful in 1m4s
All checks were successful
Push & PR Check / check (push) Successful in 1m4s
回填改为先解析路径再归并写入(按 note id 排序),同路径多登记合并为一个文件; 新增回归:两登记同路径 → 4 条全归档不覆盖。cargo test notes_ 全绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
87494ba055
commit
01c9433a08
@ -1125,37 +1125,55 @@ fn backfill_notes_archive(conn: &rusqlite::Connection) -> Result<String, String>
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
let mut by_project: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
|
||||
let mut by_project: std::collections::BTreeMap<String, Vec<(i64, String)>> = std::collections::BTreeMap::new();
|
||||
for (id, pid, source, content, created_at) in &rows {
|
||||
by_project
|
||||
.entry(pid.clone())
|
||||
.or_default()
|
||||
.push(note_jsonl_line(*id, pid, source, content, created_at));
|
||||
.push((*id, note_jsonl_line(*id, pid, source, content, created_at)));
|
||||
}
|
||||
|
||||
let _guard = NOTES_ARCHIVE_LOCK.lock().map_err(|e| e.to_string())?;
|
||||
// 按「解析后的落盘路径」归并——同路径重复登记(mcp-single-server 已知遗留)指向同一文件,
|
||||
// 若按 project_id 逐项目整写会互相覆盖(2026-09-11 真机回填实锤:后写覆盖先写丢 20 条)
|
||||
let mut by_path: std::collections::BTreeMap<std::path::PathBuf, (Vec<String>, Vec<(i64, String)>)> =
|
||||
std::collections::BTreeMap::new();
|
||||
let mut report = String::from("## notes 档案回填(全量重建)\n\n");
|
||||
let (mut archived, mut skipped) = (0usize, 0usize);
|
||||
for (pid, lines) in &by_project {
|
||||
match notes_archive_path(conn, pid) {
|
||||
let mut skipped = 0usize;
|
||||
for (pid, entries) in by_project {
|
||||
match notes_archive_path(conn, &pid) {
|
||||
Ok(path) => {
|
||||
if let Some(dir) = path.parent() {
|
||||
std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
std::fs::write(&path, lines.join("\n") + "\n").map_err(|e| e.to_string())?;
|
||||
report.push_str(&format!("- ✅ {}:{} 条 → {}\n", pid, lines.len(), path.display()));
|
||||
archived += 1;
|
||||
let slot = by_path.entry(path).or_default();
|
||||
slot.0.push(pid);
|
||||
slot.1.extend(entries);
|
||||
}
|
||||
Err(e) => {
|
||||
report.push_str(&format!("- ⚠️ {}:跳过({}),{} 条仅存 DB\n", pid, e, lines.len()));
|
||||
report.push_str(&format!("- ⚠️ {}:跳过({}),{} 条仅存 DB\n", pid, e, entries.len()));
|
||||
skipped += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _guard = NOTES_ARCHIVE_LOCK.lock().map_err(|e| e.to_string())?;
|
||||
let mut archived_notes = 0usize;
|
||||
for (path, (pids, mut entries)) in by_path {
|
||||
entries.sort_by_key(|(id, _)| *id);
|
||||
if let Some(dir) = path.parent() {
|
||||
std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let lines: Vec<&str> = entries.iter().map(|(_, l)| l.as_str()).collect();
|
||||
std::fs::write(&path, lines.join("\n") + "\n").map_err(|e| e.to_string())?;
|
||||
archived_notes += entries.len();
|
||||
report.push_str(&format!(
|
||||
"\n共 {} 条笔记;{} 个项目已归档,{} 个跳过(跳过项待修复登记路径后重跑即可)。",
|
||||
"- ✅ {}:{} 条 → {}\n",
|
||||
pids.join(" + "),
|
||||
entries.len(),
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
report.push_str(&format!(
|
||||
"\n共 {} 条笔记,{} 条已归档,{} 个项目登记被跳过(修复登记路径后重跑即可)。",
|
||||
rows.len(),
|
||||
archived,
|
||||
archived_notes,
|
||||
skipped
|
||||
));
|
||||
Ok(report)
|
||||
@ -1513,6 +1531,36 @@ mod tests {
|
||||
assert_eq!(count2, 3, "重跑不应翻倍");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notes_backfill_merges_duplicate_registrations_same_path() {
|
||||
// 同路径重复登记(mcp-single-server 已知遗留):两条 project 记录指向同一目录,
|
||||
// 回填必须归并写入而非互相覆盖(2026-09-11 真机实锤丢 20 条的回归)
|
||||
let conn = rusqlite::Connection::open_in_memory().unwrap();
|
||||
setup_notes_schema(&conn);
|
||||
let base = temp_base("duppath");
|
||||
for pid in ["uuid-form", "name-form"] {
|
||||
conn.execute(
|
||||
"INSERT INTO project_workspaces (id, win_path) VALUES (?1, ?2)",
|
||||
rusqlite::params![pid, base.to_str().unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
for i in 0..3 {
|
||||
conn.execute(
|
||||
"INSERT INTO project_notes (project_id, content) VALUES ('uuid-form', ?1)",
|
||||
[format!("a-{i}")],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
conn.execute("INSERT INTO project_notes (project_id, content) VALUES ('name-form', 'b-0')", []).unwrap();
|
||||
|
||||
let report = backfill_notes_archive(&conn).expect("回填应成功");
|
||||
let file = base.join(".blueprint").join("notes-archive.jsonl");
|
||||
let text = std::fs::read_to_string(&file).unwrap();
|
||||
assert_eq!(text.lines().count(), 4, "同路径两登记的笔记应全部归并: {report}");
|
||||
assert!(report.contains("4 条已归档"), "报告应按笔记数计数: {report}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notes_archive_concurrent_appends_no_interleave() {
|
||||
let base = temp_base("concurrent");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user