Resource · Public beta

Heptabase → Obsidian Migration Guide

A portable migration specification and guide designed for inspection, verification, and reuse.

Updated
Sep 25, 2026
Version
1.0.1-beta
License
CC BY 4.0

Public Edition beta。 本指南整理自實際遷移與修復經驗,目前是 beta 版。它不是一鍵搬家工具,也不應直接套用到未盤點的資料。遇到規格沒涵蓋的情況,歡迎回報問題。

這份指南要解決什麼

把 Heptabase 搬到 Obsidian,不只是把 Markdown 複製到另一個資料夾。真正困難的是同時保留:

  • 卡片正文與 metadata;
  • Whiteboard 的空間配置與巢狀關係;
  • MindMap 的文字、階層與 edges;
  • Journal 的唯一正文與多個白板 instances;
  • 圖片、附件、highlight、transcript 與 references;
  • 已經在 Obsidian 裡新增或修改的使用者內容。

這份 Public Edition 提供一套可檢查、可回復、可以分階段驗證的遷移方法。它是寫給 AI Agent 執行、也讓人可以檢查的規格,不是一鍵 converter。案例數字只代表特定來源快照,不是其他 Vault 的固定目標。

先判斷你是哪一種遷移

情境 正確做法
Fresh Vault 在隔離的 test vault 重建並驗證,凍結成唯讀 canonical migration source,再從副本建立日常使用的 Active Vault。
Existing Active Vault 先辨識舊 migration layer、使用者新增內容、使用者修改內容與 Active-preferred files,再選擇 replace、import 或 semantic merge。不可整包覆蓋。

Canonical Migration Source 是已完成結構驗證、保持唯讀的重建來源;Active Vault 才是持續使用的 production vault。原始 Heptabase export、canonical source 與 Active Vault 是三個不同角色,都需要各自保存。

四個不可妥協的原則

  1. 先盤點,再轉換。 不先假設 export schema,也不以舊版本欄位名稱取代實際檢查。
  2. Low value 不等於刪除。 不轉成正式筆記的資料仍需保留原始來源與 audit evidence。
  3. 路徑存在不等於遷移成功。 Reference resolved、embed rendered、結構關係與實際可用性必須分開驗證。
  4. 先忠實重建,再整理可用性。 不要在還沒建立可回復基準前,就清理 ID、改 taxonomy 或覆蓋舊內容。

交給 AI Agent:第一段指令

使用者通常會貼上 README 的第一段指令(中文)。它要求的五項輸出,對應到本指南是:

  1. Source inventory(有哪些資料、各有多少)
  2. Migration Decision Table(建議轉換、封存或跳過,以及原因)
  3. 需要使用者決定的項目
  4. 主要風險
  5. POC 建議(先試哪一張白板,以及為什麼)

第一輪只分析。「不要修改任何檔案」指的是不修改 Heptabase export 與 test vault;Agent 可以在工作資料夾裡另建 _migration_work/,放 scripts、hash manifest 與報告:

我的搬家資料夾/
  Heptabase-Data-Backup-…/   ← 原始匯出,唯讀
  Obsidian_Test_Vault/        ← 測試 Vault,POC 通過前不寫入
  _migration_work/            ← Agent 的 scripts、manifests、報告

使用者多半不寫程式。回報時用使用者的語言,先講結論和需要他決定的事;技術細節放在 _migration_work/ 的報告裡,並告訴使用者報告在哪裡。

選 POC 白板

POC 應挑一張中等大小(大約 30–80 個物件)、元素齊全的白板:有 Card、圖片、文字、Connection、MindMap,最好還有 nested Whiteboard。不要挑最大的白板,太大會讓 POC 難以逐項檢查。如果 Journal 或 PDF 不在這張白板上,另外各做一個小 POC。

確認 Inventory 與 POC 後才進入批次轉換。

POC 的最低驗證

  • Canvas 是合法 JSON,node IDs 唯一,edge endpoints 都存在。
  • 每個 source instance 都對應到一個 Canvas node,數量對帳;例外逐一列出。
  • 卡片正文與來源 ProseMirror 文字比對,沒有遺失。
  • 圖片檔與來源檔內容相同(hash),不是只有路徑存在。
  • Nested whiteboard 在 parent Canvas 裡有對應的 file node。
  • MindMap 的節點數、階層與來源一致。
  • 請使用者在 Obsidian 實際打開:左下角「開啟其他 Vault」→「開啟資料夾作為 Vault」→ 選 test vault,再打開 POC 的 .canvas。

第一步:凍結來源並建立 Inventory

至少保存以下資料:

  • 完整 Heptabase export 與 All-Data.json;
  • Markdown、assets 及其他附件;
  • 來源 snapshot ID、匯出時間與工具版本;
  • 每個檔案的相對路徑、大小與 SHA-256;
  • 各 data type 的數量、active/deleted 狀態與抽樣結果;
  • All-Data.json 裡的 VERSION 與 DB_SCHEMA_VERSION(本指南的實測欄位來自 Heptabase 1.103.0、DB schema 133)。

來源應設為唯讀或使用不可變快照。Hash manifest 放在來源目錄之外(例如 _migration_work/),避免 manifest 自己改變計算範圍。

Windows 先檢查長路徑。 Heptabase export 可能有超過 260 字元的完整路徑(實測 6 個)。第一次掃描前就要處理,不要等到程式當掉:Python 讀取時使用 \\?\ 前綴,或把工作資料夾放在較短的路徑。不要替使用者修改系統設定(例如 LongPathsEnabled);需要時說明原因,請使用者自己決定。

Markdown export 不是完整的語意來源

All-Data.json 是必要來源,不是選配。正文以 All-Data.json 的 ProseMirror content 為準;Markdown export 主要用來取得 binary(圖片、PDF、影音)與交叉比對。

資料來源 主要用途
All-Data.json 正文(ProseMirror)、object type、ID、relation、placement、fileId、semantic structure
Markdown/native export 圖片與附件的 binary、交叉比對、使用者可直接閱讀的檔案

不要把 Markdown export 當成正文來源,原因是它很難可靠地對回 source ID:

  • .md 檔沒有 ID,檔名由標題轉換而來,部分特殊字元會被替換。
  • 沒有標題的卡片會被命名為 A wonderful new card N(實測 569 個)。
  • 同名卡片會加數字後綴;部分 .md 的內容和 All-Data.json 不一致。
  • 垃圾桶裡的卡片(isTrashed: true)也會被匯出(實測 3,312 個 .md,其中約 830 張在垃圾桶)。
  • Card Library/ 的 .md 放在同一層,但圖片放在各自的 -assets 子資料夾(實測 539 個)。

Native export 也可能有 Mindmap/(只有階層大綱,沒有座標與節點 ID)和 Text Element/。它們可以拿來核對,但 MindMap 結構、nested whiteboard 的 parent → child 關係、textElements 與 mediaElements 的座標,只有 All-Data.json 有。只靠 Markdown export,很容易得到一個「看起來差不多」但少了結構的 Vault。

不要假設 Whiteboard elements 一定巢狀在 whiteBoardList。實測 export 中,cardInstances、textElements、mediaElements、highlightElements、journalInstances、sections、connections 與 mindMapInstances 可能是頂層陣列,再用 whiteboardId 關聯 Whiteboard。缺 key、型別不符、孤立引用與重複 ID 都應明確報告,不能默默變成空陣列。

實測的 schema(Heptabase 1.103.0/DB schema 133)

以下是一份實際匯出的欄位,作為起點,不是保證。欄位會隨版本改變,寫 join 邏輯前一定要先實際檢查。

陣列 重點欄位 關聯
cardList title、content(ProseMirror)、isTrashed Card 本體
whiteBoardList name、isTrashed Whiteboard 本體
cardInstances cardId、whiteboardId、x、y、width、height、isFolded、foldedHeight Card 放在白板上的位置
mediaCards/mediaCardInstances fileId、type、transcript/cardId、whiteboardId Media card 與它的位置
pdfCards/pdfCardInstances fileId、title/pdfCardId、whiteboardId PDF card 與它的位置
textElements content、whiteboardId、座標 白板上的文字,直接帶位置
mediaElements fileId、whiteboardId、座標 白板上的圖片,直接帶位置
sections/sectionObjectRelations title、座標/sectionId、objectId 分區與分區裡的物件
connections beginId、beginObjectType、endId、endObjectType、beginStyle、endStyle、description 連線
whiteboardInstances whiteboardId(child)、containerId(parent)、containerType、isChild、座標 Nested whiteboard
mindMaps/mindMapInstances layout/mindMapId、whiteboardId、座標、boundingBoxRelativeX/Y MindMap 與它的位置
mindMapNodes mindMapId、parentId、childNodeIds、side、isCollapsed MindMap 階層
mindMapTextNodes/mindMapCardNodes content/cardId 節點內容;ID 與 mindMapNodes 共用
journalList/journalInstances date、content/journalDate、whiteboardId Journal 與它的位置
files id(即 fileId)、name、size、type 附件的 metadata

幾個容易出錯的地方:

  • 連線端點的型別名稱和陣列名稱不一樣。 實測 beginObjectType/endObjectType 的值有 cardInstance、textElement、imageElement(對應 mediaElements)、mindMapTextNode、mindMapCardNode、highlightElementInstance、pdfCardInstance、section。先列出所有出現過的值,再逐一對應。
  • 有些 ID 是刻意共用的。 mindMapNodes 和 mindMapTextNodes/mindMapCardNodes 用同一個 node ID,actionItems 和 actionItemAdds 也是。檢查重複 ID 要在同一張表內做,不要跨表報告。
  • Card 被丟進垃圾桶,instance 不一定跟著消失。 使用中的白板可能還有指向已刪除或不存在卡片的 instance,要列出來問使用者。

第二步:建立 Migration Decision Table

每一種 data type 都要回答:誰建立、是否刻意維護、是否有使用證據、關係價值、內容是否唯一、轉換品質,以及資料量與維護成本。

Value/來源狀態 預設行為
High value Convert:轉換並保留內容、關係與 metadata。
Medium/Ambiguous Ask:先提出映射方式、風險與建議,只詢問真正需要決定的項目。
Low value Archive only:保留來源,不主動變成正式 Obsidian 內容。
Source deleted/system artifact Skip active migration:不恢復成正式內容,但保留排除原因與 audit evidence。

決策表至少包含 Data type / Count / Value / Recommended action / Reason。Count 必須標明範圍與單位,區分 unique content、instances、relationships 和 active/deleted;未知值寫「待盤點」,不要填成 0。

決策表要涵蓋 All-Data.json 裡每一個非空的陣列,不只 Card 和 Whiteboard。容易被漏掉的有:sections、connections、PDF cards、media cards、sources(例如 Readwise 匯入)、insights(Heptabase AI 自動產生的洞察,不是使用者寫的)、chats、templates、collections、tabs、actionItems。使用者價值不明的系統資料,預設 Archive only。

Journal 也要分開盤點:instance 可能指向沒有正文的日期,也可能有空白日記或匯出日期之後的日期。這些都列成例外,不要默默略過。

第三步:定義目標 Vault 邊界

建議讓資料夾主要表達來源、系統或 workflow,而不是讓 AI 自動推測內容分類:

Active Vault/
  heptabase/       # 已核准的 Heptabase migration layer
  daily/           # Journal / Daily Notes
  _local/          # Obsidian-native 使用者內容
  _meta/           # 必須留在 production 的 metadata preservation artifacts
  Readwise/
  ai-workspace/
  .obsidian/

heptabase/ 內的子資料夾(例如 cards/、sources/、media/)是遷移時的設計選擇,不是 Heptabase 的原始結構;Heptabase export 的 Card Library 把 .md 放在同一層,附件放在各自的 -assets 子資料夾。分流規則要寫下來,才能重現。

重建用的 manifests、來源快照與 audit artifacts 應留在 migration workspace,不要因歷史範例而自動塞進 production Vault。每個 Whiteboard 可以保留自己的 Markdown、Canvas 與 assets 子資料夾,但移動任何檔案後,必須更新整個 Vault 的 references,而不只更新同一個白板或只更新 Canvas。

第四步:決定每種物件的 Obsidian 表示方式

先用 All-Data.json 確認 source object type、source ID 與 instance relationship,再決定 representation。不要只根據 Canvas 上看起來像文字或圖片來判斷。

Heptabase object 建議的 Obsidian representation
Card Markdown note;Canvas 使用 file node 引用。
沒有標題的 Card 讀取 content JSON 的 type 判斷;image Card 轉成含 embed 的 Markdown note 與 Canvas file node。不可因為 title 空白就略過或變成空白。
Whiteboard .canvas。
Nested whiteboard Parent Canvas 中指向 child Canvas 的 file node,保留 placement 與 geometry。
MindMap Canvas text/file nodes 加 edges,保留文字、階層與關係。
URL-only 或一般 textElement Canvas text node,保留可讀、可點擊的 URL。
含圖片或媒體的 rich textElement Markdown file node,保留內容與 embed order。
Journal 正文只存一份 daily/YYYY-MM-DD.md;各白板 instance 用 Canvas file node 引用。
Highlight 將 ProseMirror 文字與註解轉成可讀 Markdown;真正空白的才移除。
Media card Binary 可取得時保留媒體與 metadata;無法取得時留下可讀的缺失說明與 audit evidence。
Tag/Property Frontmatter;不要批次覆寫使用者已有的 metadata。

沒有標題,不等於沒有內容

Card 的 title 可以是空的,但 content 仍可能是一張完整的圖片:{"type":"image","attrs":{"fileId":"..."}}。以 title 是否存在來決定要不要轉換,會靜默遺失內容。實測中,17 個原本被轉成 *(empty card)* 的 Canvas nodes,回查後全部是可以復原的 image Card;另有 3 張來源真的沒有內容,才保留 empty representation。

注意「沒有標題的 image Card」和 mediaCards 是兩種不同的東西:前者是 cardList 裡 title 空白、content 是圖片的卡片,圖片通常在 export 裡找得到;後者是獨立的 media card,binary 常常不在 export 裡(見「Placeholder 是警訊」)。

檔名的預設規則

第一次建立 Obsidian 檔名,和事後 rename 是兩件事。下面是建議預設,POC 時先給使用者看,同意後再批次套用:

  • 有標題:用標題當檔名。
  • 沒有標題:依內容命名,例如取正文第一行的前 30 個字;圖片卡用「未命名圖片卡」加 6 碼 source ID。不要用 UUID 當主要檔名。
  • Windows 與 Obsidian 不允許的字元(\ / : * ? " < > |),以及會干擾連結的 # ^ [ ]:換成全形字元或移除。
  • 同名碰撞:加 6 碼 source ID 後綴,不要覆寫。
  • 全部寫進 mapping log:source ID、原標題、最後的檔名、套用的規則。

Canvas 的最低資料

{
  "id": "stable-node-id",
  "type": "file|text|group",
  "x": 0,
  "y": 0,
  "width": 420,
  "height": 240,
  "file": "vault/relative/path.md",
  "text": "Markdown text"
}

JSON Canvas 並不要求 Tab 縮排。舊案例曾把 Canvas 開啟失敗歸因於 space indentation,但這不是通用規格。真正應驗證的是合法 JSON、欄位型別、唯一 node IDs、有效 edge endpoints、有限座標與正確的 Vault-relative paths。

幾個要事先決定、並寫進報告的對應:

  • 摺疊的卡片(isFolded: true):Canvas 沒有摺疊狀態。選擇用完整 height(內容完整可見)或 foldedHeight(外觀接近原本),全部一致。
  • 箭頭樣式:connections 的 beginStyle/endStyle 對應 Canvas edge 的 fromEnd/toEnd(arrow 或 none)。
  • 連線文字:description 轉成 edge 的 label。

MindMap 與 nested whiteboards

MindMap 不能因為目標格式不同就直接 skip。實測的來源結構(欄位名稱仍要先實際檢查):

  • mindMaps:MindMap 本體;mindMapInstances:它放在哪張白板、外框座標。
  • mindMapNodes:階層,用 parentId 與 childNodeIds 表示;沒有獨立的 edges 陣列,edges 要從 parent → child 推出來。
  • mindMapTextNodes/mindMapCardNodes:節點的文字(ProseMirror)或指向的 Card,ID 與 mindMapNodes 相同。

每個 instance 使用穩定 ID(instance ID + node ID)轉成 Canvas nodes 與 edges,edges 保留 fromNode/toNode/fromSide/toSide。節點本身沒有座標,用 instance 的 x/y/width/height 當外框,內部採可重現的樹狀 layout。boundingBoxRelativeX/Y 的確切意義尚未確認,不要單憑推測換算。

事先告訴使用者:MindMap 搬過去後會重新排版,外觀和 Heptabase 不同,但節點、文字與階層應該一致。 否則使用者很可能以為搬壞了。

Nested whiteboard 的關係記在 whiteboardInstances:whiteboardId 是 child,containerId 是 parent,containerType 為 whiteboard 時才是白板裡的白板(實測另有 map,是放在總覽地圖上的位置)。實測中同一張 child 可能同時放在兩張以上的 parent 裡,也可能有 containerId 指向不存在的白板,兩者都要列出來。

Nested whiteboard 的 fidelity 不只包括 child Canvas 檔案存在,還包括 parent → child relationship、child 在 parent 中的位置、child 自身內容與最終 reference。把所有 Canvas 攤平成互不相干的檔案,仍然是結構遺失;而且這種遺失不會產生任何 broken reference。

Journal

同一天的 Journal 正文只保存一份,多個白板 instances 都指向同一份 daily note。Manifest 應記錄 instance ID、日期、daily path、whiteboard ID、geometry 與 folded state。Unique dates、正文檔案數和 instances 數量要分開對帳。

第五步:用可回復的 Pipeline 執行

  1. Inspect export schema、來源狀態與 data types。
  2. 建立 Inventory 與 Migration Decision Table。
  3. 在隔離 test vault 重建內容、關係與空間結構。
  4. 驗證結構、references、metadata、assets 與代表性案例。
  5. 凍結通過結構驗證的 canonical source。
  6. 從副本建立 production candidate,才開始 usability cleanup。
  7. 若已有 Active Vault,先 audit user-created、user-modified 與 Active-preferred files。
  8. 建立完整可還原 backup 與 replace/import/merge mapping。
  9. 只取代已確認未修改的 migration layer;先保護使用者內容。
  10. 對 user-modified notes 做 semantic merge,不用新版直接覆蓋。
  11. 更新 Canvas、Markdown links、wikilinks、embeds 與 attachment references。任何 move/rename/資料夾重組之後都要重做這一步,範圍是整個 Vault 的筆記,不只 Canvas。
  12. 執行分層程式驗證與 Obsidian UI 抽查。
  13. 比較 canonical source 與 Active Vault 的相對路徑差集。
  14. 只有證據充分的 residuals 才移到 Vault 外 quarantine。
  15. 再跑一次完整驗證,保存 counts、hashes、例外與 quarantine report。

常見技術陷阱

ProseMirror 不是純文字

Heptabase 富文本使用 ProseMirror JSON。必須遞迴處理;空 paragraph 不一定有 content,不要用固定深度索引。

不只要取文字,還要把每一種 node 轉成對應的 Markdown。先列出資料裡實際出現過的所有 node 與 mark 類型,再逐一決定轉法,未處理的類型要報告,不要默默丟掉。實測出現過:

  • Nodes:paragraph、heading、bullet_list_item、numbered_list_item、todo_list_item、toggle_list_item、blockquote、code_block、horizontal_rule、table/table_row/table_cell/table_header、image、video、audio、file、math_inline/math_display、date、mention、card、whiteboard、pdf_card、image_card、video_card、highlight_element、section、chat、embed、hard_break。
  • Marks:strong、em、underline、strike、code、link、highlight、color、anchor。

兩個特別要處理的情況:

  • 內部連結寫成網址。 指向其他卡片的連結可能是 https://app.heptabase.com/<space>/card/<id>(實測 553 個),要依 source ID 轉成 Obsidian 的 wikilink。
  • 圖片直接內嵌在正文裡。 部分圖片以 base64 data:image/... 存在 content 中(實測 16 個),要取出成獨立檔案再 embed。

Connection 的 description 也是 ProseMirror,轉成 Canvas edge 的 label 文字。

欄位名稱不能用猜的

各種 instance 指向來源物件的欄位名稱並不一致。實測中,mediaCardInstances 指向 media card 的欄位是 cardId,不是 mediaCardId;textElements 直接帶 whiteboardId,不經過 instance table。每種 instance 都要先實際檢查欄位,再寫 join 邏輯。

Placeholder 是警訊,不是終點

*(empty card)* 這類 placeholder 或 fallback,不代表轉換成功,而是在問:為什麼這個 source object 最後只能變成 placeholder?每個 placeholder 都要回查來源,分成「可以復原」與「來源真的沒有內容」。

同樣地,缺圖要分開兩種情況:Migration 漏掉的,與來源 export 本來就沒有 binary 的。若 source object、placement 與 fileId 都在,但 export 裡確實沒有檔案,標記為 source-unrecoverable 並保留 object 與位置,不要讓 Agent 反覆重試不存在的檔案。

獨立的 mediaCards 特別要先盤點比例。實測中,146 張圖片類 media card,以 files 表的大小比對,在 export 裡一張都找不到對應檔案,影片與音訊類也一樣;另有 78 張沒有 files 紀錄。遇到這種情況要在 Inventory 階段就告訴使用者,並建議他:重要的幾張可以回 Heptabase 手動另存。

媒體檔名可能碰撞

大量來源檔可能都叫 image.png,export 後才被加上數字後綴。export 的檔名裡也沒有 fileId。

建議的對應方式:

  1. 從 files 表用 fileId 取得 name 與 size。
  2. 在 export 裡找 size 相同的檔案當候選;再用檔名、所在的 -assets 資料夾與卡片標題縮小範圍。
  3. 只剩一個候選時才算對上。多個候選時,比較內容 hash:內容完全相同就可以共用;內容不同就列為 ambiguous,不能直接選第一個。
  4. 找不到候選時,標記 source-unrecoverable。

重複的 Canvas

多輪遷移容易留下重複或過期的 Canvas。判斷方法是比對 Canvas node IDs 與 All-Data.json 的 source instance IDs:完全對不上的,是其他輪次留下的產物,應在備份後移除。不要用「檔名帶數字後綴」當作刪除條件。

路徑正規化與重新命名要分開

Canvas 內使用 / 與 Vault-relative paths。先以 reference syntax 解決 encoding 問題,rename 是最後手段。所有 move/rename 都保存 mapping log,至少包含來源 ID、old/new path、原因、規則版本、碰撞處理、受影響 references 與驗證結果。

Windows 長路徑支援取決於系統、API 與應用程式。\\?\ 可以協助某些工具讀取來源,但不能寫進 Canvas 或 Markdown references。縮短路徑時保留可辨識前綴、副檔名與穩定 hash,並檢查碰撞。

Markdown 圖片路徑含 )

實測 image embed 路徑含 ) 時,angle-bracket destination 仍可能無法正常 render。可嘗試將 ) 編碼為 %29、空格編碼為 %20,但最後仍需在 Obsidian 實際確認圖片顯示。Reference resolved 不等於 embed rendered。

PowerShell 的方括號

PowerShell 會把 [] 當 wildcard。驗證真實路徑時使用:

Test-Path -LiteralPath $path

否則可能把存在的檔案誤報為 broken reference。

Block IDs 與 transcripts

不要把每個來源 UUID 都變成使用者可見的 ^UUID。先建立 definitions 與 references 的雙向索引,只保留確實被 block link 或結構依賴使用的 anchors。實測中,保留全部 anchors 會產生超過六萬個可見 ID,但真正被引用的只有幾百個。

Transcript 應依來源順序轉成可讀、含 timestamp 的 Markdown;failed、processing 或 unavailable 狀態顯示清楚的 placeholder。Raw JSON 與 technical IDs 留在 audit layer,不進 normal reading content。

Existing Active Vault:不能直接覆蓋

先將檔案分類:

分類 處理原則
old-migration-duplicate 確認未被使用者修改後才可由新版取代。
old-only-user-created 保留;必要時搬到 _local/ 並更新 references。
same-file-identical 保留,不需要重寫。
same-file-user-modified 使用 baseline、Active 與新 reconstruction 做三方 semantic merge。
attachments-old-only 保留並檢查 Active references。
.obsidian 與 templates 保留 Active 設定,不可整包覆蓋。
Active-preferred 明確記錄理由與 hash,保留 Active version。

Semantic merge 要保留使用者新增、修訂與刪除意圖,同時整合新版恢復的來源內容、frontmatter、附件和 links。缺 baseline 或無法可靠判斷時,保留雙方版本與差異供人工決定,不猜測使用者意圖。

較好的新版結構,應以 selective backport 回植已確認的改進(例如 nested whiteboards、MindMap 結構、漏掉的 image Cards),同時保留 Active Vault 既有的資料夾慣例、Journal 用法與使用者內容。遷移完成後,Active Vault 就是使用者的工作區;之後的每次修正都是 reconciliation,不是 overwrite。

Cleanup 優先移到 Vault 外 quarantine,不直接永久刪除。Quarantine report 應保存原路徑、新路徑、原因、來源判斷、hash、時間與 reference 影響。

四層驗證

層次 要回答的問題
內容完整 原始內容有沒有留下?
關係完整 Card、Whiteboard、MindMap 之間的關係有沒有留下?
空間與結構 位置、階層與 nesting 是否合理保留?
實際可用 在 Obsidian 裡是不是真的能讀、能開、能點?

程式 audit 與實際 UI 使用,是兩個不同的驗收層級,要分開記錄。

內容完整

  • Source snapshot、範圍、counts、paths 和 hashes 已保存。
  • 每個 data type 都有來源數量、輸出數量與例外分類。
  • Canonical source 在 import/merge 前後保持不變。
  • 每個 placeholder 都已回查來源,並分類為「已復原」或「來源確實沒有內容」。
  • Recoverable media 已恢復;unrecoverable 項目有來源證據且未被捏造或靜默刪除。
  • User-created、user-modified 與 Active-preferred files 都有明確處理紀錄。
  • Tags、Properties 與其他使用者 metadata 沒有被批次覆寫。

關係完整

  • 所有 Canvas file nodes 指向 Vault 內存在的 mapped target。
  • Markdown links、wikilinks、images、attachments 與 block links 分別驗證。
  • 任何 rename/move/資料夾重組之後,全部筆記內的 references 都已依 mapping 更新,不只 Canvas。
  • MindMap 的 node text、edges、hierarchy 與 relationships 已對帳。
  • Journal instances 指向正確且唯一的 daily notes。

空間與結構

  • 所有 Canvas 都是合法 JSON,node IDs 唯一,edges endpoints 有效。
  • Nested whiteboard containment、placement 與 child contents 已對帳。
  • Canvas node 的 type 與來源 object type 一致(圖片不是 text placeholder)。
  • 重複或過期的 Canvas 已用 source ID 比對並處理。

實際可用

  • 圖片與附件不只路徑存在,也能在 Obsidian 正常 render/開啟。
  • 筆記正文沒有大量不必要的 UUID anchors 或 raw JSON。
  • 抽查代表性白板:在 Obsidian 實際點擊卡片、連結與嵌入內容。

收尾

  • Invalid Canvas JSON = 0。
  • Canvas broken file refs = 0。
  • 筆記內 Markdown links/wikilinks 的 broken 數量 = 0,或每一個都已分類說明。
  • Unresolved recoverable items = 0。
  • 已知無法恢復的項目逐筆分類並保留 evidence。
  • Active-only residuals 全部分類;沒有未判定就刪除的檔案。

Zero broken references 並不能證明 structural fidelity。 如果一個 relation 根本沒被建立,就不會出現 broken reference:child Canvas、MindMap edges、geometry、內容順序或 media rendering 遺失時,即使每個 path 都存在,遷移仍未完成。

反過來也要注意,「Canvas broken refs = 0」只涵蓋 Canvas。實測中,最終驗收時 Canvas 全數正常,但資料夾重組只更新了 Canvas 內的路徑,筆記裡仍有約 1,700 個連結指向已不存在的舊資料夾;補做全 Vault 的筆記連結掃描後才發現並修復。

完成的定義

一次可信的遷移至少要能回答:

  • 哪個來源 snapshot 被處理?
  • 每一種資料被轉換、封存或排除的理由是什麼?
  • 哪些資訊被完整保留,哪些只能部分保留?
  • 哪些項目無法恢復,證據在哪裡?
  • 使用者在 Active Vault 的修改是否仍在?
  • 如果結果不理想,是否能回復到遷移前狀態?

Migration success 不是「所有檔案都存在」,而是內容、關係、空間結構與可用性都被保存,所有例外也誠實可追溯。這份 Guide 真正要回答的問題是:怎麼知道你真的搬到了你以為自己搬到的東西?

變更紀錄

  • 1.0.1-beta(2026-09-25):依一次盲測修訂(一個不知道先前過程的 AI Agent,只靠 README 與本指南完成 Inventory 與一張白板的 POC)。新增:工作資料夾與報告位置、POC 選法與最低驗證、實測 schema 表、正文以 All-Data.json 為準、檔名預設規則、fileId 對應方式、ProseMirror 類型清單、Windows 長路徑提前檢查。更正:MindMap 欄位名稱、native export 其實可能有 Mindmap/ 與 Text Element/、Card Library 不是完全扁平、media card 缺檔的規模。
  • 1.0.0-beta(2026-09-25):第一個公開版本。

版本、來源與授權

  • Public Edition:1.0.1-beta(依盲測結果修訂;變更見文末)
  • 技術來源:Heptabase → Obsidian Migration Guide v3.2 / field-tested specification(2026-09-21)
  • 技術來源 SHA-256:25F67AB318F56DD75F2FF24FB51AEFCF1E552CC6984CC54E7727ADD76E4B2DE8
  • Public Edition 更新日期:2026-09-25
  • 授權:Creative Commons Attribution 4.0 International(CC BY 4.0)

你可以依 CC BY 4.0 分享與改作這份 Public Edition,但必須標示作者 Cindy Young、作品名稱、授權方式,並說明是否做過修改。案例中的個人資料、原始 Vault、第三方圖片與軟體本身不因本文件授權而自動改變其權利狀態。