Files
Arcrun/shared/resource-rule
richblack 68f042cfd0 fix(resource-rule): 帳號上資源超過一頁時,規則看到的必須是全部(Arcrun#123 續集)
三支清單方法只打 `?per_page=100`,也就是**只看第一頁**。這個洞在 #123 的修法
前後嚴重度不同,這才是它必須跟那張票一起修的理由:

  · 修法前:被截掉的是「worker 綁著的那顆」→ 2b 判「綁著的資源不見了」
            → blocker → 停手。誣告使用者,但安全。
  · 修法後:被截掉的是「同名殘骸」→ 2c 判「這個名字沒被佔走」
            → 去建 → CF 回 title already exists → #123 的死路原樣回來。

⇒ 修法把它從「叫得太大聲」變成「安靜地復發」。分開出貨等於把 #123 的災情
  延後到「資源比較多的帳號」再爆。

做法:`cfListAll()` 翻到底;翻不完、或數量對不上 CF 回報的 `total_count`,
一律 throw ⇒ 變 blocker ⇒ 整趟停手(README 規則第 3 條)。
「我不知道」不准被當成「它沒有」。

三支端點的分頁行為不一樣(2026-08-14 在 geek6688 帳號實測,唯讀):
  /storage/kv/namespaces  result_info 有 total_pages
  /d1/database            result_info **沒有** total_pages ⇒ 不能拿它當終止條件
  /vectorize/v2/indexes   result_info 是 null,不分頁(分頁參數被忽略)
所以終止條件只用「三支都有或都沒有」的兩件事:result_info 在不在、total_count 對不對得上。

fixture 的清單端點同步照真 CF 的形狀分頁(三支各自不同)——假資料失真就會養出
「拿 total_pages 當終止條件」這種在 D1 上必壞的實作,而測試全綠。

新增 tests/list-pagination.mjs(在舊碼上實測會紅,且第 ③ 段直接重現
「無 blocker → 排 10 顆新建 → CF 回 title already exists」的 #123 死路)。
cli 73 項全綠、demo 與 half-finished-install 全綠。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 19:52:24 +08:00
..

shared/resource-rule — 「這個實例該用哪些資源」的唯一一份規則

leo 2026-08-12: ①「如果你沒有裝,就是新的;如果你已經有,原來叫什麼名字就繼續用下去。」 ②「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。

① 是規則本身,② 是它該住哪裡。這個目錄就是 ②。


1. 規則(三句話)

判準是「這顆 worker 現在綁著誰」,不是「有沒有叫這個名字的資源」。

  1. 已部署的 worker 上綁著什麼,那就是事實 → 原封不動沿用,不管那顆資源叫什麼名字。
  2. 只有「確定沒有任何人綁過它」才准新建(新版本新增的 binding、或真的全新帳號)。
  3. 只要有一點說不準就整趟停手——讀不到綁定/綁著的資源不見了/同一個 binding 指向兩顆/ 該更新的 worker 一顆都不在 ⇒ 什麼都不建、什麼都不部署,把話說清楚讓人來判斷。

planResources()(不寫入,只出計畫)與 applyResourcePlan()(有 blocker 就拒絕執行)分兩段, 所以「被擋下的時候一顆資源都不會被建出來」是結構上的保證,不是靠誰記得寫 early return。

1.1 第 2 條的例外:上一次裝到一半死掉(Arcrun#123)

「沒有人綁過它」有兩種成因,第 2 條原本只想到第一種:

名字在帳號上 有 worker 綁著 該怎麼做
新版本新增的 binding/全新帳號 新建(照舊)
上次安裝建到一半就中斷 接回那一顆(否則 CF 回「title already exists」,這個帳號永遠裝不起來

接管同名資源的唯一依據是呼叫端在 BindingRequirement 上聲明 createNameIsOurs: true 意思是「這個名字是我用使用者自己的身分可重現地算出來的」——安裝器的 arcrun-rag-<slugFromEmail(email)>-kv-<binding> 合格;acr 從 wrangler.toml 讀到的裸 binding 名 WEBHOOKS不合格,因為使用者自己也可能拿那個名字去建東西。

沒聲明就撞名 ⇒ 停手(訊息帶 RES-NAME-TAKEN 錯誤碼讓使用者回報, 不叫他自己去 Cloudflare 後台動手)。

🔴不是把 #97 刪掉的「照名字 ensure」搬回來。差別:#97 是找不到就新建一顆頂上去 (會把活著的實例洗成空的);這裡是找到才沿用、找不到才照舊新建,而且排在 「已部署的綁定=事實」之後——名字永遠只在「確定沒有任何綁定可看」時才有發言權。

1.2 上面那條的前提:清單必須是完整的Arcrun#123 的續集)

1.1 整條規則建立在一個沒被說出口的假設上:「我列出來的,就是帳號上全部的資源」。 cf-resource-api.mjs 原本三支清單方法只打 ?per_page=100——只看第一頁。 CF 的 KV 上限是每帳號 1,000 顆,所以「超過一頁」不是理論狀況。

同一個截斷,在 1.1 修好前後後果不一樣,這才是它非修不可的理由:

被截掉的那顆 規則走到哪 結果
1.1 修好worker 綁著它,但它落在第二頁 「綁著的資源不見了」 blocker停手(誣告使用者,但安全)
1.1 修好:同名殘骸落在第二頁 「這個名字沒被佔走」 去建 → CF 回 title already exists ⇒ #123 的死路原樣回來

⇒ 1.1 把這個洞從「叫得太大聲」變成「安靜地復發」。

所以規約是:看不完整就不准當作看完了cfListAll 會翻到底;翻不完、 或翻出來的數量對不上 CF 自己回報的 total_count,一律 throw ⇒ 變成 blocker ⇒ 整趟停手(第 3 條)。「我不知道」永遠不准被當成「它沒有」。

三支端點的分頁行為不一樣2026-08-14 在 geek6688 帳號實測,別假設它們同款):

端點 result_info 備註
/storage/kv/namespaces {page, per_page, count, total_count, total_pages} 真分頁
/d1/database {page, per_page, count, total_count} 真分頁,但沒有 total_pages ⇒ 不准拿它當終止條件
/vectorize/v2/indexes null 不分頁pageper_page 被忽略,一次回全部

2. 為什麼在這裡,不在 cypher-executor 的 API

.claude/rules/07-thin-shell.md 的標準答案是「能力放 API」。這一條不走那條路,理由是自舉:

問題 說明
cypher 可能還不存在 這條規則要在「決定怎麼裝」的當下就用得到,而安裝器的工作正是把 cypher 生出來。把規則放進 cypher = 要先有雞才能有蛋。
輸入是使用者自己的帳號狀態 判斷的依據是使用者 Cloudflare 帳號上的綁定。送去平台託管的 worker 換一個答案 ⇒ ①「能不能安裝」綁在平台是否活著,②使用者的帳號拓撲交給第三方。
它根本不需要是服務 這是純函式:唯一的 IO 由呼叫端注入(ResourceApi)。薄殼原則要求「能力只實作一次」,不是「能力一定要是 HTTP」。

所以形態是一份零依賴的 ESM——Node 18+ 與 Cloudflare Workers runtime 都能直接 import, 不必編譯、不必連網、不必先有任何 arcrun 元件活著。

其他評估過的形態:共用 npm 套件 → 要多發一個 package + token,且安裝器得先 npm i 才能判斷, 自舉問題只是換個位置;做成一顆零件 → 得用 TinyGo/AssemblyScript 重寫一次,那正是「第二份實作」。


3. 檔案

檔案 內容
rule.mjs 規則本體:planResources / applyResourcePlan / parseWranglerRequirements 把 CF 回應讀成事實的 normalizeLiveBindings / normalizeLiveVars
cf-resource-api.mjs ResourceApi 的 CF REST 實作(只用 global fetch)。眼睛也要共用——見下 §5
installer-entry.mjs 安裝器唯一該碰的入口:resolveInstanceResources()
tests/fixture-account.mjs 假 Cloudflare 帳號(fetch 替身)+四種情境。清單端點照真 CF 分頁KV 有 total_pagesD1 沒有/Vectorize 不分頁),形狀是 2026-08-14 在真帳號實打抄回來的
tests/demo.mjs node shared/resource-rule/tests/demo.mjs——零依賴、零建置就能跑的示範
tests/half-finished-install.mjs #123 的迴歸守衛:上次裝到一半死掉的帳號,回來再按一次要裝得起來(§1.1)
tests/list-pagination.mjs #123 的續集:帳號上資源多到一頁裝不下時,規則看到的仍是全部(§1.2)

🔴 零依賴是硬規則:只准 import 同目錄的兄弟檔,不准碰 node:*。 有外部依賴就會有某條路吃不到它。cli/tests/single-implementation.test.ts ③ 會擋。


4. 兩條路怎麼取用

安裝器 / 任何 Worker(不需要副本)

安裝器本來就會下載本 repo 的 archive 當部署來源(.claude/rules/05-deploy-convention.md 「WASM 來源」),shared/resource-rule/ 就在那份 archive 裡:

import { resolveInstanceResources } from './shared/resource-rule/installer-entry.mjs';

const r = await resolveInstanceResources({
  accountId, apiToken,
  wranglerTomls: [cypherToml, registryToml, mcpToml, kbdbToml],  // toml 的「內容」,不是路徑
  mode: isUpdate ? 'update' : 'init',
});

if (r.blocked) {
  // 🔴 一顆資源都沒被建。把 r.blockers 原文顯示給使用者,**不要自己「試著繼續」**。
  return showAndStop(r.blockers);
}
// r.bindings  : { 'kv_namespace:WEBHOOKS': 'kvid-…', 'd1:DB': 'uuid-…', … }
// r.origin    : { 'kv_namespace:WEBHOOKS': 'adopted' | 'created', … }
// r.liveVars  : { 'arcrun-cypher-executor': { ARCRUN_BUNDLE_VERSION: '1.4.33', … } }  ← #106

安裝器不准自己判斷要不要建資源,也不准自己解讀 CF 的 binding 回應。只呼叫這一支。

acr CLI(需要一份鏡射)

arcrun 是獨立 npm 套件,npm pack 打不進套件目錄外的檔案 ⇒ 套件裡必須自帶一份。 cli/src/lib/resource-rule/ 就是本目錄的逐位元組鏡射,由 node scripts/sync-resource-rule.mjs 產生。

要改規則就改這個目錄,然後重跑 sync。 手改鏡射會被擋下: npm run buildnpm test 都先跑 sync-resource-rule.mjs --check 差一個位元組就 exit 1(同 cli/harness/ 的產生物+世代閘慣例)。


5. 為什麼連 CF client 也共用

判斷一致還不夠,看到的東西也要一致。

「已部署的 worker 綁著什麼」是從 GET /workers/scripts/{script}/settings 讀來的。 兩條路各自寫一份 client,只要有一邊把 404 當錯誤、漏了 per_page、少認一種欄位名 namespace_id vs id),那一邊就會「看不到既有綁定」—— 而看不到既有綁定的下一步,依規則就是新建

Arcrun#97 不需要規則寫錯,眼睛不一樣就足以重演。 所以 cli/src/lib/cf-api.tsCfAccountClientResourceApi 那七個方法全部委派cf-resource-api.mjs,自己不留實作。


6. 驗收

cd cli && npm test          # 73 項,含下列三組
node shared/resource-rule/tests/demo.mjs                # 安裝器那條路,零依賴獨立跑
node shared/resource-rule/tests/half-finished-install.mjs   # #123
node shared/resource-rule/tests/list-pagination.mjs         # #123 續集(清單分頁)
測試 證的事
cli/tests/two-paths-agree.test.ts 同一個帳號狀態餵給 acr 那條與安裝器那條,選出的 resource id 相同、建的東西相同、停手的理由相同
cli/tests/single-implementation.test.ts ①規則的 7 支函式全 repo 只有這裡有實作 ②鏡射逐位元組相同 ③共用層零依賴
cli/tests/resource-adoption.test.ts #97 本身的迴歸(沿用/不多建/四種停手情境),改共用層後照樣全過

四種情境(tests/fixture-account.mjsSCENARIOS):

  • fresh — 沒裝過 → 正常建新的(不能為了沿用而變成永遠不建)
  • installed — 裝過了 → 沿用原本那幾顆,工作流與登入 session 都還在
  • renamed資源在但名字與預期完全不同 → 仍然沿用(#97 的病根,專門驗)
  • half-finished資源已建、worker 一顆都沒部署 → 接回殘骸(#123 的病根)

另有一個與情境正交的旋鈕:makeAccount(情境, { decoyKv, decoyD1 }) 會在帳號上多塞 N 顆「別人的」資源,把我們自己那幾顆擠到第二頁以後——§1.2 的分頁測試靠它。


7. 相關

  • Arcrun#97 — 「我按了更新,工作流和登入全不見了」:CLI 那條已修,本目錄是把同一條規則交給所有路徑
  • Arcrun#106 — 重部署把 plain_text var(含版本標籤)洗掉:liveVars 就是那些標籤
  • Arcrun#80 / arcrun-rag#39 — 同一個「重複做 Arcrun 的工作」家族;Arcrun 是唯一編譯點的既有慣例
  • .claude/rules/07-thin-shell.md — 本目錄存在的依據