# 薄殼原則（鐵律）— 能力長在 API，介面只暴露

> 來源：`docs/壓測報告.md` §5.4/§5.5（設計者本人於壓測中釐清）+ DECISIONS §1。
> 違反此原則的典型後果：每改一個能力要同步多份介面、介面間漂移（壓測 §5.1：CLI 改了讀
> 全域/專案/.env，MCP 沒跟上，兩者打不同帳號）、能力被某個介面綁架後別的介面用不到。
> 這條由 `.claude/hooks/pre-write-guard.sh` 規則 7.x 部分強制（見下「hook 強制範圍」）。

---

## 0. 一句話

**所有能力（business logic）只實作一次，放在 API（cypher-executor HTTP 端點）。
CLI / MCP / Python lib / JS lib 全是薄殼：只做「介面轉換 + 暴露」，不含任何商業邏輯。**

```
   CLI ─┐
   MCP ─┤   ← 全是薄殼：參數解析 / 格式轉換 / 暴露，不含商業邏輯
   Python lib ─┤
   JS lib ─┘
        ↓ 全部呼叫同一個
   ┌──────────────────────────────┐
   │  API（唯一真相，能力都在這）        │
   └──────────────────────────────┘
```

---

## 1. 什麼是「能力下沉到 API」（正例 vs 反例）

### 正例：upsert
- ✅ **API 提供 `upsert` 端點**（內部 GET 找→有則 update 無則 insert）。CLI/MCP/lib 只呼叫它。
- ❌ 在 MCP 裡自製「先 call update API、失敗再 call insert API」的拼裝邏輯。
- ❌ 在 recipe 層拼湊 upsert（recipe/零件補 API 缺的能力 = 走歪；正解是補在 API）。

### 正例：seed recipe（壓測 §4.1 的反例修正）
- ✅ **API 在「部署/註冊完成」時保證 recipe 就緒**（seed 是 API 行為，由一個端點完成 `POST /init/seed`）。
- ✅ **種子資料（清單）放 server**：`cypher-executor/src/lib/*-seeds.ts`。「裝好後預設有哪些 recipe」
  是 API 的能力，種子資料是這能力的一部分。薄殼只呼叫 `/init/seed` 一次。
- ❌ 在 CLI `init.ts` 裡用迴圈 POST 11 個 recipe + 客戶端「全部成功才 seed」的 if 判斷
  （這正是 §4.1 seed 永遠不被 seed 的根因：邏輯被寫進了某個介面）。

> **種子資料檔（`*-seeds.ts`）是一個普遍類別，不是某個零件的特例。**
> 它含 endpoint / `{{template}}` 字串（recipe 的資料欄位），rule 02 §2.2 hook 對**整類** `*-seeds.ts`
> 豁免 endpoint/template 檢查——因為那是「資料宣告」不是「呼叫實作」。新增任何 `xxx-seeds.ts`
> 自動適用，**不需為個別零件/recipe 改 hook**（richblack 原則：不為單一零件改全域規則）。

### 判準口訣
> **「這段邏輯換一個介面（CLI→MCP）要不要重寫？」**
> 要重寫 → 它是能力，該在 API。
> 不用重寫（只是把 API 回傳值換個格式印出來）→ 它是薄殼該做的事。

---

## 2. 薄殼「允許」做的事（窮舉）

1. 解析介面慣例的輸入（CLI 吃檔案路徑 / MCP 吃 JSON 參數）→ 轉成 API 期望的 payload。
2. 呼叫 API（HTTP fetch / service binding）。
3. 把 API 回傳值轉成該介面的輸出格式（CLI 印彩色文字 / MCP 回 structured JSON）。
4. **client 端加密**（AES-GCM）——唯一例外，因 API 期望收到已加密 payload（見 rule 01 加解密）。
5. 讀取「身份設定」（哪個帳號 / 哪個 cypher URL）——但所有薄殼必須讀**同一份**身份來源
   （見 §4 統一帳號來源）。

## 3. 薄殼「禁止」做的事

1. ❌ **在薄殼介面（CLI/MCP/lib）裡用多個 API 呼叫拼裝**出一個 API 沒有的能力（upsert / seed / 任何 N-step 編排）。
   > **界線（2026-06-26 收窄，issue #4）**：禁的是「**把編排邏輯寫進介面層 TS**」，不是禁「用資料方式（workflow/code-node）自救」。
   > 自家 API 缺能力 → 補進 API（你能改）；**第三方 API 缺能力 → 走 workflow/code-node 補丁是合法的**（你改不了第三方 API，不能被規則卡死）。詳見 §3.5 自力救濟階梯。
2. ❌ **用零件**補 API 缺的能力 → 污染零件庫（缺能力 → 先看 §3.5 階梯，真需新穩定能力才走零件 PR）。
   > 這條的原始精神（要保留）：當初是 AI 把多步驟工作寫成**零件**污染零件庫，才訂此禁令。**禁的是「亂建零件」，不是「禁止任何補丁」。**
3. ❌ 寫死判斷來補 API 缺口（例：`deployFullyOk` 那種 client 端 gate）。
4. ❌ 同一 API 能力在不同介面用不同參數簽名（`validate` 在 CLI 吃 YAML、在 MCP 卻要 `api_key`+`graph` = 底層分歧，違反「同一 API」）。差異只能來自介面慣例（檔案路徑 vs 字串），不能來自底層實作。
5. ❌ 任一薄殼連的帳號 / 後端與別的薄殼不同（CLI 連自架、MCP 連平台 = 違反「同一 API」前提）。

---

## 3.5 缺能力時怎麼補：自力救濟階梯（普世規則，issue #4）

> **問題**：§3 舊版預設「缺能力 → 去補 API」，這**預設 API 是你能改的**。對**第三方 API**（如 Google Sheets：一次只能倒全部、輸出前無法 filter）不成立——若 Google 不開該 API、規則又禁用 workflow「倒出來自己篩」，用戶被自己的規則卡死。
> **解法**：把「補丁」分層，維持零件庫最小，但開放「用資料方式自救」的合法路徑。

**主界線（一句話）**：**「那個 API 你能不能改？」** 自家 API 缺能力 → 補 API；第三方 API 缺能力 → workflow/code-node 補丁。

| 情況 | 正解 | 為何 |
|---|---|---|
| 能打既有 API | **recipe**（沒有就建 recipe） | 單一 API 呼叫的封裝 |
| **自家** API（KBDB / cypher）缺能力 | **補進 API** + 可同時發 issue | 你能改，能力該長在 API |
| **第三方** API 缺能力（gsheets filter / 無 upsert API） | **可投稿的 workflow 補丁** + 發 issue 建議原廠加 API | 你改不了第三方 API，但不能被卡死 |
| 非 call-api 的純計算（如整篇文章轉大寫） | **code-node**（空白 code 零件內寫 JS） | recipe/workflow 都做不到，又不該為此建一堆專用零件 |
| 真需新穩定能力（極少數） | 自建零件 → PR | 維持零件庫最小，只有非用零件不可才建 |

**三個配套原則**：
1. **補丁 workflow 可像 recipe 一樣被呼叫，但明示它是 workflow、且可投稿**（呼應 wishlist C6「工作流即零件」）。讓 AI 一遇阻就「用資料方式」自救，而非建零件。
2. **code-node**：原廠不提供某純計算時，AI 至少能用一個空白 code 零件寫 JS 自救（呼應 wishlist C1；架構決策：JS 在 CF Workers isolate 跑，不嵌 QuickJS/Rust）。
3. **補丁是過渡**：原廠出 API 後，補丁 workflow 因效能較差自然被減少使用、淘汰。

**upsert 範例**：有些服務原廠提供 upsert API（→ recipe 直接打），有些沒有（→ 做一個 upsert workflow 達成，**而非建專用零件**）。§1 把 upsert 當「該補進 API」的正例——那**只對自家 API 成立**；對改不了的第三方 API，arcrun 端永遠補不進去，正解是 workflow 補丁。

> **與 hook 的關係**：§3.1 禁的「介面層拼裝」由 `pre-write-guard.sh` 7.x 擋（範圍 `cli/src/`、`arcrun-mcp/src/` 的 TS）。**workflow/code-node 補丁是資料產物（YAML / 空白零件內的 JS），不是介面層 TS → 本就在 hook 範圍外，合法不被擋。** hook 防線不變，本次只釐清「資料方式自救」是合法路徑。

---

## 3.6 自舉例外：能力該「只實作一次」，但不一定要是 HTTP API（2026-08-12 立）

> 立這條的原因：`Arcrun#97`（更新把使用者的工作流與登入弄不見）的修法一開始寫在
> `cli/src/lib/resource-resolver.ts` ——**能力住在介面層，違反 §0**。
> 後果不是理論：**安裝器（arcrun-rag）拿不到它，於是同一個 bug 只修了一半**，
> 走 `acr` 的人有保護、走 `install.arcrun.dev` 的人沒有——**而所有真實用戶走後者**。
> leo 2026-08-12：「**根本就不應該在 CLI，我要的是一個大家都可以用到的規則。**」

修法（PR #111）把它搬到 **`shared/resource-rule/`：一份零依賴 ESM**，
`acr` 與安裝器共用。**它刻意不是 cypher 的 API 端點**，三個理由：

| 為什麼不放 API | 說明 |
|---|---|
| **自舉** | 這條規則要在「決定怎麼裝」的當下用得到，而安裝器的工作正是把 cypher 生出來。放進 cypher ＝ 要先有雞才能有蛋。 |
| **輸入是使用者自己的帳號狀態** | 判斷依據是使用者 CF 帳號上的綁定。送去平台託管的 worker 換答案 ⇒ ①「能不能安裝」綁在平台是否活著 ②使用者的帳號拓撲交給第三方。 |
| **它根本不需要是服務** | 這是**純函式**，唯一的 IO 由呼叫端注入。**§0 要求「能力只實作一次」，不是「能力一定要是 HTTP」。** |

🔴 **所以本檔 §0 的正確讀法是**：能力**只准有一份**，且**不准住在任何單一介面裡**。
「放 API」是達成它的**常見手段**，不是唯一手段。
**判準仍然是那句口訣**：「這段邏輯換一個介面要不要重寫？」要 → 它是能力。

📌 **給下一個人**：看到 `shared/` 底下的純函式**不要「修正」成 API 端點**——
先讀 `shared/resource-rule/README.md §2`，那裡記著評估過並否決的其他形態
（共用 npm 套件＝自舉問題換位置；做成零件＝要用 TinyGo 重寫一次，那才是第二份實作）。

📌 **打包例外**：`acr` 是獨立 npm 套件，`npm pack` 打不進套件目錄外的檔案 ⇒
`cli/` 下必須有一份**逐位元組副本**。那不是第二份實作——
`scripts/sync-resource-rule.mjs --check` 一有漂移就 exit 1，且 `build`／`test` 都會先跑它
（同 `cli/harness/` 的既有慣例）。**手改副本 ＝ build 紅 ＝ publish 擋下。**

---

## 4. 統一帳號來源（薄殼共用同一身份）

所有薄殼讀**同一份**身份設定：
- self-hosted：`~/.arcrun/config.yaml` / 專案層 `.arcrun.yaml` / `ARCRUN_*`、`CLOUDFLARE_*` env（見 `config-layering.md`）。
- standard：平台 api_key。

**MCP 目前的已知違反**（壓測 §5.2）：MCP 用 Cloudflare service binding 焊死平台 `arcrun-cypher-executor`，
self-hosted 用戶用 MCP 連不到自己的 cypher。修法見
`docs/3-specs/arcrun/sdk-and-website/mcp-account-source.md`（SDD proposal）。

---

## 5. 出貨順序（最低出貨標）

- **CLI + MCP 兩個薄殼先到位**（AI 偏好 MCP，故 MCP 不可長期落後），且兩者覆蓋**同一組 API 能力**。
- Python / JS lib 隨後補。
- 出貨順序由「介面被誰用」決定，不是由「哪個好做」決定。
- **介面進度本來就會不一致**（薄殼模型的預期狀態）——這本身不是 bug。
  bug 是「**底層 API 能力不齊 / 介面含了不該含的邏輯 / 帳號來源不統一**」這三者。

---

## 6. hook 強制範圍（與「靠人判斷」的邊界）

`pre-write-guard.sh` 規則 7.x 能擋的是**語法層可偵測**的反例：
- CLI/MCP 檔案內出現「迴圈 POST 多個 recipe」「先 update 失敗再 insert」這類拼裝 pattern 的明顯特徵 → 警告/擋。
- 新增 `seedApiRecipes` / `seedAuthRecipes` 這類「seed 邏輯寫在介面層」的函式 → 擋（改去 API）。

**hook 擋不了的**（需 CC 自律 + code review）：
- 把商業邏輯藏在看似無害的 helper 裡。
- recipe 層拼裝（recipe 是資料，hook 不解析語意）。
→ 故本檔是 mindset，hook 是底線；兩者都不可省。誠實限制見 mindset §7（不假裝 hook「不可能繞過」）。
