把「該用哪些資源」搬出 CLI——一份實作,acr 與安裝器吃同一條規則 #111

Merged
Leo merged 1 commits from fix/adopt-rule-shared into main 2026-08-12 15:44:50 +00:00
Owner

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

「這個實例該用哪些資源」換到安裝器就要重寫一次 ⇒ 依 .claude/rules/07-thin-shell.md 的判準口訣,
它是能力;而它原本住在 cli/src/lib/resource-resolver.ts(431 行)⇒ 那本身就是違規。
後果已經真的發生:acr 那條有 Arcrun#97 的修法、安裝器那條沒有 ⇒「我按了更新,工作流和登入全不見了」。

CLI 淨 -496 行(邏輯是搬走,不是複製)。


① 規則放在哪、為什麼(含自舉怎麼解)

shared/resource-rule/ —— 零依賴 ESM,Node 18+ 與 Cloudflare Workers runtime 都能直接 import。

檔案 內容
rule.mjs 規則本體(planResources / applyResourcePlan / parseWranglerRequirements)+把 CF 回應讀成事實的 normalizeLiveBindings / normalizeLiveVars
cf-resource-api.mjs ResourceApi 的 CF REST 實作(只用 global fetch
installer-entry.mjs 安裝器唯一該碰的入口 resolveInstanceResources()

為什麼不是 cypher-executor 的 API 端點(=自舉那一題)

薄殼原則的標準答案是「能力放 API」,這一條刻意不走,理由有三,都不是形式問題:

  1. cypher 可能還不存在。 這條規則要在「決定怎麼裝」的當下就用得到,而安裝器的工作
    正是把 cypher 生出來 —— 放進 cypher 等於要先有雞才能有蛋。
  2. 輸入是使用者自己帳號的綁定狀態。 送去平台託管的 worker 換一個答案 ⇒
    (a)「能不能安裝」從此綁在平台是否活著,(b) 使用者的 CF 帳號拓撲交給第三方。兩件都不該做。
  3. 它根本不需要是服務。 這是純函式,唯一的 IO 由呼叫端注入(ResourceApi)。
    薄殼原則要求的是「能力只實作一次」,不是「能力一定要是 HTTP」。

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

⚠️ 這是自舉例外,不是放寬:能放 API 的仍然放 API,不要拿自舉當藉口把業務邏輯搬回介面層。

順帶:連 CF client 也共用

判斷一致還不夠,看到的東西也要一致。「已部署的 worker 綁著什麼」是從
GET /workers/scripts/{script}/settings 讀來的;兩條路各自寫一份 client,只要有一邊把
404 當錯誤、漏了 per_page、少認一種欄位名(namespace_id vs id),那一邊就會
「看不到既有綁定」—— 而看不到既有綁定的下一步,依規則就是新建
#97 不需要規則寫錯,眼睛不一樣就足以重演。
所以 CfAccountClientResourceApi 七個方法全部委派共用 client,自己不留實作。


② 「只有一份」怎麼證明

  • 安裝器不需要副本:它本來就會下載本 repo 的 archive 當部署來源
    rules/05-deploy-convention.md「WASM 來源」),shared/resource-rule/ 就在那份 archive 裡,直接 import。
  • acr 需要一份鏡射arcrun 是獨立 npm 套件,npm pack 打不進套件目錄外的檔案。
    cli/src/lib/resource-rule/逐位元組鏡射,由 scripts/sync-resource-rule.mjs 產生
    ——同 cli/harness/(產生物進 repo + check:harness 世代閘)的既有慣例,不是為本票新發明的做法。

cli/tests/single-implementation.test.ts 把 leo 要求的那個 grep 寫成了會紅的東西:

#   ① 掃過 288 個原始碼檔,7 支規則函式的實作 全部只出現在 shared/resource-rule/(+機械鏡射)
#   ② cf-resource-api.mjs      sha256 70d278d7c55b9557  原稿 = 鏡射
#   ② installer-entry.mjs      sha256 5a397a23e7b01446  原稿 = 鏡射
#   ② rule.mjs                 sha256 38f8c722f54c62ec  原稿 = 鏡射
#   ③ cf-resource-api.mjs      import: ./rule.mjs
#   ③ installer-entry.mjs      import: ./rule.mjs, ./cf-resource-api.mjs
#   ③ rule.mjs                 import: (無)
ok 39 - ① 規則的實作全 repo 只有一份(原稿目錄 + 它的鏡射,沒有第三處)
ok 40 - ② CLI 帶的那份與原稿逐位元組相同(漂移=第二份實作偷偷長出來)
ok 41 - ③ 共用層零外部依賴(只准 import 同目錄的兄弟檔)

閘是真的會擋——實測手改鏡射一行後跑 npm test

> arcrun@1.3.14 check:rule
> node ../scripts/sync-resource-rule.mjs --check

✔ cli/src/lib/resource-rule/cf-resource-api.mjs  = 原稿(sha256 70d278d7c55b)
✔ cli/src/lib/resource-rule/installer-entry.mjs  = 原稿(sha256 5a397a23e7b0)
✗ cli/src/lib/resource-rule/rule.mjs 與原稿不一致(副本 34db99a88535 ≠ 原稿 38f8c722f54c)。
  這一份是**產生物**,不要手改:規則要改就改 shared/resource-rule/rule.mjs,然後跑 `node scripts/sync-resource-rule.mjs`。

❌ 1 項與規則原稿脫節。

npm run buildnpm test 都先跑這道閘 ⇒ 漂移擋在 build 與 npm publish 之前。


③ 兩條路一致性的 fixture 對照

shared/resource-rule/tests/fixture-account.mjs 造一個假帳號,假的是 fetch 不是 ResourceApi
——所以兩條路都真的走完 HTTP → 解析 → 判斷整條鏈(只假 ResourceApi 會跳過「怎麼把 CF 回應讀成事實」,
而那正是 #97 能重演的地方)。同一份帳號狀態餵給:

  • A acr 那條CfAccountClient + resource-resolver(CLI 真正跑的 import 鏈)
  • B 安裝器那條:只 import shared/resource-rule/installer-entry.mjs

cli/tests/two-paths-agree.test.ts 實跑輸出:

#   ── fresh:沒裝過(全新帳號,一顆 worker 都沒有)
#      binding                            acr 選的                   安裝器選的        一致?
#      d1:CREDENTIALS_DB                  d1id-NEW-1               d1id-NEW-1         ✓
#      d1:DB                              d1id-NEW-1               d1id-NEW-1         ✓
#      kv_namespace:ANALYTICS_KV          kvid-NEW-4               kvid-NEW-4         ✓
#      kv_namespace:CREDENTIALS_KV        kvid-NEW-3               kvid-NEW-3         ✓
#      kv_namespace:EXEC_CONTEXT          kvid-NEW-1               kvid-NEW-1         ✓
#      kv_namespace:OAUTH_KV              kvid-NEW-9               kvid-NEW-9         ✓
#      kv_namespace:RECIPES               kvid-NEW-5               kvid-NEW-5         ✓
#      kv_namespace:SESSIONS_KV           kvid-NEW-7               kvid-NEW-7         ✓
#      kv_namespace:SUBMISSIONS_KV        kvid-NEW-8               kvid-NEW-8         ✓
#      kv_namespace:USERS_KV              kvid-NEW-6               kvid-NEW-6         ✓
#      kv_namespace:WEBHOOKS              kvid-NEW-2               kvid-NEW-2         ✓
#   ── installed:裝過了(安裝器命名慣例 arcrun-rag-<instance>-kv-<binding>)
#      d1:CREDENTIALS_DB                  d1id-kbdb-REAL           d1id-kbdb-REAL     ✓
#      d1:DB                              d1id-kbdb-REAL           d1id-kbdb-REAL     ✓
#      kv_namespace:ANALYTICS_KV          kvid-analytics_kv-REAL   kvid-analytics_kv-REAL ✓
#      kv_namespace:CREDENTIALS_KV        kvid-credentials_kv-REAL kvid-credentials_kv-REAL ✓
#      kv_namespace:EXEC_CONTEXT          kvid-exec_context-REAL   kvid-exec_context-REAL ✓
#      kv_namespace:OAUTH_KV              kvid-oauth_kv-REAL       kvid-oauth_kv-REAL ✓
#      kv_namespace:RECIPES               kvid-recipes-REAL        kvid-recipes-REAL  ✓
#      kv_namespace:SESSIONS_KV           kvid-sessions_kv-REAL    kvid-sessions_kv-REAL ✓
#      kv_namespace:SUBMISSIONS_KV        kvid-submissions_kv-REAL kvid-submissions_kv-REAL ✓
#      kv_namespace:USERS_KV              kvid-users_kv-REAL       kvid-users_kv-REAL ✓
#      kv_namespace:WEBHOOKS              kvid-webhooks-REAL       kvid-webhooks-REAL ✓
#   ── renamed:資源在,但名字與預期完全不同(使用者自己改過/別的安裝器版本取的名)
#      d1:CREDENTIALS_DB                  d1id-kbdb-REAL           d1id-kbdb-REAL     ✓
#      d1:DB                              d1id-kbdb-REAL           d1id-kbdb-REAL     ✓
#      kv_namespace:ANALYTICS_KV          kvid-analytics_kv-REAL   kvid-analytics_kv-REAL ✓
#      kv_namespace:CREDENTIALS_KV        kvid-credentials_kv-REAL kvid-credentials_kv-REAL ✓
#      kv_namespace:EXEC_CONTEXT          kvid-exec_context-REAL   kvid-exec_context-REAL ✓
#      kv_namespace:OAUTH_KV              kvid-oauth_kv-REAL       kvid-oauth_kv-REAL ✓
#      kv_namespace:RECIPES               kvid-recipes-REAL        kvid-recipes-REAL  ✓
#      kv_namespace:SESSIONS_KV           kvid-sessions_kv-REAL    kvid-sessions_kv-REAL ✓
#      kv_namespace:SUBMISSIONS_KV        kvid-submissions_kv-REAL kvid-submissions_kv-REAL ✓
#      kv_namespace:USERS_KV              kvid-users_kv-REAL       kvid-users_kv-REAL ✓
#      kv_namespace:WEBHOOKS              kvid-webhooks-REAL       kvid-webhooks-REAL ✓

ok 39 - 兩條路一致 — fresh
ok 40 - 兩條路一致 — installed
ok 41 - 兩條路一致 — renamed

除了 resource id,還斷言建了什麼停手的理由也一樣
(「一邊停手、一邊照做」是最危險的分歧)。


④ 三種情境的實測

#   ① 新建:KV 9 顆、D1 1 顆(D1 共用:CREDENTIALS_DB = DB = d1id-NEW-1)
#   ② 沿用:WEBHOOKS → kvid-webhooks-REAL(工作流 3 支還在)|SESSIONS_KV → kvid-sessions_kv-REAL(登入 session 還在)|DB → d1id-kbdb-REAL(子庫 4 個還在)|新建 0 顆
#   ③ 名字全不同(例:WEBHOOKS 那顆實際叫 "kv-51mggz") → 仍沿用 kvid-webhooks-REAL,新建 0 顆

ok 42 - 情境① 沒裝過 → 正常建新的(不能為了沿用而變成永遠不建)
ok 43 - 情境② 裝過了 → 沿用原本那幾顆,工作流與登入 session 都還在
ok 44 - 情境③ 資源在但名字與預期完全不同 → 仍然沿用(#97 的病根,專門驗)

情境③ 的資源名(kv-51mggz 之類)刻意取成跟 binding 名毫無關聯的雜湊字串
——只要規則裡還有一絲「照名字對號」就會在這裡露餡。

安裝器那條路也能零依賴、零建置獨立跑node shared/resource-rule/tests/demo.mjs),
這本身就是「安裝器把 repo archive 拉下來就能直接用」的證據:

── installed(mode=update):裝過了(安裝器命名慣例 arcrun-rag-<instance>-kv-<binding>)
   d1:CREDENTIALS_DB              → d1id-kbdb-REAL             adopted
   d1:DB                          → d1id-kbdb-REAL             adopted
   kv_namespace:WEBHOOKS          → kvid-webhooks-REAL         adopted
   …
   本趟新建:KV 0 顆、D1 0 顆、Vectorize 0 顆|沿用既有版本標籤 ARCRUN_BUNDLE_VERSION=1.4.33

(末欄順帶證明 Arcrun#106plain_text var 沿用也走同一條路。)


全套驗證

$ cd cli && npx tsc --noEmit      # 無輸出
$ cd cli && npm test
# tests 58
# pass 58
# fail 0

58 = 原本 49(全數保留通過,含 resource-adoption.test.ts#97 迴歸)+ 本次新增 9。

npm 打包確認產生物真的出得去:npm pack --dry-run | grep -c resource-rule16
dist/lib/resource-rule/*.mjs 就位,載入後 ResourceApi 七個方法齊全。


◐ 沒做到的一件事(誠實標)

「在 youlin 上走安裝器那條路更新一次」沒有跑成。 兩個原因,都不是選擇性略過:

  1. 安裝器不在本 repo。 products/arcrun-rag/installer/oauth-prototype/worker.js 屬於
    arcrun-rag,本工作副本裡不存在(find 全無命中)。派工單也說明本票不做移交(另票)。
  2. 本工作副本取不到任何 CF 憑證(無 .env、沙箱讀不到 ~/.arcrun/),
    連唯讀查 youlin 帳號都做不到。

所以本 PR 交付的是「規則側」:安裝器需要改的是一行 import(見
shared/resource-rule/README.md §4 的完整片段),而不是抄一份邏輯過去。
接手 arcrun-rag 那張票時:

import { resolveInstanceResources } from './shared/resource-rule/installer-entry.mjs';
// 刪掉 worker.js:685 建 KV、:705 建 D1 的「照名字冪等」兩段,改呼叫這一支
// r.blocked → 把 r.blockers 原文顯示給使用者,不要自己「試著繼續」

在 youlin 上做更新前後 resource id 對照,應該是那張票的驗收
(依紅線「不確定時寧可中止」,我沒有在拿不到帳號的情況下猜著跑)。

另一件被擋下的

想在 .claude/rules/07-thin-shell.md §1 補一段「自舉例外」的正例(讓下一個人不會又把它寫回介面層),
但該檔是受保護檔案、本 session 改不動。內容已完整寫在 shared/resource-rule/README.md §2,
要的話可由總管代補。


📍 Arcrun#97(CLI 已修,本票是把同一條規則交給所有路徑)|Arcrun#106liveVars)|
Arcrun#80arcrun-rag#39(同一個「重複做 Arcrun 的工作」家族)|.claude/rules/07-thin-shell.md(依據)

> leo 2026-08-12:①「如果你沒有裝,就是新的;**如果你已經有,原來叫什麼名字就繼續用下去**」 > ②「**根本就不應該在 CLI,我要的是一個大家都可以用到的規則。**」 「這個實例該用哪些資源」換到安裝器就要重寫一次 ⇒ 依 `.claude/rules/07-thin-shell.md` 的判準口訣, 它是**能力**;而它原本住在 `cli/src/lib/resource-resolver.ts`(431 行)⇒ 那本身就是違規。 後果已經真的發生:`acr` 那條有 `Arcrun#97` 的修法、安裝器那條沒有 ⇒「我按了更新,工作流和登入全不見了」。 CLI 淨 **-496 行**(邏輯是搬走,不是複製)。 --- ## ① 規則放在哪、為什麼(含自舉怎麼解) **`shared/resource-rule/`** —— 零依賴 ESM,Node 18+ 與 Cloudflare Workers runtime 都能直接 import。 | 檔案 | 內容 | |---|---| | `rule.mjs` | 規則本體(`planResources` / `applyResourcePlan` / `parseWranglerRequirements`)+把 CF 回應讀成事實的 `normalizeLiveBindings` / `normalizeLiveVars` | | `cf-resource-api.mjs` | `ResourceApi` 的 CF REST 實作(只用 global `fetch`) | | `installer-entry.mjs` | 安裝器唯一該碰的入口 `resolveInstanceResources()` | ### 為什麼**不是** cypher-executor 的 API 端點(=自舉那一題) 薄殼原則的標準答案是「能力放 API」,這一條刻意不走,理由有三,都不是形式問題: 1. **cypher 可能還不存在。** 這條規則要在「決定怎麼裝」的當下就用得到,而安裝器的工作 正是把 cypher 生出來 —— 放進 cypher 等於要先有雞才能有蛋。 2. **輸入是使用者自己帳號的綁定狀態。** 送去平台託管的 worker 換一個答案 ⇒ (a)「能不能安裝」從此綁在平台是否活著,(b) 使用者的 CF 帳號拓撲交給第三方。兩件都不該做。 3. **它根本不需要是服務。** 這是純函式,唯一的 IO 由呼叫端注入(`ResourceApi`)。 薄殼原則要求的是「**能力只實作一次**」,不是「能力一定要是 HTTP」。 其他評估過的形態:**共用 npm 套件** → 要多發一個 package + token,且安裝器得先 `npm i` 才能判斷, 自舉問題只是換個位置;**做成一顆零件** → 得用 TinyGo/AssemblyScript 重寫一次,那正是本票要消滅的東西。 > ⚠️ 這是自舉例外,不是放寬:能放 API 的仍然放 API,不要拿自舉當藉口把業務邏輯搬回介面層。 ### 順帶:連 CF client 也共用 判斷一致還不夠,**看到的東西**也要一致。「已部署的 worker 綁著什麼」是從 `GET /workers/scripts/{script}/settings` 讀來的;兩條路各自寫一份 client,只要有一邊把 404 當錯誤、漏了 `per_page`、少認一種欄位名(`namespace_id` vs `id`),那一邊就會 「看不到既有綁定」—— 而看不到既有綁定的下一步,依規則就是**新建**。 **#97 不需要規則寫錯,眼睛不一樣就足以重演。** 所以 `CfAccountClient` 的 `ResourceApi` 七個方法全部委派共用 client,自己不留實作。 --- ## ② 「只有一份」怎麼證明 - **安裝器不需要副本**:它本來就會下載本 repo 的 archive 當部署來源 (`rules/05-deploy-convention.md`「WASM 來源」),`shared/resource-rule/` 就在那份 archive 裡,直接 import。 - **`acr` 需要一份鏡射**:`arcrun` 是獨立 npm 套件,`npm pack` 打不進套件目錄外的檔案。 `cli/src/lib/resource-rule/` 是**逐位元組鏡射**,由 `scripts/sync-resource-rule.mjs` 產生 ——同 `cli/harness/`(產生物進 repo + `check:harness` 世代閘)的既有慣例,不是為本票新發明的做法。 `cli/tests/single-implementation.test.ts` 把 leo 要求的那個 grep 寫成了會紅的東西: ``` # ① 掃過 288 個原始碼檔,7 支規則函式的實作 全部只出現在 shared/resource-rule/(+機械鏡射) # ② cf-resource-api.mjs sha256 70d278d7c55b9557 原稿 = 鏡射 # ② installer-entry.mjs sha256 5a397a23e7b01446 原稿 = 鏡射 # ② rule.mjs sha256 38f8c722f54c62ec 原稿 = 鏡射 # ③ cf-resource-api.mjs import: ./rule.mjs # ③ installer-entry.mjs import: ./rule.mjs, ./cf-resource-api.mjs # ③ rule.mjs import: (無) ok 39 - ① 規則的實作全 repo 只有一份(原稿目錄 + 它的鏡射,沒有第三處) ok 40 - ② CLI 帶的那份與原稿逐位元組相同(漂移=第二份實作偷偷長出來) ok 41 - ③ 共用層零外部依賴(只准 import 同目錄的兄弟檔) ``` **閘是真的會擋**——實測手改鏡射一行後跑 `npm test`: ``` > arcrun@1.3.14 check:rule > node ../scripts/sync-resource-rule.mjs --check ✔ cli/src/lib/resource-rule/cf-resource-api.mjs = 原稿(sha256 70d278d7c55b) ✔ cli/src/lib/resource-rule/installer-entry.mjs = 原稿(sha256 5a397a23e7b0) ✗ cli/src/lib/resource-rule/rule.mjs 與原稿不一致(副本 34db99a88535 ≠ 原稿 38f8c722f54c)。 這一份是**產生物**,不要手改:規則要改就改 shared/resource-rule/rule.mjs,然後跑 `node scripts/sync-resource-rule.mjs`。 ❌ 1 項與規則原稿脫節。 ``` `npm run build` 與 `npm test` 都先跑這道閘 ⇒ 漂移擋在 build 與 npm publish 之前。 --- ## ③ 兩條路一致性的 fixture 對照 `shared/resource-rule/tests/fixture-account.mjs` 造一個假帳號,**假的是 `fetch` 不是 `ResourceApi`** ——所以兩條路都真的走完 HTTP → 解析 → 判斷整條鏈(只假 `ResourceApi` 會跳過「怎麼把 CF 回應讀成事實」, 而那正是 #97 能重演的地方)。同一份帳號狀態餵給: - **A `acr` 那條**:`CfAccountClient` + `resource-resolver`(CLI 真正跑的 import 鏈) - **B 安裝器那條**:只 import `shared/resource-rule/installer-entry.mjs` `cli/tests/two-paths-agree.test.ts` 實跑輸出: ``` # ── fresh:沒裝過(全新帳號,一顆 worker 都沒有) # binding acr 選的 安裝器選的 一致? # d1:CREDENTIALS_DB d1id-NEW-1 d1id-NEW-1 ✓ # d1:DB d1id-NEW-1 d1id-NEW-1 ✓ # kv_namespace:ANALYTICS_KV kvid-NEW-4 kvid-NEW-4 ✓ # kv_namespace:CREDENTIALS_KV kvid-NEW-3 kvid-NEW-3 ✓ # kv_namespace:EXEC_CONTEXT kvid-NEW-1 kvid-NEW-1 ✓ # kv_namespace:OAUTH_KV kvid-NEW-9 kvid-NEW-9 ✓ # kv_namespace:RECIPES kvid-NEW-5 kvid-NEW-5 ✓ # kv_namespace:SESSIONS_KV kvid-NEW-7 kvid-NEW-7 ✓ # kv_namespace:SUBMISSIONS_KV kvid-NEW-8 kvid-NEW-8 ✓ # kv_namespace:USERS_KV kvid-NEW-6 kvid-NEW-6 ✓ # kv_namespace:WEBHOOKS kvid-NEW-2 kvid-NEW-2 ✓ # ── installed:裝過了(安裝器命名慣例 arcrun-rag-<instance>-kv-<binding>) # d1:CREDENTIALS_DB d1id-kbdb-REAL d1id-kbdb-REAL ✓ # d1:DB d1id-kbdb-REAL d1id-kbdb-REAL ✓ # kv_namespace:ANALYTICS_KV kvid-analytics_kv-REAL kvid-analytics_kv-REAL ✓ # kv_namespace:CREDENTIALS_KV kvid-credentials_kv-REAL kvid-credentials_kv-REAL ✓ # kv_namespace:EXEC_CONTEXT kvid-exec_context-REAL kvid-exec_context-REAL ✓ # kv_namespace:OAUTH_KV kvid-oauth_kv-REAL kvid-oauth_kv-REAL ✓ # kv_namespace:RECIPES kvid-recipes-REAL kvid-recipes-REAL ✓ # kv_namespace:SESSIONS_KV kvid-sessions_kv-REAL kvid-sessions_kv-REAL ✓ # kv_namespace:SUBMISSIONS_KV kvid-submissions_kv-REAL kvid-submissions_kv-REAL ✓ # kv_namespace:USERS_KV kvid-users_kv-REAL kvid-users_kv-REAL ✓ # kv_namespace:WEBHOOKS kvid-webhooks-REAL kvid-webhooks-REAL ✓ # ── renamed:資源在,但名字與預期完全不同(使用者自己改過/別的安裝器版本取的名) # d1:CREDENTIALS_DB d1id-kbdb-REAL d1id-kbdb-REAL ✓ # d1:DB d1id-kbdb-REAL d1id-kbdb-REAL ✓ # kv_namespace:ANALYTICS_KV kvid-analytics_kv-REAL kvid-analytics_kv-REAL ✓ # kv_namespace:CREDENTIALS_KV kvid-credentials_kv-REAL kvid-credentials_kv-REAL ✓ # kv_namespace:EXEC_CONTEXT kvid-exec_context-REAL kvid-exec_context-REAL ✓ # kv_namespace:OAUTH_KV kvid-oauth_kv-REAL kvid-oauth_kv-REAL ✓ # kv_namespace:RECIPES kvid-recipes-REAL kvid-recipes-REAL ✓ # kv_namespace:SESSIONS_KV kvid-sessions_kv-REAL kvid-sessions_kv-REAL ✓ # kv_namespace:SUBMISSIONS_KV kvid-submissions_kv-REAL kvid-submissions_kv-REAL ✓ # kv_namespace:USERS_KV kvid-users_kv-REAL kvid-users_kv-REAL ✓ # kv_namespace:WEBHOOKS kvid-webhooks-REAL kvid-webhooks-REAL ✓ ok 39 - 兩條路一致 — fresh ok 40 - 兩條路一致 — installed ok 41 - 兩條路一致 — renamed ``` 除了 resource id,還斷言**建了什麼**與**停手的理由**也一樣 (「一邊停手、一邊照做」是最危險的分歧)。 --- ## ④ 三種情境的實測 ``` # ① 新建:KV 9 顆、D1 1 顆(D1 共用:CREDENTIALS_DB = DB = d1id-NEW-1) # ② 沿用:WEBHOOKS → kvid-webhooks-REAL(工作流 3 支還在)|SESSIONS_KV → kvid-sessions_kv-REAL(登入 session 還在)|DB → d1id-kbdb-REAL(子庫 4 個還在)|新建 0 顆 # ③ 名字全不同(例:WEBHOOKS 那顆實際叫 "kv-51mggz") → 仍沿用 kvid-webhooks-REAL,新建 0 顆 ok 42 - 情境① 沒裝過 → 正常建新的(不能為了沿用而變成永遠不建) ok 43 - 情境② 裝過了 → 沿用原本那幾顆,工作流與登入 session 都還在 ok 44 - 情境③ 資源在但名字與預期完全不同 → 仍然沿用(#97 的病根,專門驗) ``` 情境③ 的資源名(`kv-51mggz` 之類)刻意取成跟 binding 名毫無關聯的雜湊字串 ——只要規則裡還有一絲「照名字對號」就會在這裡露餡。 安裝器那條路也能**零依賴、零建置獨立跑**(`node shared/resource-rule/tests/demo.mjs`), 這本身就是「安裝器把 repo archive 拉下來就能直接用」的證據: ``` ── installed(mode=update):裝過了(安裝器命名慣例 arcrun-rag-<instance>-kv-<binding>) d1:CREDENTIALS_DB → d1id-kbdb-REAL adopted d1:DB → d1id-kbdb-REAL adopted kv_namespace:WEBHOOKS → kvid-webhooks-REAL adopted … 本趟新建:KV 0 顆、D1 0 顆、Vectorize 0 顆|沿用既有版本標籤 ARCRUN_BUNDLE_VERSION=1.4.33 ``` (末欄順帶證明 `Arcrun#106` 的 `plain_text` var 沿用也走同一條路。) --- ## 全套驗證 ``` $ cd cli && npx tsc --noEmit # 無輸出 $ cd cli && npm test # tests 58 # pass 58 # fail 0 ``` 58 = 原本 49(全數保留通過,含 `resource-adoption.test.ts` 的 #97 迴歸)+ 本次新增 9。 npm 打包確認產生物真的出得去:`npm pack --dry-run | grep -c resource-rule` → `16`; `dist/lib/resource-rule/*.mjs` 就位,載入後 `ResourceApi` 七個方法齊全。 --- ## ◐ 沒做到的一件事(誠實標) **「在 youlin 上走安裝器那條路更新一次」沒有跑成。** 兩個原因,都不是選擇性略過: 1. **安裝器不在本 repo。** `products/arcrun-rag/installer/oauth-prototype/worker.js` 屬於 `arcrun-rag`,本工作副本裡不存在(`find` 全無命中)。派工單也說明本票不做移交(另票)。 2. **本工作副本取不到任何 CF 憑證**(無 `.env`、沙箱讀不到 `~/.arcrun/`), 連唯讀查 youlin 帳號都做不到。 所以本 PR 交付的是「規則側」:**安裝器需要改的是一行 import**(見 `shared/resource-rule/README.md` §4 的完整片段),而不是抄一份邏輯過去。 接手 `arcrun-rag` 那張票時: ```js import { resolveInstanceResources } from './shared/resource-rule/installer-entry.mjs'; // 刪掉 worker.js:685 建 KV、:705 建 D1 的「照名字冪等」兩段,改呼叫這一支 // r.blocked → 把 r.blockers 原文顯示給使用者,不要自己「試著繼續」 ``` 在 youlin 上做更新前後 resource id 對照,應該是**那張票**的驗收 (依紅線「不確定時寧可中止」,我沒有在拿不到帳號的情況下猜著跑)。 ### 另一件被擋下的 想在 `.claude/rules/07-thin-shell.md` §1 補一段「自舉例外」的正例(讓下一個人不會又把它寫回介面層), 但該檔是受保護檔案、本 session 改不動。內容已完整寫在 `shared/resource-rule/README.md` §2, 要的話可由總管代補。 --- 📍 `Arcrun#97`(CLI 已修,本票是把同一條規則交給所有路徑)|`Arcrun#106`(`liveVars`)| `Arcrun#80`/`arcrun-rag#39`(同一個「重複做 Arcrun 的工作」家族)|`.claude/rules/07-thin-shell.md`(依據)
Leo added 1 commit 2026-08-12 15:39:22 +00:00
leo 2026-08-12:「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。」

「這個實例該用哪些資源」換到安裝器就要重寫一次 ⇒ 依 rules/07-thin-shell.md 的判準
它是**能力**,而它原本住在 cli/src/lib/resource-resolver.ts ⇒ 那本身就是違規。
後果已經真的發生:acr 那條有 Arcrun#97 的修法、安裝器那條沒有,於是安裝器照名字
找、找不到就建一顆空的綁上去 ⇒「我按了更新,工作流和登入全不見了」。

規則搬到 shared/resource-rule/(零依賴 ESM,Node 與 Workers runtime 都直接跑):

  · rule.mjs           規則本體+把 CF 回應讀成事實的 normalizeLive*
  · cf-resource-api.mjs ResourceApi 的 CF REST 實作——**眼睛也共用**:
                        兩條路各自解讀 CF 回應,只要一邊看不到既有綁定就會去新建,
                        #97 不需要規則寫錯就能重演
  · installer-entry.mjs 安裝器唯一該碰的入口 resolveInstanceResources()

不是做成 cypher 端點的理由(自舉):這條規則要在「決定怎麼裝」的當下就用得到,
而那時 cypher 可能還不存在(安裝器的工作正是把它生出來);且輸入是使用者自己帳號的
綁定狀態,不該送去平台換答案。它是純函式,用不著變成服務。

只有一份,機械看守:
  · 安裝器直接 import repo archive 裡的原稿,**不需要副本**
  · acr 因為 npm pack 打不進套件目錄外的檔案,帶一份逐位元組鏡射
    (scripts/sync-resource-rule.mjs 產生;build/test 先跑 --check,差一位元組就紅)
    ——同 cli/harness/ 產生物+世代閘的既有慣例
  · cli/tests/single-implementation.test.ts 掃全 repo:7 支規則函式的實作只有一處

CLI 淨 -496 行(邏輯是搬走,不是複製)。cf-api.ts 的 CfAccountClient 保留公開介面,
ResourceApi 那七個方法全部委派共用 client。

驗證:cli 58/58 綠(含新增的兩條路一致性 fixture + 三種情境),tsc --noEmit 乾淨。
Leo merged commit 13155a1d7b into main 2026-08-12 15:44:50 +00:00
Sign in to join this conversation.