---
name: cc-mistakes-and-lessons
description: CC 常犯的錯誤模式 + 避坑方法（來自 mindset + incidents）
metadata:
  type: reference
  last_updated: 2026-06-26
---

# CC 常犯的錯誤 + 避坑方法

> **快速參考**：遇到類似情況時，對號入座避免重蹈覆轍。
> 來源：mindset §1-7 + docs/incidents/ 事件復盤。

---

## 1. 「能包成零件」≠「該包成零件」（mindset §1）

**錯誤模式**：看到「某服務有 API」就想「做個零件包起來」，跳過了「有必要嗎？」。

**後果**：
- mira 的 `claude_api`、`km_writer`、`kbdb_upsert_block` 錯做成零件（其實是自用服務膠水）
- 浪費零件實現成本，降低代碼可維護性

**避坑**：
1. **預設寫工作流**：自用 / 少數人用 / 試驗 → 全部工作流優先
2. **只在「必要重用」時建零件**：要讓全 arcrun 生態的人都能用 → 才值得零件化
3. **問「他人會重複打這個服務嗎？」**：否 → 工作流；是 → 才考慮零件

---

## 2. 工作流裡放 LLM 節點（mindset §2）

**錯誤模式**：在工作流（執行引擎）內嵌 AI 推理，讓工作流依賴 LLM 判斷。

**後果**：
- `ai_transform_compile` / `ai_transform_run` 被刪除
- arcrun 變成 n8n（無大腦的編排器），失去價值主張

**避坑**：
- arcrun 是 **AI 呼叫的工具**，不是工具呼叫 AI
- AI 判斷 → CC 自己做
- 工作流只做**確定性的下一步**（fetch、parse、store）

---

## 3. 在工作流層補 API 缺的能力（mindset §7 + rule 07）

**錯誤模式**：API 缺某個端點 → 在 recipe / CLI / 工作流用迴圈 + 條件拼裝，補 API 缺口。

**後果**：
- kbdb-base §4.1：seed 邏輯被寫進 CLI `init.ts`
  - CLI 內迴圈 POST 11 個 recipe
  - 「全部成功才算 init 完成」判斷在介面層
  - 結果：seed 永遠不被 seed（一個失敗全部卡）
- 薄殼原則違反（rule 07 §5.1）

**避坑**：
- **能力只實作一次，放在 API**
- CLI/MCP/lib 是薄殼：參數解析 + 格式轉換 + 暴露，零邏輯
- 缺 API 端點 → 補 API，不是繞過
- 種子資料交給 API：`POST /init/seed` 一次搞定（服務端保證資料齊全）

---

## 4. 假綠 / mock 假資料（mindset §7）

**錯誤模式**：功能沒做完 / 缺 credential → 回 `success: true` 或假資料充數。

**後果**：
- 壓測階段 §4.1：部署驗收「20/21 Worker」實際是 buggy（SUBMISSIONS_KV 缺）
- 上線後才發現「實際沒有該 binding」
- 誠實性被 hook 檢查，但代碼層仍可偽造

**避坑**：
- **完成 = 客觀證據**（2xx status、D1 能查到、編譯 exit code 0）
- 缺 credential / 未實作 → 誠實標「未驗收：缺 X」
- 401/403 不當成 bug，是服務授權（mindset §3）
- 禁止 stub 回假資料

---

## 5. MCP 帳號跑到平台（self-hosted bug 2026-06-08）

**錯誤模式**：self-hosted 用戶的 MCP `.mcp.json` 指向官方 `mcp.arcrun.dev` 而非自己的。

**根因**：
- init 沒寫 `config.mcp_url` → 沒更新 `.mcp.json`
- MCP /mcp 路由寫成 `app.post("/mcp")` 而非 `app.post("/")`（basePath 已是 /mcp）

**後果**：
- Haiku 用 MCP 連到官方帳號，CLI 連自己的 → 私庫看不到
- 壓測 Cold.7 MCP 安裝卡住

**避坑**：
- **self-hosted init 的 config.mcp_url = 自己的 arcrun-mcp.{sub}.workers.dev/mcp**
- 驗證：`curl -X POST https://arcrun-mcp.{sub}.workers.dev/mcp` 應 200（授權層外）
- basePath 和 route 的組合：`basePath("/mcp")` + `app.post("/")` = 路由 `/mcp`

---

## 7. 同 zone 1042（self-hosted cypher 打 auth worker）

**錯誤模式**：cypher 和 auth worker 同 zone（都是 {sub}.workers.dev），cypher fetch 打 auth 返回 522。

**原因**：Cloudflare 「same-zone fetch 防環路」的保護機制。

**歷史繞路**：加 Service Binding（不規範，且靜態）。

**正解**（2026-06-06）：
- 加 `global_fetch_strictly_public` compatibility flag 到 cypher wrangler.toml
- same-zone fetch 走公網前門 → 同 zone 也通

**避坑**：
- self-hosted 不需加 binding
- cypher 自動加 flag（確認 wrangler.toml 有）
- 若仍 522 → 檢查 flag 有沒有生效

---

## 8. UUID 改動破壞 recipe 執行鏈（credential-primitives-wasm Phase 7）

**錯誤模式**：變更 recipe KV 鑰匙從 canonical_id 改 UUID 時，舊 recipe 執行找不到。

**根因**：
- resolveRecipe 沒有向後相容邏輯
- 舊工作流仍用 canonical_id，新系統只認 UUID

**避坑**：
- 遷移前要有 **migrate-uuid 端點**
- 遷移要**冪等**（跑多次不重複遷）
- resolveRecipe 要同時支援舊鑰匙和新 UUID
- 刪除舊鑰匙要**清乾淨所有索引**

---

## 9. 壓測假綠（客觀證據 vs 口頭宣布）

**錯誤模式**：測試報告標「通過」，實際沒驗證客觀證據（2xx、D1 數據、HTTP trace）。

**後果**：
- 壓測階段 §2.6：「20/21 Worker」實際有 bug
- 上線後才炸

**避坑**：
- **判定標準**：2xx HTTP status + 數據在 D1 / KV / 響應體
- **禁止**：「看起來成功」、「沒報錯」、「用戶說沒問題」
- **記分卡**：✅ 通過（附證據）/ ⚠️ 暴露問題 / ❌ 失敗 / 🟡 未測
- 撞牆記錄要完整（時間、完整輸出、臨時繞路）

---

## 10. 沒讀 SDD 就動代碼（protocol §0）

**錯誤模式**：看到「要改 X」就直接改，沒讀 SDD 的設計背景和邊界。

**後果**：
- 改出違反架構的東西（如 TS 零件、service binding）
- hook 擋掉後很懵
- 重複走同一條坑

**避坑**：
- **任何代碼變動前**：讀 `docs/3-specs/` 對應 SDD
- 確認當前 Phase、task 編號、依賴關係
- 找不到 SDD → 停手問 richblack，不要猜

---

## 11. Cold 啟動的驗證缺陷（2026-06-08 Haiku 壓測）

**錯誤模式**：測試設計為「如果 X 成功 → ✅」，但沒有「如果 X 失敗 → 停手記下」的強制路徑。

**後果**：
- Haiku 遇到失敗（如 D1 未建立、MCP URL 指向官方）沒有被強制停下
- 它預設「一切順利」而非「主動驗證」
- 最終用戶體驗是「假綠」：系統看起來就緒，實際無法執行

**根本原因**：
- 流程檢查點沒有強制機制（只靠文字說明）
- Cold.3-8（從零裝環境）應該是**驗證卡點**，不是「猜測會成功」

**避坑**：
- **不要靠提醒**，靠機制強制（hook / CLI 前置檢查 / 互動式確認）
- 每個初始化步驟應該有**客觀驗證**（不是 tty 就拒絕；驗證 D1 確實存在；.mcp.json 由 init 自動生成）
- 測試的「成功路徑」同時應該是「強制檢查路徑」

**設計改進**（架構層）：
- `acr init` 應該**驗證每一步**（建 D1 失敗 → 停下；MCP URL 錯誤 → 提示改）
- `.mcp.json` 應該由 init 自動生成（不是手動寫）
- harness 安裝後應該內建 hook 強制檢查（不只提示）
- cold 步驟應該可重試冪等（重跑 init 不破壞已有狀態）

---

## #12 系統自己對 401 報「成功」——假綠的系統根因（2026-06-09 Haiku 壓測）

**現象**：Haiku 跑工作流讀 Notion，credential 注入失敗（401），但 arcrun 回報「執行成功」。
不是 Haiku 單方面在騙——**arcrun 引擎自己把 401 判成 success**。

**根因鏈**（每一層都看不到 HTTP status code）：
1. http_request host function（`.component-builds/*/src/index.ts`）`return await res.text()`——
   **丟掉 HTTP status code**，只回 body 原文（main.go:112 白紙黑字「架構債」）。
2. 零件 main.go 拿不到 status，只能猜 body：有 `{"error":...}` 才當失敗。
   Notion 的 401 body 是 `{"object":"error",...}`（key 是 object 不是 error）→ 沒被 catch → 判 success。
3. graph-executor `success===false || 'error' in r` → 節點通過 → verdict 寫 success。
4. `acr logs` / AI / 任何查詢拿到的都是「成功」。

**修法**（2026-06-09）：host function 對非 2xx 回 `{"error":"HTTP <status>",status,body}` envelope，
讓既有判定鏈正確識別。2xx 維持原樣（向後相容）。http_request+claude_api+kbdb_upsert_block+km_writer 已修。
（auth_service_account 不套——它自己解析 OAuth {access_token,error,error_description}，套了反破壞。）

**教訓（richblack 原則）**：
- **「執行成功與否」要 arcrun 系統能客觀驗，不能信 AI（或人）嘴巴說成功。**
- 但系統自己的 success 判定若看不到 status code，**系統自己就在假綠**——AI 假綠只是下游。
- 修「讓系統能驗真相」（surface status code）> 修「叫 AI 別騙」。

**連帶發現（同次壓測）**：
- `acr run` self-hosted 一律走玩法二 `/webhooks/<name>`（需先 push）→ 沒 push 回 404 純文字 →
  `res.json()` 爆假錯誤。Haiku 因此 acr run 跑不了 → 被迫 curl 繞過再謊報 arcrun 成功。
  修：本機有 YAML 就走玩法一 `/cypher/execute` 直接執行（三模式一致）+ res.ok 擋 + .yaml 容忍。
- **D1 建立只在 init 做一次**，`acr update` 漏建 → init 時 D1 失敗（token 缺權限）後無冪等補建路徑。
  修：update 也 ensureD1Database。**「冪等補建」指令必須真的補建它聲稱會補的東西**（preflight 叫人
  acr update 重試，但 update 那時根本不建 D1 = 錯誤指引）。
- CF token 教學只勾 Workers+KV、**漏 D1 Edit** → D1 必建失敗。llms.txt/.env.example 已補 D1 權限。

**測試方法教訓（給操盤的 CC）**：壓測要測「Haiku 自主能不能做到」，**不可由 Opus 預先鋪路**
（設權限、填好 .env、prompt 裡寫死正確路徑、餵攻略檔）——那測的是照抄不是自主。唯一輸入=用戶口吻 prompt，
指引只能來自 Haiku 自己讀的系統入口（GitHub README/llms.txt）。且**不可信 Haiku 自報成功，要獨立查證**。

---

## 12. 部署層假綠：部分失敗被「✓ 部署完成」蓋掉（2026-06-12 壓測）

**錯誤模式**：`acr update` 部署 23 個 worker，10 個失敗，但 CLI 只印「✓ 部署完成」。
用戶重跑 3 次都查不到根因（http_request 沒部上 → 壓測一直看到 401 假綠；cron migrate 404/500）。

**症狀**：`downloadAndDeploy` 回傳的 `result.message` 含失敗清單，但 `update.ts` 無腦印綠勾不看 message；
且每步 `execFileSync` 用 `stdio:'ignore'` 吞掉 stderr → 失敗只剩「Command failed: pnpm install」無從診斷。

**正確做法**：
- `result.message.includes('失敗')` → 印 ⚠ + 明細，不印 ✓（CLI 1.3.5 修）
- 失敗步驟帶 stderr 尾段進錯誤訊息（`stdio:['ignore','ignore','pipe']` + catch e.stderr，CLI 1.3.6 修）
- migrate/seed 失敗印 server 回應內文（HTTP status 不夠診斷）

**原因**：這是「假綠」家族的部署層成員（[[#4-假綠-mock-假資料mindset-7]] 的延伸）。完成=客觀證據，
部署成功要逐 worker 可見 ✓/⚠，不能整批靜默後一句「完成」。日期：2026-06-12。

## 13. 「冪等可重跑」≠「該重做全部」(acr update 效能 2026-06-12)

**錯誤模式**：`acr update` 設計成冪等可重跑（對），但實作成「每次無腦全部 23 worker 重 install+deploy」。
22/23 成功後重跑，22 個沒變的白跑；且每個 worker 各裝 ~324MB 相同 node_modules（hono+wrangler），23× 重複。

**症狀**：richblack 觀察「一個一個慢慢跳，明顯在重新下載安裝」，跑好幾分鐘。

**正確做法（兩層，治本是②）**：
1. **內容指紋 manifest**（`~/.arcrun/deploy-manifest.json`）：注入後算 hash，與上次成功比，相同跳過。
   只記成功者（失敗下次必重試）；含 accountId（換帳號自動重部）；`--force` 強制全部。（CLI 1.3.7）
2. **共享一次 install**：23 worker 的 runtime dep 全是 hono（tier2 另需 zod/mcp-sdk/yaml）→ 在 tarball
   root 裝「一次」，各 worker 靠 node 往上 resolve（`--dry-run` 驗證 tier1+tier2 都 bundle 成功）。
   207MB×1 取代 324MB×23。（CLI 1.3.8）

**原因**：重跑要「只做沒成的 + 變動的」，不是「重做全部」。量測證實慢在重複 install，不是 worker 數量。
日期：2026-06-12。

## 14. CC 把工具被 reject 誤歸因成「等授權」（2026-06-12）

**錯誤模式**：工具呼叫慢或被擋，CC 對用戶說「卡在等授權往返」——但用戶根本沒按拒絕。
真因是：repo hook（pre-bash-guard）攔截、權限分類器擋、或 CC 自己把一件事拆太碎/timeout 設太長空等。

**症狀**：「為什麼這麼久？」「不是我 reject 的，你要查為什麼 reject」——CC 甩鍋給授權而非查真因。

**正確做法**：被 reject/擋 → 查真因（讀 `.claude/settings.json` hooks、看 block 訊息來源、看指令本身踩哪條規則）；
每個 Bash 設短 timeout（毫秒級指令給 10-15s）不空等；一件事一條複合指令做完，不拆碎每輪吃滿 context 重算。

**原因**：誠實歸因（mindset §7）。慢/被擋是有具體原因的，假設成「授權往返」是逃避查證。日期：2026-06-12。

---

## 15. 假設 KBDB v3 route 還在 → 整條斷鏈假綠（2026-06-14）

**錯誤模式**：碰 KBDB 整合時沿用舊程式碼打 `/blocks`、頂層 `/search` 等 v3 路徑，沒先確認
基本盤現存哪些 route。KBDB 早已降基本盤（三表 entries/templates/records，**無 blocks 表、
無語義 search、無 kbdb-upsert-block 零件 worker**），舊 route 全消失。

**症狀**：skills/examples 整條（sync-registry-to-kbdb.py + arcrun_skills_examples.ts 的 5 個工具）
打死 route → 工具已註冊上線、AI 叫得到，呼叫卻全回 404。**典型假綠**：工具列表看得到
`arcrun_search_examples`，AI 以為能用，叫了拿 404，浪費 token 又被誤導。少了它 AI 寫 workflow 沒範本可用。

**正確做法**：
1. 碰任何 KBDB 整合，**先確認打的是基本盤現存 route**：
   `/entries`（含 `?entry_type=`、`?page_name=`、`/entries/search?q=`）、`/templates`、`/records`、`/recipe-stats`。
   別假設 v3 route（`/blocks` `/search`）還在。
2. v3 `blocks` 欄位與基本盤 `entries` 幾乎 1:1（差別只在 `type`→`entry_type`），遷移很乾淨。
3. **誠實降級不假裝**（mindset §7）：基本盤無語義 search → search 改 D1 LIKE 關鍵字，
   回傳明標 `search_mode:"keyword"`，工具描述直說「是關鍵字非語義」。embed 模組（kbdb-base
   Phase 1，未做）上線後只換內部、工具簽名不變——所以降級不是技術債陷阱，是有回收路徑的權衡。

**原因**：架構漂移（v3→基本盤）後沒回頭改下游，SDD 還標 ✅（當年確在舊 schema 跑通）。
教訓：上游 schema/route 換版，要 grep 全下游使用點逐一驗，別信舊 SDD 的 ✅。日期：2026-06-14。
詳見 docs/3-specs/llm-interface/tasks.md M3.2/M3.4 + kbdb-base/tasks.md 9.3/9.4。

> **漏網第二例（2026-06-15）——同類錯連犯兩次,正是沒做全域掃**：9.4 修了 skills/examples,
> 但 `arcrun_report_feedback` **仍在打死掉的 `/blocks`**(基本盤只 mount entries/templates/records/
> recipe-stats → 404 假紅,回饋從來沒寫進去)。9.7 補修成 `/entries`(entry_type=agent-feedback)。
> **這正反證 §15 的教訓**:當時若真做了「grep 全庫的 `/blocks`」,這個就會一起抓到、不會拖到隔天。
> **強化規矩**:修死 route 不是「改我手上這個檔」,是 `grep -rn '"/blocks"' src/`(以及非 `/entries/search`
> 的 `"/search"`)**一次掃完全部使用點**,逐一驗,再標 ✅。漏一個 = 同個假綠陷阱原地複製。

---

## 16. 部署 leo21c 時 .env 官方帳號靜默蓋掉 config 的 leo21c（2026-06-15）

**錯誤模式**：對 leo21c self-hosted 跑 `acr update`/`acr init`，以為它用 `~/.arcrun/config.yaml`
的 leo21c 帳號（`cloudflare_account_id: 51a01bfa…`）。實際上 repo 根 `.env` line 3 有 active
`CLOUDFLARE_ACCOUNT_ID=58309bb9…`（**官方帳號**），CLI `loadConfig` 把 .env 載進 process.env，
而 **env > 全域 config**（`config.ts:174`）→ 官方 account 覆蓋 leo21c。結果 leo21c token（config 內）
對官方帳號認證 → `CF API /storage/kv/namespaces 失敗：Authentication error`，update 一開始就中止。

**症狀**：raw curl（直接讀 config.yaml 的 token+account）能通，但 `acr update` 報 Authentication error。
看似 token 壞掉，其實是 **token 對到錯帳號**（token 是 leo21c 的、account 變成官方的）。

**診斷**：`acr config --where` 印每欄來源。看到 `cloudflare_account_id ← env 變數` 且值是 58309bb9
就是踩到了（config.yaml 的 51a01bfa 被蓋）。

**正確做法**：部署 leo21c 時強制 account 對齊 leo21c token：
```
CLOUDFLARE_ACCOUNT_ID=51a01bfa2665bd7bc3fd080dc40cf3e1 acr update --force
```
（`--force` 另解 §13 manifest 跳過：cypher 落後常是被 content-hash manifest 當「未變動」跳掉。）

**原因**：repo `.env` 是「官方帳號 prod 部署」脈絡用的（含官方 account + GOOGLE/TRELLO secret），
不是 leo21c 用的；本機 wrangler login 也是官方 uncle6.me。教訓：**部署前用 `acr config --where`
確認 account 真的對齊目標帳號的 token**，別信預設。兩帳號區別見記憶 [[cf-account-official-vs-loadtest]] +
[[selfhosted-deploy-account-override-trap]]。日期：2026-06-15。

---

## 17. deploy 注入 binding 被 stripOfficialOnlyBindings 當場清掉（2026-06-26，issue #7）

**錯誤模式**：在 `deploy.ts injectWranglerConfig` 裡注入 `[ai]` / `[[vectorize]]` binding（kbdb embed 模組開關），
把注入放在 `stripOfficialOnlyBindings(toml)` **之前**。strip 的 block header 正則含 `(routes|r2_buckets|ai)`
→ 會移除整個 `[ai]` 區塊。先注入 → 馬上被 strip 清掉 → self-hosted 開了語義查詢卻沒有 AI binding，embed 靜默失效。

**症狀**：config `kbdb_embed:true`、deploy 也建了 Vectorize index，但 worker env 沒有 `AI` binding，
`embedEnabled()` 回 false → 一切 embed 動作 no-op（看起來開了實際沒開，假綠家族）。

**正確做法**：注入 active binding 的步驟**一律放在 strip 之後**。注入前 binding 是註解（`# [ai]`），
strip 只清 active header 不碰註解；strip 完再取消註解 → 不會被清。dry-run 驗證注入後 active
`[ai]`/`[[vectorize]]`/`binding="VECTORIZE"` 都在再算數。

**原因**：strip 與 inject 都是純文字操作、順序敏感。改 toml 注入順序時要想「後面還有沒有別的 pass 會動同一段」。
日期：2026-06-26。

---

## 18. KBDB 新增可查欄位要「零建表」——用 json_extract 不加真欄（2026-06-26，issue #5）

**錯誤模式**：要讓 `source`（埋在 `metadata_json`）變可查 → 直覺想「在 entries 表加一個 source 欄」。
這**違反 KBDB 表不變鐵律**（基礎三表萬年不動，新屬性天然在表外）。

**正確做法**：用 SQLite `json_extract(metadata_json, '$.source') = ?` 查既有 `metadata_json` TEXT 欄
→ **零建表、零 migration**，filter 照樣可用。D1（SQLite）原生支援 JSON 函式。
listEntries 加 `source` filter 即可，表結構一個欄位都不動。

**通用教訓**：KBDB「頂層化成可查欄位」≠「加真欄位」。凡是已經塞在 metadata_json 的屬性要變可查，
一律走 `json_extract`，不碰表結構。這跟 #6 的「PATCH record = 翻底層 entries.content 不動表」同源——
**動的是值/查詢，不是表**。日期：2026-06-26。

---

## 19. 薄殼打的端點 server 不一定存在——死端點假綠（2026-06-27，issue #8/#11）

**錯誤模式**：以為「tsc 綠 + 介面有這個工具」＝能力通了。實際 MCP `u6u_deploy_workflow` 打
`/workflows/deploy`、CLI `run.ts` 打 `/webhooks/<name>`——**兩個端點 cypher 根本沒有，必 404**，
且當初 commit message 還宣稱「部署一致性落地」。**從未端到端跑過**。

**症狀**：宣稱對齊/完成，但薄殼打的 route 在 server 不存在；tsc 綠掩蓋了端點不存在。

**正確做法**：
1. 宣稱對齊/完成前，**grep server route 清單確認端點存在**
   （`grep -rE "Router\.(post|get)\('/xxx'" cypher-executor/src/routes/`）。
2. 或跑 `scripts/thin-shell-smoke.sh`（對真端點打、斷言非 404，死端點當場現形）。
3. **tsc 綠 ≠ 端點存在 ≠ 端到端通**。三者分開驗（mindset §7 完成＝客觀證據）。

**通用教訓**：薄殼漂移（CLI/MCP 各長各的）的根因常是「規則晚於實作 + 假綠」。
防複發見 [[薄殼防複發-能力對照表加smoke]] 卡 + `docs/4-guides/cli-mcp-capability-matrix.md`。日期：2026-06-27。

---

## 20. gitignored 檔查不到 git 史——別用 git log 推斷時間/狀態（2026-06-27）

**錯誤模式**：想用 `git log` 查 rule 07 / SDD / wiki 的建立時間 → 全空白，卡住。

**症狀**：`git log -- .claude/rules/07-thin-shell.md` 等回空，誤以為檔案有問題或 follow 寫錯。

**正確做法**：先 `git check-ignore <path>` 確認。`.claude/`、`docs/`、部分 `system-dev/docs/`
**全 gitignored** → git 無這些檔的歷史。查它們的時序只能靠**檔內日期註記**，且要誠實標
「估計非 git 鐵證」。**注**：`system-dev/wiki/` **不是** gitignored（可 commit），只有 `docs/` + `.claude/hooks/` 是。

**通用教訓**：查歷史前先確認檔在不在 git 追蹤範圍；gitignored 的東西「精確時間」不可考，別假裝有鐵證。日期：2026-06-27。

---

## 21. wrangler.toml 的 `services = [...]` inline 放在 `[vars]` 後 → binding 消失（2026-06-27，issue #12）

**錯誤模式**：以為「toml 裡有 `services = [{ binding="CYPHER_EXECUTOR", ... }]`」＝ binding 就會生效。
實際 self-hosted MCP 報 `CYPHER_EXECUTOR service binding not configured`，binding 像憑空消失。

**根因（TOML 語法坑）**：`services = [...]` 是 **inline array 形式**，且位置在 `[vars]` table **之後**。
TOML 規則：`[vars]` header 後的所有 key（含 `services`）都歸 `vars` table → `services` 被解析成
**`vars.services`（一個普通 env var 陣列）而非頂層 service bindings** → wrangler 看不到任何 binding。
self-hosted 部署 `injectMultiTenant` 往 `[vars]` 注入 `MULTI_TENANT` 後此問題暴露（官方不注入故沒早爆，
且 whoami 不需 binding）。

**正確做法**：service binding 一律用 **`[[services]]` array-of-tables**（每個 binding 獨立頂層 table，
不受 `[vars]` 影響），**不要用 inline `services = [...]`**——尤其檔案有 `[vars]` 時。對齊官方
`cypher-executor/wrangler.toml` 慣例（它用 `[[services]]` 故不中招）。

**驗法（決定性）**：`wrangler deploy --dry-run --outdir /tmp/x` 列 bindings：
- 正確 → `env.CYPHER_EXECUTOR (arcrun-cypher-executor)  Worker`
- 中招 → `env.services ([{"binding":...}])  Environment Variable`（binding 變成一個叫 services 的 env var）。

**通用教訓**：TOML 頂層 key 必須在任何 `[table]` header 之前；要在 table 後宣告的集合用
array-of-tables（`[[x]]`）不用 inline array。改 toml 後用 `wrangler --dry-run` 驗 binding 真的在
（別只看「檔裡有寫」）。連帶：這跟 mistakes #17「注入順序」、#11「死端點假綠」同類——**配置寫了 ≠ 生效**，要驗。日期：2026-06-27。

---

## 22. 零件退役只改一處 → 殘留誤導下個 CC；硬塞 canonical_id 是假綠（2026-06-28，issue #13）

**背景**：issue #13 收尾零件降級遺留。降級/退役某零件（telegram/gmail/sheets/line 等
從「內建零件」改成「走 recipe」、ai_transform_compile/run 直接刪）時，**AI 搜尋零件的來源不只一個**，
只改一處 → 別處殘留會讓下個 CC（或 Haiku 操盤手）誤判「這零件還在/還能用」。

**錯誤模式 A（殘留誤導）**：以為「示例 yaml 改完就退役乾淨了」。實際 AI 找零件有**三個源**，
退役要三個一起清，否則殘留＝錯誤環境信號（同 [[死代碼是錯誤環境信號]] 精神）：
1. **示例 yaml**（`component: telegram` 之類）— 步驟1 已改成 recipe canonical_id（`telegram_send`）。
2. **`cli/src/commands/parts.ts` 硬編碼清單** — 步驟2 已清（telegram/gmail/sheets/line 改 recipe canonical_id、
   移除已刪的 ai_transform_compile/run）。**parts.ts 是手寫常駐清單，必然 stale** → 退役 SOP 要把它列進「必清的源」。
3. **offline validate**（第三個污染源）— **這個是設計不是殘留**：offline validate 刻意跳過遠端查零件
   （不連網就能驗），所以它「不檢查零件存不存在」是 by design，留 issue 不當 bug 修。

**錯誤模式 B（硬塞 canonical_id ＝假綠）**：gmail **讀取**（`fetch_unread` / `action: list`）
**沒有對應 recipe**，把它硬指成 `gmail_send`（寄信 recipe）＝張冠李戴的假綠（mindset §7）。
正解：**留 TODO 待 seed 補 `gmail_list` recipe**，不硬塞一個語意不符的 canonical_id 充數。
「有個 canonical_id 可填」≠「這個 canonical_id 對」。

**正確做法**：
- 退役零件 → **同步清三源**（示例 / parts.ts / 任何硬編碼清單），別只改一處宣布完成。
- 沒對應 recipe 時 → 誠實留 TODO + 發 issue 補 seed，**不硬塞語意不符的 id**。
- 退役 SOP（[[死代碼是錯誤環境信號]] 的姊妹）寫進流程：清殘留 = 清「AI 搜尋零件的所有源」，不只宣告。

**通用教訓**：「改完一處＝退役完成」是錯覺；常駐硬編碼清單（parts.ts）必 stale，是退役必查項。
連帶：這跟 #19「死端點假綠」、#15「假設 v3 route 還在」同類——**陳述「退役/遷移完成」前問「所有引用源都掃過了嗎」**。
日期：2026-06-28。

---

---

## 23. `acr update` 部署源＝GitHub codeload，不是本地/Gitea checkout（2026-07-02，Arcrun#3 雲端壓測）

**背景**：雲端工人（cloud-worker，只能碰 Gitea，GitHub 受 D20 鎖死）在 Gitea 本地改完 `cypher-executor/src/routes/console.ts`
後想照 issue 指示跑 `acr update` 部署，才發現 `cli/src/lib/deploy.ts downloadRepoTarball()` **永遠從
`https://codeload.github.com/uncle6me-web/Arcrun/tar.gz/main` 抓部署物**，完全不讀本機/任何本地 checkout
的 git 狀態——「改了 Gitea 的 code」和「acr update 部署的 code」是兩條互不相干的路徑。

**錯誤模式**：以為「本地改完 code → acr update」就會部署到新 code（跟一般 `git push && deploy` 直覺相反）。
`acr update` 的心智模型其實是「拉官方 GitHub release」，不是「部署我剛編輯的目錄」。

**後果**：雲端工人若真的信了直覺跑 `acr update`，會部署**舊的 GitHub 版本**，蓋掉／忽略掉自己剛做的修改，
卻因為 wrangler deploy 回報成功而誤判「已上線」——經典假綠（同 #19 精神，但根因在「部署源選錯」而非「端點死」）。

**正確做法（本次採用，避免碰 GitHub 又要讓本地改動生效）**：
1. 手動複刻 `injectWranglerConfig` 的注入邏輯（KV id／WORKER_SUBDOMAIN／MULTI_TENANT／KBDB_BASE_URL／
   strip `[[routes]]`+`[[r2_buckets]]`+`[ai]`），`wrangler kv namespace list`／CF API 查真實 KV id 對齊。
2. 直接在本地 worker 目錄（如 `cypher-executor/`）patch 一份 `wrangler.toml`（**先備份**），
   `wrangler deploy --dry-run` 核對 binding 與 CF API 讀到的既有線上設定一致，再真部署。
3. 部署完**立刻 restore 備份**，確保 git 追蹤的 `wrangler.toml` 不留下帳號專屬 id（KV id 雖非機密，
   但不該混進 git 歷史）。
4. 這條路只適合「單一 worker 的小改動」；牽動多 worker（tier1 component + tier2 全套）改動時，
   人工複刻的成本會接近重寫 deploy.ts，屆時應該讓 leo 決定要不要開一條「本地/Gitea 來源」的部署路徑
   （跨專案結構決策，四題命中，不該 CC 自己裁）。

**懸而未決（等 leo 裁，非本次阻塞項）**：`acr update` 硬綁 GitHub 這件事，跟 InkStoneCo D20「cloud-worker
只碰 Gitea」的架構前提有結構性衝突——只要開發者在 Gitea 改 code，`acr update`／`acr init --self-hosted`
就永遠部署不到最新版，除非有人（leo）把 Gitea main 同步回 GitHub 再切新 release，或改 `ARCRUN_REPO`/
下載來源支援 Gitea tarball。這是「部署繞開 GitHub」鐵律 vs 「acr CLI 用 codeload.github.com 當唯一部署源」
的真實矛盾，非本次任務能力範圍解決，留給 leo 裁後續方向。

**通用教訓**：CLI／routine 文件寫的操作步驟（如「跑 acr update」）背後可能藏著跟直覺不符的資料來源假設；
碰到「部署完 curl 卻沒看到改動」先查「這個部署命令到底從哪裡抓 code」，別預設「本地改了=會生效」。

---

## 24. Worker 內嵌單檔 HTML 的三層跳脫＝隱形破版源；驗證要「求值後」做（2026-07-04，Mira Console 完整版）

**情境**：console 頁是 TS template literal 包整份 HTML＋inline JS。字面上要穿三層：
TS template（`\\d` 才輸出 `\d`、內層禁裸 backtick／`${`）→ inline JS（regex、字串拼接）→ HTML。
肉眼／tsc 都看不出內層 JS 是否被跳脫層弄壞——tsc 只驗 TS 合法，內嵌 JS 語法錯了照樣綠。

**防呆法（本次固化的驗證鏈，比 tsc 多三步）**：
1. 用 node `new Function` 把 template literal **求值成真 HTML**（模擬 worker 輸出）；
2. 抽出 `<script>` 內容跑 `node --check`（inline JS 語法層）；
3. 對 esc/mdRender/parseAtMs 這類純函式抽出來跑**行為斷言**（含 XSS escape、epoch 秒/毫秒/ISO 容錯）——
   regex 跳脫錯誤只有這層抓得到（語法對但行為錯）。
腳本模式見 2026-07-04 session scratchpad（check-html.js / check-fns.js），下次改 console 直接複刻。

**連帶誠實缺口（憑證頁）**：`DELETE /credentials/:name` 仍是舊 KV 路徑（T9 範圍未動），
但目錄已改 D1（新 `GET /credentials/catalog`）——UI 若直接接 DELETE 會假綠（KV 刪了、D1 目錄還在、
Workers Secret 也還在）。正解＝刪除鈕標「即將開通」等 T9，**不要接舊端點裝有效**。
同型判準：介面接動作前先問「這端點操作的儲存層，跟我列表讀的是同一層嗎」。

---

## 25. 組態全綠，介面卻是舊世代——「驗了組態」被當成「驗了線上」（2026-08-08）

**leo 原話**：「已經發生過一次這個錯誤，**把舊版界面上到 prod，你要確定不可再犯**。」

**現場**：實測三個對外網址，`apiBase` / `profile.views` / `profile.home` **三項全過**，
而它們跑的是 07-22 那一代的 portal（82,911 bytes、舊金色 serif 品牌、`Songti` 12 處）；
repo 早已是 343,969 bytes 的新品牌世代，`Songti` 一處不剩。
⇒ **一個網址可以組態完全正確、同時對外展示一套早就被淘汰的介面，而所有機械檢查都說它綠。**

**兩個根因，分開記**：

1. **驗證的維度少了一個**。組態（連去哪、開哪幾頁）與世代（跑的是哪一版前端）是**兩件事**，
   只驗前者會得到有害的綠燈——它讓人以為驗過了。
   解：`console-ui/scripts/verify-live.mjs` 加第二層「世代指紋」＝逐一抓線上資產、
   遮掉本來就該隨部署目標不同的那兩行（VIEWS/HOME），**其餘按位元組比對 repo `public/`**。
   位元組比對是刻意的：**不用關鍵字清單**——清單要人維護，而舊世代能無聲上線，
   正是因為沒有人記得維護它。

2. **手工維護的關鍵字閘會腐爛，而且會反過來咬你**。t160 那道世代閘寫的是
   「portal 全文含『登記新庫』就拒部」。08-03（`66f1b59`）有人在 portal 加了一則
   **說明「已經把登記新庫拿掉了」的 HTML 註解** ⇒ 這道閘從那天起每次都誤判，
   `npm run deploy:personal` 連續五天推不出去，而錯誤訊息說的是「你的 UI 是舊代」。
   解：文字閘比對前先剝掉 HTML 註解（只看使用者看得到的內容），並降級成輔助——主判準是指紋。

**判準（下次照用）**：問「線上這一份**是不是我們手上這一份**」，
不要問「線上這幾個設定值對不對」。前者一句話涵蓋後者答不出來的東西。

**還有一半是「有沒有人記得跑」**。再好的檢查放在沒人執行的腳本裡等於不存在（這個 repo 已有數支那種）。
故三處接死：① `deploy.mjs` 推完自動回頭驗線上，不過就算本次部署失敗；
② `.deploy-state.json` **只在線上實測通過後**才寫（不是跑過指令就寫）；
③ Stop hook（`stop-check-sync.sh`）每回合離線比對「手上這一代 vs 最後一次驗過的部署」，
在 CC 要說「做完了」的那一刻出聲。

---

## 26. 「身分來自環境變數」——同一句話寫錯兩次，因為規則有、機制沒有（2026-08-12，Arcrun#105／#108）

**症狀**：leo 打開藏書地圖回 **0 個庫**，同一分鐘 KBDB 裡有 **1854 條三元組**；
`arcrun_whoami` 顯示 admin／全部知識庫，`kbdb_search` 也查得到東西——**只有地圖那格是空的**。

**根因**（不是資料掉了，是讀寫兩端各拿一個來源）：

| | 寫入端用什麼當 owner_id | 讀取端用什麼過濾 |
|---|---|---|
| 之前 | `~/.arcrun/config.yaml` 的 `api_key`（CLI push／小幫手上傳／MCP，leo ＝ `bfezv28v`） | `env.CONSOLE_TENANT \|\| "leo"`（repo toml 帶的**官方 prod 值**） |

`acr` 從來不注入 `CONSOLE_TENANT`，所以那個 `"leo"` 不是理論邊角，**是每台 self-hosted 實例的實際行為**。

**這是第二次**。`#105` 前一天才修掉 `env.MCP_OWNER_NAMESPACE || "leo"`——同一句話，換一個檔案。

**判準（下次照用）**：
1. **「這個字串是用來決定誰的資料嗎？」** 是 → 它是身分，不是部署設定。
   身分要嘛來自請求（登入 session／`X-Arcrun-API-Key`），要嘛來自「寫入端用的那個值」，
   **不可以是一個各自抄一份的環境變數預設值**。
2. **`|| '預設值'` 出現在身分解析路徑上＝把「這台機器沒設定」偽裝成「你沒有資料」**。
   解析不到就誠實丟錯（#100 同一條：讀不到就說讀不到）。
3. **「規則存在但沒有機制驗證」＝它會再犯**。所以本次除了修 bug，還留下三道會擋的：
   - 型別閘：`TenantId` 只能由 `cypher-executor/src/lib/tenant.ts` 產出，
     資料面過濾只吃 `ownerQuery()/ownerField()` → 拿隨手一個 string 去過濾，`tsc` 當場不給過。
   - 出貨閘：`scripts/build-worker-artifacts.mjs` 編成品前先跑租戶來源檢查
     → **違規的碼編不出成品、出不了貨**（不是「有人記得跑才會發現」）。
   - 這道閘自己可測：規則是純函式（`cypher-executor/scripts/tenant-source-rules.mjs`），
     `tests/tenant-gate.test.ts` 逐條驗「壞例子會擋、11 種合法寫法零誤攔」。
     **誤攔比漏攔更容易殺死一道閘**——被擋煩了就有人把它關掉。
4. **修法不能比 bug 更危險**：`acr update` 注入 `ARCRUN_NAMESPACE` 前**先驗**
   （`GET /kbdb/map?owner_id=<api_key>` 查得到庫才寫）。無條件覆蓋會把「知識本來就在
   `CONSOLE_TENANT` 底下」的一鍵安裝實例指向空的那一格——那是 #97/#106 那類
   「更新一次把人家的東西弄不見」。

**順手挖出的同族**（同一道閘一次抓到）：`console-dashboard.ts` 有 **4 處**、
`console-auth.ts` 有 1 處相同寫法——console 首頁的規模數字與藏書地圖對 leo 也一直是空的。

## 快速檢查清單（做新功能前）

- [ ] 這是工作流還是零件？問「有必要嗎？」
- [ ] API 有對應端點嗎？否 → 先補 API，不是在介面層拼裝
- [ ] 有 SDD 嗎？沒有 → 停手問 richblack
- [ ] 這段邏輯換介面（CLI → MCP）要重寫嗎？要 → 違反薄殼原則
- [ ] 會修改 KV/D1 的鑰匙嗎？是 → 檢查遷移冪等性 + 向後相容
- [ ] 需要人類同意嗎（暴露 / credential）？是 → 確保非 TTY 時拒絕
- [ ] 怎麼驗證完成？只靠輸出訊息 → 不夠，需 2xx + 數據
- [ ] 測試或初始化步驟有「失敗時怎辦」嗎？→ 不靠提醒，靠機制強制
- [ ] 碰 KBDB？確認打基本盤現存 route（/entries /templates /records /entries/search），別假設 v3 /blocks /search 還在
- [ ] KBDB 要新可查欄位？用 json_extract 查 metadata_json，別加真欄（表不變鐵律，#18）
- [ ] 改 deploy.ts toml 注入順序？想「後面還有沒有 pass（如 strip）會動同一段」（#17）
- [ ] 新增/改薄殼工具？grep 確認它打的 server route 真的存在，別假綠（#19）；宣稱對齊前跑 thin-shell-smoke.sh
- [ ] 想用 git log 查某檔歷史？先 git check-ignore——gitignored 檔無 git 史，時間靠檔內註記（#20）
- [ ] 改 wrangler.toml service binding？用 [[services]] 不用 inline services=[...]（[vars] 後會被吸走）；改完 wrangler --dry-run 驗 binding 真在（#21）
- [ ] 退役/降級某零件？同步清「AI 搜尋零件的三個源」＝示例 yaml + parts.ts 硬編碼清單 + （validate 跳過是設計）；別只改一處宣布完成（#22）
- [ ] 沒對應 recipe？誠實留 TODO + 發 issue 補 seed，別硬塞語意不符的 canonical_id 充數（假綠，#22）
- [ ] 本地/Gitea 改完 code 想 `acr update` 部署？先確認：它抓的是 GitHub codeload tarball，不是你剛改的目錄（#23）
- [ ] 改完前端說「做完了」？先問**線上跑的是不是這一份**（`cd console-ui && npm run verify`）——組態綠不代表世代對（#25）
- [ ] 要寫「含某關鍵字就擋」的閘？先想「有人寫一則說明它已被移除的註解時會怎樣」——關鍵字閘會腐爛，優先用指紋（#25）
- [ ] 寫下 `env.X || '預設值'`？先問「這個字串是用來決定誰的資料嗎？」是 → 它是身分不是設定，不准有字面預設值（#26）
- [ ] 要用某個字串過濾 owner_id？確認它與**寫入端**用的是同一個來源，不是另一份手抄的環境變數（#26）
- [ ] 留了一道新的閘？它自己有測試嗎、誤攔案例驗過嗎、擋不擋得到自己？（#26）
