---
title: Heptabase → Obsidian Migration Guide
summary: A portable migration specification and guide designed for inspection, verification, and reuse.
updatedAt: 2026-09-25
status: public-beta
draft: false
version: 1.0.1-beta
license: CC BY 4.0
downloadUrl: https://ideameka.com/downloads/heptabase-to-obsidian.md
repositoryUrl: https://github.com/cindyinee/heptabase-obsidian-migration-guide
rawUrl: https://raw.githubusercontent.com/cindyinee/heptabase-obsidian-migration-guide/main/guide.md
relatedWriting:
  - heptabase-obsidian
  - heptabase-obsidian-migration-guide
---

> **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 與報告：

```text
我的搬家資料夾/
  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 自動推測內容分類：

```text
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 的最低資料

```json
{
  "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。驗證真實路徑時使用：

```powershell
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、第三方圖片與軟體本身不因本文件授權而自動改變其權利狀態。
