# km-wiki-ingest — 機械式 wiki 卡片 → KBDB ingest（Arcrun#8 / 頂層 SDD T2–T4）

> Phase A 產物：機械 ingest 邏輯 + 乾跑證據 + workflow 設計。**不部署、不寫 live KBDB。**

## 解決什麼問題

把各 repo 的 `system-dev/wiki/cards/**/*.md`（人工精耕卡）**機械地**（無 LLM）灌進 leo21c KBDB：

- **卡片 → base entry**（`metadata.embed=true`，供語意搜尋）。
- **`## 實體` → graph node**；**`## 關聯` 的 typed-edge（`A >> 關係 >> B`）與 `[[wikilink]]` → graph triplet**。

取代舊 `kbdb-ingest-plugin/scripts/ingest-cli.mjs` 的 `raw → Haiku → 三元組` 路：新路純解析卡片內既有結構，**決定性、零 token、零幻覺**。

## 形式選擇與理由（給總管）

**形式 = Arcrun workflow（YAML 編排）＋ 通用 `code` 零件（sandbox inline JS，Arcrun#10）承載卡片→envelope 解析 ＋ 現成零件（`cron` / `http_request` / `foreach_control` / `kbdb_upsert_block`）。** 不再鑄 domain 零件 `km_wiki_card_parse`（Arcrun#10 裁定：一次性解析走通用逃生口）。

理由：

1. **編排本來就是 arcrun 的主場**：cron 限速 drain、Gitea webhook 只吃 delta、foreach 小批、冪等 upsert——這些跟現成零件 1:1 對得上，且 leo 要「Arcrun workflow 慢慢做」、arcrun 哲學禁一次性腳本。
2. **arcrun 唯一缺的是「卡片 → envelope」的解析**。那是一段**決定性純轉換**（無 LLM、無網路、無檔案）——正好是 `code` 零件 sandbox 的理想形狀（`stdin_stdout_json` + `no_network_syscall` + `no_filesystem_syscall`）。用通用 `code` 節點內聯這段 JS（而非鑄 domain 零件、也非在 YAML 裡塞 `string_ops` 正則）：workflow 可讀、解析可單元測試、且 registry 不因一次性邏輯增生 domain 零件。
3. **小批是結構性的，不是靠祈禱**：一卡一 tick，每卡在 graph worker 的 fan-out ≈ `7+4N+M` subrequest（notes 卡 N≈4/M≈5 → est 28~33，穩壓 CF 50 頂下）；`code` 節點內聯解析會**預先把超大卡以 `source_uri` anchor 分段**，任何單一 graph 呼叫都不破頂。

**Phase A 交付**：純解析＋打包核心（`lib/card-to-envelope.mjs`，現在就能跑，＝ `code` 節點內聯 JS 的權威來源）＋乾跑驗證器（`lib/dry-run.mjs`，印出「將寫入什麼」）＋本 workflow.yaml（parse_card = `code` 節點）。解析零件＝通用 `code`（Arcrun#10 分支，已就緒待部署）；本 example 不再自帶 domain 零件契約。部署被閘控，故 live 接線是「設計而非執行」。

## 診斷小結：fan-out 精確來源 + 小批為何解得掉

讀 `kbdb-graph-plugin` 現役寫入路徑（`triplet-ingest.ts` / `triplet-crud.ts` / `templates.ts` / `kbdb-client.ts`）逐行拆帳：

```
POST /triplets/ingest（graph worker 單次 invocation）對 base 的 subrequest：
  ensurePluginTemplates(3)            # 頂層一次
  + listRecordsByTemplate(1)          # 抓同 source 現存 active（冪等分組）
  + Σ_triplet [ createTriplet → ensurePluginTemplates(3) + createRecord(1) ]   # ★ 每條邊重跑 ensure！
  + persistNodes [ ensurePluginTemplates(3) + Σ_node createRecord(1) ]
  + Σ_deprecated updateRecord(1)
  = 7 + 4*N_triplets + M_nodes + D_deprecated
```

- **精確炸點還原**：07_01 單一 envelope 吞 `N=11, M=10, D=0` → `7+44+10 = 61 > 50` → 破頂半殘。**放大器＝`createTriplet` 內每條邊都重呼 `ensurePluginTemplates`（3 個 GET）**，佔了 33/61。
- **小批為何解得掉**：把「整檔一 envelope」改成「一卡一 envelope、必要時再 anchor 分段」，把 `N` 壓到讓 `7+4N+M ≤ 40`。notes 三卡實測 est 上限 = 33，全綠。超大卡（自測 20 邊/22 節點＝114）→ 自動分 4 段，每段 ≤ 38。
- **附帶建議（非本 Phase 必改）**：graph 端把 `createTriplet`/`persistNodes` 內重複的 `ensurePluginTemplates` 提到 ingest 入口只跑一次，可把每 envelope 省下 `3*(N+1)` 個 subrequest（單卡 est 33→約 18），批量還能更大。此為 graph-plugin 的可選優化，記此存查。

## 冪等設計

| 對象 | 冪等鍵 | 行為 |
|---|---|---|
| **entry** | `page_name`（穩定：`wikicard:<repo>/<canonical>`）+ `metadata.content_hash` | 找到同 page_name：hash 相同 → skip；不同 → PATCH content（觸發重嵌）。沒有 → POST 新建。 |
| **triplet envelope** | `source.uri` + `source.content_hash` | graph 現役 per-source 冪等：同 hash 整包 no-op（`triplet-ingest.ts:65`）。 |
| **分段** | 各段 `source.uri = <基uri>#segNN` | 各段獨立 uri → 各自獨立冪等，**繞開 per-source content_hash 整包 skip**（否則同 uri 第 2 段起會被判定「已落地」而整包跳過）。節點只放進「首次引用它的段」，跨段不重送（避免 graph 重建 entity）。 |

## 觸發（兩階段，對齊 SDD R3）

- **Phase 0（一次性 backfill）**：`cron */2` 每 tick drain 一張卡（限速慢推），反覆跑到全庫清空。冪等 → 可續傳、重跑零寫入。
- **穩態（日常增量）**：**Gitea push webhook → arcrun workflow**，只吃 `commits[].{added,modified}` 中的 `system-dev/wiki/cards/**/*.md`。⚠️ Gitea → Cloudflare(arcrun)，**非 GitHub Actions**，不觸 GitHub flag 紅線（D4/D20）。量小、不撞頂、不限速。

## 乾跑證據（Phase A，不寫 live）

```
node lib/dry-run.mjs --repo-path <notes clone> --repo Leo/notes --self-test
```

對 `Leo/notes` 的 3 張卡實測：3 entries（embed=true）+ 3 envelopes、15 triplets、16 nodes；
**單次 graph 呼叫 subrequest 上限 = 33（< 50），無任一 envelope 破頂**。
self-test 合成超大卡（不分段 est=114 會炸）→ 自動分 4 段、每段 ≤ 38，全綠。

## 待 live 部署 + 寫入（總管過 leo 閘用）

1. **部署通用 `code` 零件**（Arcrun#10 分支 `feat/issue-10-code-component`，已就緒）：`cd registry/components/code && npm install && npx wrangler deploy`（→ `code.arcrun.dev`）＋ `register-component.sh code`。本 workflow 的 parse_card 以 `component: code` 引用它，解析 JS 已內聯在 workflow.yaml（＝ `lib/card-to-envelope.mjs` 邏輯）。不再部署 domain 零件 `km_wiki_card_parse`。
2. **部署 workflow**：`km_wiki_ingest_drain`（cron drain）；`wrangler` 直推 leo21c（**禁 `acr update`**——codeload 綁 GitHub 假綠，Arcrun#4）。
3. **注入環境變數**（不放 repo）：`repo=Leo/notes ref=main gitea_token kbdb_url=https://arcrun-kbdb.leo21c.workers.dev kbdb_api_key graph_url graph_api_key=leo`。`CLOUDFLARE_ACCOUNT_ID=leo21c`（別讓官方 58309b 污染）。
4. **entry 寫入路徑確認**：若 `kbdb_upsert_block` 尚不透傳 `metadata_json`（需 `embed:true`/`content_hash`），entry 改用 `http_request` 直打 base `POST/PATCH /entries` 帶 `body_json.metadata_json`。
5. **預期寫入量（Leo/notes 現況 3 卡）**：3 entries + 15 triplets + 16 node records（去重後更少）；分 3 次 graph 呼叫（每次 ≤ 33 subrequest）+ 3 次 entry upsert。全庫鋪開時照 cron 一卡一 tick 慢推。
6. **驗收**：ingest 後 `GET /embed/backfill/status` 應見 pending 上升→drain 後歸零、embedded 增加；三模式（關鍵字/語意/圖）curl 驗。
