feat(ingest): 機械式 wiki 卡片→KBDB ingest(Arcrun#8 Phase A,dry-run 驗證)

新 ingest 模型(無 LLM,取代舊 raw→Haiku 路):
- 來源 = system-dev/wiki/cards/**/*.md(精耕卡)+ ## 實體/## 關聯 typed-edge + [[wikilink]]
- 卡片→base entry(metadata.embed=true);typed-edge/wikilink→graph triplet
- 純機械決定性,零 token

形式 = Arcrun workflow(cron drain + Gitea webhook delta)+ 新自訂零件 km_wiki_card_parse

不撞頂設計:一卡一 tick + 超大卡以 source_uri anchor 自動分段;
graph fan-out 精確 = 7+4N+M+D,dry-run 對 notes 三卡上限 33 subrequest(<50)。

Phase A:不部署、不寫 live。含純核心 + dry-run 證據(3 entries/15 triplets/16 nodes)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJiLCRUU2o3aSpPEzVCt2o
This commit is contained in:
Arcrun CC
2026-07-06 03:52:50 +00:00
parent ed2e42e007
commit cc494a250e
7 changed files with 599 additions and 0 deletions
@@ -0,0 +1,74 @@
# km-wiki-ingest — 機械式 wiki 卡片 → KBDB ingestArcrun#8 / 頂層 SDD T2T4
> 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 workflowYAML 編排)+ 1 個新自訂零件 `km_wiki_card_parse`(機械解析)+ 現成零件(`cron` / `http_request` / `foreach_control` / `kbdb_upsert_block`)。**
理由:
1. **編排本來就是 arcrun 的主場**cron 限速 drain、Gitea webhook 只吃 delta、foreach 小批、冪等 upsert——這些跟現成零件 1:1 對得上,且 leo 要「Arcrun workflow 慢慢做」、arcrun 哲學禁一次性腳本。
2. **arcrun 唯一缺的是「卡片 → envelope」的解析**。那是一段**決定性純轉換**(無 LLM、無網路、無檔案)——剛好是自訂零件的理想形狀(`stdin_stdout_json` + `no_network_syscall` + `no_filesystem_syscall`)。把它做成零件(而非在 YAML 裡塞 `string_ops` 正則)讓 workflow 可讀、解析可單元測試,貼合 arcrun「零件」複用哲學。
3. **小批是結構性的,不是靠祈禱**:一卡一 tick,每卡在 graph worker 的 fan-out ≈ `7+4N+M` subrequestnotes 卡 N≈4/M≈5 → est 28~33,穩壓 CF 50 頂下);解析零件會**預先把超大卡以 `source_uri` anchor 分段**,任何單一 graph 呼叫都不破頂。
**Phase A 交付**:純解析+打包核心(`lib/card-to-envelope.mjs`,現在就能跑)+乾跑驗證器(`lib/dry-run.mjs`,印出「將寫入什麼」)+本 workflow.yaml +零件契約(部署被閘控,故 live 接線是「設計而非執行」)。
## 診斷小結:fan-out 精確來源 + 小批為何解得掉
`kbdb-graph-plugin` 現役寫入路徑(`triplet-ingest.ts` / `triplet-crud.ts` / `templates.ts` / `kbdb-client.ts`)逐行拆帳:
```
POST /triplets/ingestgraph 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_namehash 相同 → 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 entriesembed=true+ 3 envelopes、15 triplets、16 nodes
**單次 graph 呼叫 subrequest 上限 = 33< 50),無任一 envelope 破頂**
self-test 合成超大卡(不分段 est=114 會炸)→ 自動分 4 段、每段 ≤ 38,全綠。
## 待 live 部署 + 寫入(總管過 leo 閘用)
1. **部署新零件 `km_wiki_card_parse`**:移植 `lib/card-to-envelope.mjs` → arcrun 元件(Go/WASM 或 workerd JS 承載)→ `acr` 註冊。
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 驗。