Files
Arcrun/.claude/rules/07-thin-shell.md
T
uncle6me-web fad5da0e17 rules/07 §3.6:自舉例外——能力只實作一次,但不一定要是 HTTP API
代補 PR #111 的 agent 想加但改不動的段落(該檔受保護)。

立這條的原因不是理論:#97 的修法一開始寫在 cli/src/lib/resource-resolver.ts
(能力住在介面層,違反 §0)⇒ 安裝器拿不到它 ⇒ 同一個 bug 只修了一半,
走 acr 的人有保護、走 install.arcrun.dev 的人沒有——而所有真實用戶走後者。

leo 2026-08-12:「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。」

記三件,都是為了不讓下一個人「修正」回去:
① 為什麼不放 cypher API(自舉/輸入是使用者自己的帳號狀態/它是純函式)
② §0 的正確讀法是「只准有一份、不准住在單一介面裡」,放 API 只是常見手段
③ npm 打包例外:cli/ 下的逐位元組副本由 sync --check 機械擋漂移,不是第二份實作

📍 repo:matrix/arcrun(shared/resource-rule/、cli/、.claude/rules/07)
   + products/arcrun-rag(安裝器待接上,已派工)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:50:53 +08:00

166 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 薄殼原則(鐵律)— 能力長在 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)只實作一次,放在 APIcypher-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 層拼湊 upsertrecipe/零件補 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. 呼叫 APIHTTP 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 呼叫的封裝 |
| **自家** APIKBDB / 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 API2026-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「不可能繞過」)。