feat: integrate pi rust lab runtime

This commit is contained in:
Agent Board
2026-07-10 10:54:34 +08:00
parent c8472bb898
commit 896c94b696
73 changed files with 19852 additions and 1152 deletions
@@ -56,6 +56,11 @@ static LOCAL_PAGE_TREE_SNAPSHOT_CACHE: OnceLock<
Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>>,
> = OnceLock::new();
#[cfg(not(test))]
static LOCAL_SEARCH_INDEX_CONTROL_PLANE: OnceLock<
std::sync::Arc<dyn control_plane::ControlPlaneStore>,
> = OnceLock::new();
fn local_page_tree_snapshot_cache(
) -> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
@@ -3363,13 +3368,26 @@ fn save_local_markdown_page_inner(
}
fn refresh_local_search_index_best_effort(root: &Path, root_uri: &str, workspace_id: &str) {
let control_plane = open_control_plane_store();
let _ = local_search_index::refresh_local_search_index_for_change_with_store(
control_plane.as_ref(),
root,
root_uri,
workspace_id,
);
#[cfg(test)]
{
let control_plane = open_control_plane_store();
let _ = local_search_index::refresh_local_search_index_for_change_with_store(
control_plane.as_ref(),
root,
root_uri,
workspace_id,
);
}
#[cfg(not(test))]
{
let control_plane = LOCAL_SEARCH_INDEX_CONTROL_PLANE.get_or_init(open_control_plane_store);
let _ = local_search_index::refresh_local_search_index_for_change_with_store(
control_plane.as_ref(),
root,
root_uri,
workspace_id,
);
}
}
fn local_markdown_conflict_error(
@@ -4605,13 +4623,20 @@ fn add_local_file_operation_contract(root_uri: &str, action: &str, mut result: V
let previous_relative_path = local_tree_previous_relative_path(&result);
let document_id = local_tree_result_string(&result, "documentId")
.or_else(|| local_tree_result_string(&result, "id"));
let resource_kind = local_tree_result_string(&result, "resourceKind");
let previous_document_id =
local_tree_result_string(&result, "previousDocumentId").or_else(|| {
previous_relative_path
.as_deref()
.map(local_markdown_path_page_id)
if matches!(
resource_kind.as_deref(),
Some("markdown") | Some("markdown_bundle")
) {
document_id.clone()
} else {
previous_relative_path
.as_deref()
.map(local_markdown_path_page_id)
}
});
let resource_kind = local_tree_result_string(&result, "resourceKind");
let affected_parents = add_local_tree_command_affected_parents(&result, &resolved_action);
let resource = relative_path.as_deref().and_then(|path| {
local_file_operation_resource(path, document_id.clone(), resource_kind.as_deref())
@@ -5529,6 +5554,15 @@ fn move_local_directory(
}
fn trash_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
if let Some(idempotent) = idempotent_deleted_local_directory(root, entry_id)? {
return Ok(idempotent);
}
if let Some(idempotent) = idempotent_deleted_local_markdown_bundle_directory(root, entry_id)? {
return Ok(idempotent);
}
if let Some(idempotent) = idempotent_deleted_local_raw_file(root, entry_id)? {
return Ok(idempotent);
}
if let Some(directory) = resolve_local_directory_id(root, entry_id)? {
// 若目录是页面 bundle(包含同名 .md),按 Markdown 页面生命周期入 trash
if let Some(main_md) = nested_bundle_main_markdown(&directory) {
@@ -5552,6 +5586,12 @@ fn restore_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
if find_local_file_trash_entry_key(&metadata, entry_id).is_some() {
return restore_local_raw_file(root, entry_id);
}
if let Some(idempotent) = idempotent_restored_local_directory(root, entry_id)? {
return Ok(idempotent);
}
if let Some(idempotent) = idempotent_restored_local_raw_file(root, entry_id)? {
return Ok(idempotent);
}
restore_local_markdown_page(root, entry_id)
}
@@ -5563,6 +5603,12 @@ fn purge_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
if find_local_file_trash_entry_key(&metadata, entry_id).is_some() {
return purge_local_raw_file(root, entry_id);
}
if let Some(idempotent) = idempotent_purged_local_directory(root, entry_id)? {
return Ok(idempotent);
}
if let Some(idempotent) = idempotent_purged_local_raw_file(root, entry_id)? {
return Ok(idempotent);
}
if entry_id.starts_with("local:asset:") || entry_id.starts_with("local:node:") {
return Err(WebError::bad_request_code(
"local_trash_entry_not_found",
@@ -5574,13 +5620,20 @@ fn purge_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
fn trash_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, WebError> {
let mut metadata = load_local_folder_metadata(root)?;
let markdown_file =
find_markdown_by_page_id(root, &metadata, document_id)?.ok_or_else(|| {
WebError::bad_request_code(
let markdown_file = match find_markdown_by_page_id(root, &metadata, document_id)? {
Some(markdown_file) => markdown_file,
None => {
if let Some(idempotent) =
idempotent_deleted_local_markdown_page(root, &metadata, document_id)?
{
return Ok(idempotent);
}
return Err(WebError::bad_request_code(
"local_markdown_not_found",
"找不到要删除的本地 Markdown 页面",
)
})?;
));
}
};
let trash_dir = root.join(".mnote").join("trash");
fs::create_dir_all(&trash_dir).map_err(|error| {
WebError::bad_request_code(
@@ -5660,6 +5713,95 @@ fn trash_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, We
}))
}
fn idempotent_deleted_local_markdown_page(
root: &Path,
metadata: &LocalFolderMetadata,
document_id: &str,
) -> Result<Option<Value>, WebError> {
let Some(trash_entry) = metadata.trash_entries.get(document_id) else {
return Ok(None);
};
if trash_entry.resource_kind != "markdown" && trash_entry.resource_kind != "markdown_bundle" {
return Ok(None);
}
let trash_path = resolve_metadata_relative_path(root, &trash_entry.trash_relative_path)?;
let trash_exists = if trash_entry.resource_kind == "markdown_bundle" {
trash_path.is_dir()
} else {
trash_path.is_file()
};
if !trash_exists {
return Ok(None);
}
Ok(Some(json!({
"ok": true,
"id": document_id,
"documentId": document_id,
"resourceKind": trash_entry.resource_kind,
"originalRelativePath": trash_entry.original_relative_path,
"trashPath": trash_entry.trash_relative_path,
"action": "delete",
"sourceKind": "local_folder",
"idempotent": true,
})))
}
fn idempotent_deleted_local_markdown_bundle_directory(
root: &Path,
entry_id: &str,
) -> Result<Option<Value>, WebError> {
let Some(relative_path) = local_directory_relative_path_from_entry_id(entry_id) else {
return Ok(None);
};
let metadata = load_local_folder_metadata(root)?;
let Some((_, trash_entry)) = metadata.trash_entries.iter().find(|(_, entry)| {
entry.resource_kind == "markdown_bundle" && entry.original_relative_path == relative_path
}) else {
return Ok(None);
};
let trash_path = resolve_metadata_relative_path(root, &trash_entry.trash_relative_path)?;
if !trash_path.is_dir() {
return Ok(None);
}
Ok(Some(json!({
"ok": true,
"id": entry_id,
"documentId": trash_entry.document_id,
"resourceKind": "markdown_bundle",
"resourceScope": "local_folder",
"originalRelativePath": trash_entry.original_relative_path,
"originalFilePath": local_trash_original_path(trash_entry),
"trashEntryId": trash_entry.trash_entry_id,
"trashPath": trash_entry.trash_relative_path,
"action": "delete",
"canonicalCommand": "tree.resource.archive",
"sourceKind": "local_folder",
"idempotent": true,
})))
}
fn idempotent_purged_local_markdown_page(
root: &Path,
document_id: &str,
) -> Result<Option<Value>, WebError> {
let Some(relative_path) = local_markdown_relative_path_from_document_id(document_id) else {
return Ok(None);
};
let markdown_path = resolve_metadata_relative_path(root, &relative_path)?;
if markdown_path.exists() {
return Ok(None);
}
Ok(Some(json!({
"ok": true,
"id": document_id,
"documentId": document_id,
"originalRelativePath": relative_path,
"action": "purge",
"sourceKind": "local_folder",
"idempotent": true,
})))
}
fn trash_local_raw_file(root: &Path, entry_id: &str, file: &Path) -> Result<Value, WebError> {
let mut metadata = load_local_folder_metadata(root)?;
let relative_path = normalize_relative_path(root, file)?;
@@ -5725,6 +5867,39 @@ fn trash_local_raw_file(root: &Path, entry_id: &str, file: &Path) -> Result<Valu
}))
}
fn idempotent_deleted_local_raw_file(
root: &Path,
entry_id: &str,
) -> Result<Option<Value>, WebError> {
let metadata = load_local_folder_metadata(root)?;
let Some(trash_key) = find_local_file_trash_entry_key(&metadata, entry_id) else {
return Ok(None);
};
let Some(trash_entry) = metadata.trash_entries.get(&trash_key) else {
return Ok(None);
};
let trash_relative_path = local_trash_file_path(trash_entry);
let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?;
if !trash_path.is_file() {
return Ok(None);
}
Ok(Some(json!({
"ok": true,
"id": entry_id,
"documentId": entry_id,
"resourceKind": "local_file",
"resourceScope": "local_folder",
"originalRelativePath": trash_entry.original_relative_path,
"originalFilePath": local_trash_original_path(trash_entry),
"trashEntryId": trash_key,
"trashPath": trash_relative_path,
"action": "delete",
"canonicalCommand": "tree.resource.archive",
"sourceKind": "local_folder",
"idempotent": true,
})))
}
fn trash_local_directory(root: &Path, entry_id: &str, directory: &Path) -> Result<Value, WebError> {
if directory == root {
return Err(WebError::bad_request_code(
@@ -5801,16 +5976,18 @@ fn trash_local_directory(root: &Path, entry_id: &str, directory: &Path) -> Resul
fn restore_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, WebError> {
let mut metadata = load_local_folder_metadata(root)?;
let trash_entry = metadata
.trash_entries
.get(document_id)
.cloned()
.ok_or_else(|| {
WebError::bad_request_code(
let trash_entry = match metadata.trash_entries.get(document_id).cloned() {
Some(trash_entry) => trash_entry,
None => {
if let Some(idempotent) = idempotent_restored_local_markdown_page(root, document_id)? {
return Ok(idempotent);
}
return Err(WebError::bad_request_code(
"local_trash_entry_not_found",
"找不到要恢复的本地回收站记录",
)
})?;
));
}
};
let trash_path = resolve_metadata_relative_path(root, &trash_entry.trash_relative_path)?;
if trash_entry.resource_kind == "markdown_bundle" {
return restore_local_markdown_bundle(
@@ -5868,6 +6045,33 @@ fn restore_local_markdown_page(root: &Path, document_id: &str) -> Result<Value,
}))
}
fn idempotent_restored_local_markdown_page(
root: &Path,
document_id: &str,
) -> Result<Option<Value>, WebError> {
let Some(relative_path) = local_markdown_relative_path_from_document_id(document_id) else {
return Ok(None);
};
let markdown_path = resolve_metadata_relative_path(root, &relative_path)?;
if !markdown_path.is_file() {
return Ok(None);
}
let restored_document_id = local_markdown_path_page_id(&relative_path);
if restored_document_id != document_id {
return Ok(None);
}
Ok(Some(json!({
"ok": true,
"id": restored_document_id,
"documentId": restored_document_id,
"relativePath": relative_path,
"previousDocumentId": document_id,
"action": "restore",
"sourceKind": "local_folder",
"idempotent": true,
})))
}
fn restore_local_markdown_bundle(
root: &Path,
document_id: &str,
@@ -6004,6 +6208,34 @@ fn restore_local_raw_file(root: &Path, entry_id: &str) -> Result<Value, WebError
}))
}
fn idempotent_restored_local_raw_file(
root: &Path,
entry_id: &str,
) -> Result<Option<Value>, WebError> {
let Some(relative_path) = local_file_relative_path_from_entry_id(entry_id) else {
return Ok(None);
};
let original_path = resolve_metadata_relative_path(root, &relative_path)?;
if !original_path.is_file() {
return Ok(None);
}
let trash_entry_id = local_file_trash_entry_id(&relative_path);
Ok(Some(json!({
"ok": true,
"id": entry_id,
"documentId": entry_id,
"resourceKind": "local_file",
"resourceScope": "local_folder",
"originalFilePath": relative_path,
"relativePath": normalize_relative_path(root, &original_path)?,
"trashEntryId": trash_entry_id,
"action": "restore",
"canonicalCommand": "tree.resource.restore",
"sourceKind": "local_folder",
"idempotent": true,
})))
}
fn restore_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebError> {
let mut metadata = load_local_folder_metadata(root)?;
let trash_key = find_local_directory_trash_entry_key(&metadata, entry_id).ok_or_else(|| {
@@ -6073,6 +6305,116 @@ fn restore_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebErro
}))
}
fn idempotent_deleted_local_directory(
root: &Path,
entry_id: &str,
) -> Result<Option<Value>, WebError> {
let metadata = load_local_folder_metadata(root)?;
let Some(trash_key) = find_local_directory_trash_entry_key(&metadata, entry_id) else {
return Ok(None);
};
let Some(trash_entry) = metadata.trash_entries.get(&trash_key) else {
return Ok(None);
};
let trash_relative_path = local_trash_file_path(trash_entry);
let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?;
if !trash_path.is_dir() {
return Ok(None);
}
Ok(Some(json!({
"ok": true,
"id": entry_id,
"documentId": entry_id,
"resourceKind": "local_directory",
"resourceScope": "local_folder",
"originalRelativePath": trash_entry.original_relative_path,
"originalFilePath": local_trash_original_path(trash_entry),
"trashEntryId": trash_key,
"trashPath": trash_relative_path,
"action": "delete",
"canonicalCommand": "tree.resource.archive",
"sourceKind": "local_folder",
"idempotent": true,
})))
}
fn local_directory_relative_path_from_entry_id(entry_id: &str) -> Option<String> {
let trimmed = entry_id.trim();
if let Some(relative_path) = trimmed.strip_prefix("local-dir-trash:") {
return Some(relative_path.to_string());
}
if let Some(encoded) = trimmed.strip_prefix("local-dir:") {
return decode_local_id_segment(encoded).ok();
}
trimmed
.strip_prefix("local:node:")
.or_else(|| trimmed.strip_prefix("local:folder:"))
.map(ToOwned::to_owned)
}
fn local_file_relative_path_from_entry_id(entry_id: &str) -> Option<String> {
let trimmed = entry_id.trim();
trimmed
.strip_prefix("local-file:")
.or_else(|| trimmed.strip_prefix("local:asset:"))
.or_else(|| trimmed.strip_prefix("local:node:"))
.map(ToOwned::to_owned)
}
fn idempotent_restored_local_directory(
root: &Path,
entry_id: &str,
) -> Result<Option<Value>, WebError> {
let Some(relative_path) = local_directory_relative_path_from_entry_id(entry_id) else {
return Ok(None);
};
let original_path = resolve_metadata_relative_path(root, &relative_path)?;
if !original_path.is_dir() {
return Ok(None);
}
let trash_entry_id = local_directory_trash_entry_id(&relative_path);
Ok(Some(json!({
"ok": true,
"id": entry_id,
"documentId": entry_id,
"resourceKind": "local_directory",
"resourceScope": "local_folder",
"relativePath": relative_path,
"trashEntryId": trash_entry_id,
"action": "restore",
"canonicalCommand": "tree.resource.restore",
"sourceKind": "local_folder",
"idempotent": true,
})))
}
fn idempotent_purged_local_directory(
root: &Path,
entry_id: &str,
) -> Result<Option<Value>, WebError> {
let Some(relative_path) = local_directory_relative_path_from_entry_id(entry_id) else {
return Ok(None);
};
let original_path = resolve_metadata_relative_path(root, &relative_path)?;
if original_path.exists() {
return Ok(None);
}
let trash_entry_id = local_directory_trash_entry_id(&relative_path);
Ok(Some(json!({
"ok": true,
"id": entry_id,
"documentId": entry_id,
"resourceKind": "local_directory",
"resourceScope": "local_folder",
"originalRelativePath": relative_path,
"trashEntryId": trash_entry_id,
"action": "purge",
"canonicalCommand": "tree.resource.purge",
"sourceKind": "local_folder",
"idempotent": true,
})))
}
fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, WebError> {
let mut metadata = load_local_folder_metadata(root)?;
if let Some(markdown_file) = find_markdown_by_page_id(root, &metadata, document_id)? {
@@ -6108,12 +6450,18 @@ fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, We
"sourceKind": "local_folder",
}));
}
let trash_entry = metadata.trash_entries.remove(document_id).ok_or_else(|| {
WebError::bad_request_code(
"local_markdown_not_found",
"找不到要永久删除的本地 Markdown 页面",
)
})?;
let trash_entry = match metadata.trash_entries.remove(document_id) {
Some(trash_entry) => trash_entry,
None => {
if let Some(idempotent) = idempotent_purged_local_markdown_page(root, document_id)? {
return Ok(idempotent);
}
return Err(WebError::bad_request_code(
"local_markdown_not_found",
"找不到要永久删除的本地 Markdown 页面",
));
}
};
remove_file_order_path(
&mut metadata.file_order,
&trash_entry.original_relative_path,
@@ -6194,6 +6542,33 @@ fn purge_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebError>
}))
}
fn idempotent_purged_local_raw_file(
root: &Path,
entry_id: &str,
) -> Result<Option<Value>, WebError> {
let Some(relative_path) = local_file_relative_path_from_entry_id(entry_id) else {
return Ok(None);
};
let original_path = resolve_metadata_relative_path(root, &relative_path)?;
if original_path.exists() {
return Ok(None);
}
let trash_entry_id = local_file_trash_entry_id(&relative_path);
Ok(Some(json!({
"ok": true,
"id": entry_id,
"documentId": entry_id,
"resourceKind": "local_file",
"resourceScope": "local_folder",
"originalRelativePath": relative_path,
"trashEntryId": trash_entry_id,
"action": "purge",
"canonicalCommand": "tree.resource.purge",
"sourceKind": "local_folder",
"idempotent": true,
})))
}
fn purge_local_raw_file(root: &Path, entry_id: &str) -> Result<Value, WebError> {
let mut metadata = load_local_folder_metadata(root)?;
let trash_key = find_local_file_trash_entry_key(&metadata, entry_id).ok_or_else(|| {
@@ -12223,6 +12598,50 @@ fn main() {}
Some("local-dir-trash:Folder")
);
let repeated_delete =
execute_local_tree_command(&root_uri, "delete", &folder_id, None, None)
.expect("repeated folder delete should be idempotent");
assert_eq!(repeated_delete["idempotent"].as_bool(), Some(true));
assert_eq!(
repeated_delete["trashEntryId"].as_str(),
Some("local-dir-trash:Folder")
);
let restored = execute_local_tree_command(&root_uri, "restore", &folder_id, None, None)
.expect("restore folder");
assert_eq!(restored["resourceKind"].as_str(), Some("local_directory"));
assert!(root
.join("Folder")
.join("Nested")
.join("note.txt")
.is_file());
let repeated_restore =
execute_local_tree_command(&root_uri, "restore", "local-dir-trash:Folder", None, None)
.expect("repeated folder restore should be idempotent");
assert_eq!(repeated_restore["idempotent"].as_bool(), Some(true));
assert_eq!(
repeated_restore["resourceKind"].as_str(),
Some("local_directory")
);
assert!(root
.join("Folder")
.join("Nested")
.join("note.txt")
.is_file());
execute_local_tree_command(&root_uri, "delete", &folder_id, None, None)
.expect("re-delete folder");
let purged =
execute_local_tree_command(&root_uri, "purge", "local-dir-trash:Folder", None, None)
.expect("purge folder");
assert_eq!(purged["resourceKind"].as_str(), Some("local_directory"));
assert!(!root.join(".mnote").join("trash").join("Folder").exists());
let repeated_purge =
execute_local_tree_command(&root_uri, "purge", "local-dir-trash:Folder", None, None)
.expect("repeated folder purge should be idempotent");
assert_eq!(repeated_purge["idempotent"].as_bool(), Some(true));
let _ = std::fs::remove_dir_all(&root);
}
@@ -12256,6 +12675,45 @@ fn main() {}
assert!(root.join(trash_path).join("Page.md").is_file());
assert!(root.join(trash_path).join("asset.txt").is_file());
let repeated_delete =
execute_local_tree_command(&root_uri, "delete", &folder_id, None, None)
.expect("repeated folder-row delete should be idempotent");
assert_eq!(repeated_delete["idempotent"].as_bool(), Some(true));
assert_eq!(
repeated_delete["resourceKind"].as_str(),
Some("markdown_bundle")
);
assert!(
root.join(trash_path).join("Page.md").is_file(),
"重复删除不应破坏回收站中的页面 bundle"
);
let restored = execute_local_tree_command(&root_uri, "restore", &page_id, None, None)
.expect("restore page bundle");
assert_eq!(restored["documentId"].as_str(), Some(page_id.as_str()));
assert!(root.join("Page").join("Page.md").is_file());
assert!(root.join("Page").join("asset.txt").is_file());
let repeated_restore =
execute_local_tree_command(&root_uri, "restore", &page_id, None, None)
.expect("repeated restore should be idempotent");
assert_eq!(repeated_restore["idempotent"].as_bool(), Some(true));
assert_eq!(
repeated_restore["documentId"].as_str(),
Some(page_id.as_str())
);
assert!(root.join("Page").join("Page.md").is_file());
execute_local_tree_command(&root_uri, "delete", &page_id, None, None)
.expect("re-delete page bundle");
let purged = execute_local_tree_command(&root_uri, "purge", &page_id, None, None)
.expect("purge page bundle");
assert_eq!(purged["documentId"].as_str(), Some(page_id.as_str()));
assert!(!root.join(".mnote").join("trash").join("Page").exists());
let repeated_purge = execute_local_tree_command(&root_uri, "purge", &page_id, None, None)
.expect("repeated page purge should be idempotent");
assert_eq!(repeated_purge["idempotent"].as_bool(), Some(true));
let _ = std::fs::remove_dir_all(&root);
}
@@ -12287,6 +12745,37 @@ fn main() {}
.expect("trash index");
assert!(trash_index.contains("local-file:docs/photo.png"));
let repeated_delete = execute_local_tree_command(
&root_uri,
"delete",
"local-file:docs/photo.png",
None,
None,
)
.expect("repeated local-file delete should be idempotent");
assert_eq!(repeated_delete["idempotent"].as_bool(), Some(true));
let restored = execute_local_tree_command(
&root_uri,
"restore",
"local-file:docs/photo.png",
None,
None,
)
.expect("restore local-file asset");
assert_eq!(restored["resourceKind"].as_str(), Some("local_file"));
assert!(root.join("docs").join("photo.png").is_file());
let repeated_restore = execute_local_tree_command(
&root_uri,
"restore",
"local-file:docs/photo.png",
None,
None,
)
.expect("repeated local-file restore should be idempotent");
assert_eq!(repeated_restore["idempotent"].as_bool(), Some(true));
let _ = std::fs::remove_dir_all(&root);
}
@@ -12325,6 +12814,9 @@ fn main() {}
.expect("purge chinese-named asset");
assert_eq!(purged["ok"].as_bool(), Some(true));
assert!(!root.join(".mnote").join("trash").join("资源.ext").exists());
let repeated_purge = execute_local_tree_command(&root_uri, "purge", asset_id, None, None)
.expect("repeated asset purge should be idempotent");
assert_eq!(repeated_purge["idempotent"].as_bool(), Some(true));
let trash_index_after_purge =
std::fs::read_to_string(root.join(".mnote").join("trash-index.json"))
.expect("trash index after purge");