Commit Graph

118 Commits

Author SHA1 Message Date
uncle6me-web 4b6cc159b8 feat(auth): 認證儲存搬回 D1/KV——落實方案C(leo confirm「走C」,arcrun-rag#99)
實作 pending-changes.md「認證儲存要不要搬回 D1/KV」提案(commit 8286c8a):
D61 的病根「重裝時 binding 被安裝器照名字重新指到新建空資源」已被更通用的
shared/resource-rule(Arcrun#97,2026-08-13)解掉,故不再需要繞開 binding
去躲這個病——而繞開的代價正是這次要收的債:Workers Secrets 寫入需要外部
CF_SECRETS_API_TOKEN,這把 token 從安裝那天起就沒被種過,止血版只解掉
「建第一個帳號」這一格,之後的每一次寫入(換密碼/加帳號/改權限)仍卡死。

改動:
- console 管理員帳密:家改回 SESSIONS_KV(binding,console-auth.ts)
- portal 多人帳號:家改回 KBDB(binding,走 base HTTP API,D38 零 SQL,portal.ts)
- D61 認證儲存(CF Workers Secrets)留為舊實例的唯讀回退路徑:讀取零成本、
  零外部憑證需求(只有寫入才要 token);登入成功即 best-effort 自動搬進新家,
  且**這次登入發出的 session 就直接指向新 record_id**(不必等下一次登入)
- D61 的三項「明顯失敗」語意全部保留:auth_store_empty(讀不到不算密碼錯、
  不計入鎖定)、/console/setup 遇既有帳號說清楚密碼沒被採用、/health 與
  /console/auth-status 吐儲存狀態
- 移除止血版的 x-arcrun-install-token 表頭傳遞機制(installToken 參數)——
  帳號寫入從此不需要任何外部 CF token,這個結構性缺口已從根拔除

測試:cypher-executor 全套 vitest 439/453(14 個既存失敗與本改動無關,已用
git stash 對照 clean checkout 逐一比對檔名確認完全相同);tsc --noEmit
無新增錯誤(3 個既存錯誤同上核實無關)。已跑 build-worker-artifacts.mjs
重打 tier2 bundle,grep 複驗 createKbdbUserRecord/promoteToKbdb 進了成品、
promoteLegacyUser/x-arcrun-install-token 完全從成品消失。

未覆蓋:POST /credentials(一般 workflow API 金鑰儲存)仍依賴
CF_SECRETS_API_TOKEN——這是 01-tech-stack.md 既有的、獨立於 D61 之外的
credential 儲存架構(D19「擁有目錄不擁有內容物」),本提案範圍只涵蓋「認證」
(登入帳密),不涵蓋一般 credential 儲存;07-29 已知缺口仍待另案處理。

不准 merge 進 main(SDD 鐵律③,等總管審過再併);不准部署(D20 出貨閘)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:52:07 +08:00
uncle6me-web 8286c8afec docs(pending-changes): 提案——認證儲存搬回 D1/KV(撤 D61 的儲存層決定,留其明顯失敗語意)
arcrun-rag#99 止血修法(安裝精靈傳 OAuth token 給 cypher 建第一個帳號)只解一格,
之後的寫入(換密碼/加使用者/存金鑰)仍永久 writable:false,是留債。

查證(讀碼+讀本機重打的 .worker-builds/manifest.json,非猜測):D61 想解的病根
(重裝時 binding 被照名字重新指到新建空資源)已在 2026-08-13 被更通用的
shared/resource-rule(Arcrun#97)解掉——SESSIONS_KV、KBDB 的 D1 都在 cypher 的
requires 清單裡,且 runInstall:1762 每次安裝/更新都會走這條規則。技術前提成立。

提案:認證資料搬回 D1/KV(走 binding,零外部 CF token、零過期、零新 OAuth scope、
零手動步驟),保留 D61 加的三個明顯失敗機制。附影響分析、遷移考量、信心水位
(靜態推論,未動態實測)、D84 三段式驗法(③ uninstall 目前做不到,如實標記)。

⏸ 等 leo/總管 confirm,本 commit 不動任何架構程式碼。

順手修:writeAuthStore 的錯誤訊息移除誤導的「/ CF_ACCOUNT_ID」(已查證全新實例
一定有這個 var,installer worker.js:1354 無條件注入,列兩項只會讓下一個人白工)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:06:50 +08:00
uncle6me-web ca1ed2aaf6 fix(auth): 錯誤訊息不再叫用戶做做不到的事(arcrun-rag#99,leo 傳實測畫面點破)
leo 傳了封測者實際看到的畫面:撞牆訊息叫他「重新執行安裝/更新讓它就緒」,
但安裝/更新從來不會把 CF_SECRETS_API_TOKEN 種成一個常駐的值——這句話在
訊息會出現的所有情況下都不保證成立(尤其是安裝精靈本身之外的路徑,如事後
新增使用者、改密碼)。使用者照做 → 回到同一畫面 → 以為是自己操作錯誤 →
無限迴圈。這比「訊息不夠白話」嚴重一個級數:不是難懂,是死路。

改:不再承諾一個我們自己都不確定會生效的動作,改成誠實描述現況+給得出去
的下一步(回報支援並附上下情境)。訊息集中在 writeAuthStore 單一出處,
console-auth.ts/portal.ts 的 502 回應共用同一份文字,不會各說各話。

測試:全套 vitest 443/457(14 個既存失敗與本檔無關,已用 git stash 對照
clean checkout 確認);tsc --noEmit 無新增錯誤。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:56:59 +08:00
uncle6me-web 88f308642e fix(auth): 全新安裝補上 CF_SECRETS_API_TOKEN 的缺口——第一個帳號建得起來了(arcrun-rag#99)
根因:D61(認證與資料分離,c4cee35)把 /console/setup、/portal/admin/bootstrap
的寫入全部改走 CF Workers Secrets(env.CF_SECRETS_API_TOKEN),但這把 token
從安裝那天起就沒被種過——07-29 已知缺口(pending-changes.md「credential 走
n8n 模式」),當時只降級某個功能;D61 之後升級成「連第一個帳號都建不起來」
的硬斷點,每台全新安裝必中(leo 本人+封測者實撞:裝得起來但卡在註冊)。

修法:putWorkerSecret/deleteWorkerSecret/authStoreWritable/writeAuthStore/
mutateAuthStore 新增可選的 tokenOverride 參數(呼叫端提供 > worker 自身 env)。
/console/setup、/portal/admin/bootstrap 讀取 x-arcrun-install-token 表頭,
只有安裝精靈(裝機當下手上有一把自己還有效的 OAuth token,workers-scripts.write
scope,同一把已用於 putWorkerSecretDirect/seedCredential)會帶這個表頭;
一般使用者自己在瀏覽器操作不受影響。沿用既有「D36 安裝器代寫」precedent,
不是新開一條路;bootstrap 本身已被「已有 admin → 409」擋成只能成功一次,
不會被拿來反覆濫用。

測試:cypher-executor vitest 443/457(14 個既存失敗與本改動無關,已用
git stash 對照確認);新增 2 則直接證明「缺 token→502 auth_store_not_writable/
帶 token→200」。tsc --noEmit 無新增錯誤。已跑 build-worker-artifacts.mjs
重打 tier2 bundle 供驗證(工作區未 commit 前提下的本地驗證版)。

未完成:安裝器(products/arcrun-rag/installer/oauth-prototype/worker.js)
端的 x-arcrun-install-token 表頭傳遞已另外修好,但兩邊都還沒部署——需要
①重打正式 worker artifact ②install.arcrun.dev 的安裝器 wrangler deploy
③已卡住的封測者要再走一次安裝精靈讓他的 cypher worker 拿到新 bundle。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:46:56 +08:00
uncle6me-web b223a69884 fix(portal): 藏書地圖看得到自己的知識——租戶字串改從「寫入端」來,不再拿環境變數預設值(Arcrun#108)
leo 2026-08-12 實撞:藏書地圖回 0 個庫,同一分鐘 KBDB 裡有 1854 條三元組,
`arcrun_whoami` 顯示 admin/全部知識庫、`kbdb_search` 也查得到——只有地圖那格是空的。

病根(不是資料掉了,是讀寫兩端各拿一個來源):
  寫入端 owner_id = `~/.arcrun/config.yaml` 的 `api_key`(CLI push/小幫手上傳/MCP,
    leo = `bfezv28v`)
  讀取端過濾 = `portalTenant(env) = env.CONSOLE_TENANT || "leo"`
    ——repo toml 帶的**官方 prod 值**,而 `acr` 從來不注入 CONSOLE_TENANT
  ⇒ 那個 `"leo"` 不是理論邊角,是每台 self-hosted 實例的實際行為,1854 條全被濾掉。

與 #105(`env.MCP_OWNER_NAMESPACE || "leo"`)同一句話,換一個檔案。

租戶字串該從哪裡來(本票的核心判斷):
  **從「寫入這批知識的那一方」來,不是從一份手抄的環境變數預設值來。**
  不是「掛到每個帳號上」——portal 帳號共用同一台實例的知識庫(design D-2),
  帳號之間的差別是 libraries 權限不是 owner_id;複製一份到帳號上只是多一個會過期的副本。
  #105 真正的教訓是:過濾用的租戶字串要有單一權威來源、解析不到要誠實失敗、且要能機械驗證。

修法:
1. 唯一產地 `cypher-executor/src/lib/tenant.ts`
   - `knowledgeOwner(env)` → branded `TenantId`:`ARCRUN_NAMESPACE` → `CONSOLE_TENANT` →
     丟 `TenantUnresolvedError`。**沒有字面預設值**——`|| 'leo'` 正是把「這台機器沒設定」
     偽裝成「你沒有資料」的元凶。
   - `accountTenant(env)` → 普通 `string`(帳號子 namespace `{tenant}::portal` 與 cypher
     自己寫的設定用它)。**回 string 是刻意的**:型別上就不可能流進知識資料面。
   - 資料面過濾一律經 `ownerQuery()` / `ownerField()`,只吃 `TenantId`。
2. 值的正解由 CLI 從真相源導出:`acr update` 把 config 的 `api_key` 注入成 `ARCRUN_NAMESPACE`,
   但**先驗再寫**(`GET /kbdb/map?owner_id=<api_key>` 查得到庫才寫;查不到/問不到就一個字
   都不動)。無條件覆蓋會把「知識本來就在 CONSOLE_TENANT 底下」的一鍵安裝實例指向空的那一格
   ——那是 #97/#106 那類「更新一次把人家的東西弄不見」,比原本的 bug 更糟。
   未注入時回退 CONSOLE_TENANT ⇒ 對官方 prod 與未更新的實例,這次改動是惰性的。
3. 空地圖分四態(沿 #100「讀不到就說讀不到」):no_library_grant/filtered_out/
   scope_mismatch/confirmed_empty。scope_mismatch 以前不存在,所以設定錯誤被畫成
   「你沒有資料」。回應仍不含租戶字串(design §3.3 紅線)。
4. 同族一起修(同一道閘一次抓到):console-dashboard 4 處、console-auth 1 處
   ——console 首頁的規模數字與藏書地圖對 leo 也一直是空的。

留下的閘(規則存在但沒機制驗證=會再犯第三次):
  · 型別閘:TenantId 只能由 tenant.ts 產出 → 拿隨手一個 string 去過濾,tsc 當場不給過。
  · 出貨閘:scripts/build-worker-artifacts.mjs 編 tier2 成品前先掃,違規 → 編不出成品。
  · 閘自己可測:規則是純函式(tenant-source-rules.mjs),tests/tenant-gate.test.ts
    逐條驗「5 種壞例子會擋」+「11 種合法寫法零誤攔」;掃描範圍只有 src/,擋不到自己。
  規範寫入 .claude/rules/02-forbidden.md 第六類、system-dev/wiki/mistakes.md #26。

沒動:庫權限過濾(一字未改,回歸測試釘住)、帳號資料落點、任何金鑰、租戶字串仍不下發前端。

驗證:
  cypher   vitest 441 綠 / 14 紅,14 紅與 base commit e05518a 逐字相同(既有)
           tsc 5 個既有錯誤,零新增
  cli      node:test 60/60 綠(含本次新增 12 條);tsc 零錯誤
  閘       壞例子實跑 exit 1;build 實跑「建置中止」;乾淨時實跑通過
  端到端   ◐ 未驗:需部署到 leo21c,那道閘要 leo 親手解(見 PR ③)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 00:15:58 +08:00
uncle6me-web 53b05c6d3d fix(cli): 更新完還看得到版本號——CLI 重部署不再把版本標籤(和你的設定)洗掉
leo 08-12 實撞:更新完 leo21c,Portal 設定頁的「版本」變成
「無法讀取目前版本(知識庫服務可能正在啟動)」。版本號是 leo 唯一的驗收介面,
看不到就等於他無法自己確認任何一次更新有沒有生效。

根因(Arcrun#106):`bundle_version` 來自部署時注入的 plain_text var
`ARCRUN_BUNDLE_VERSION`,而**只有安裝器會注入**。wrangler deploy 是整份覆蓋,
toml 沒寫的 var 直接消失 ⇒ CLI 更新那條路每跑一次就把標籤洗掉一次。
#97 修好了「櫃子」(KV/D1/Vectorize 沿用既有),沒修「櫃子上的標籤」。

修法(兩種 var 走相反的規則,這是本次的判斷):
· 設定類 var = 使用者實例的事實 → **沿用**(讀綁定時同一份回應就帶回來,不多打 API)
  ——把 #97「已部署的 worker 上綁著什麼就是事實」原封不動套用到 plain_text var。
· 版本標籤 = 這份成品的屬性 → **每趟重烙,絕不沿用舊值**。
  沿用舊值會得到一個永遠停在安裝當天的假標籤——比沒有標籤更糟,
  因為它會讓人以為驗收過了。
  版號取部署當下發行頻道公告的 release(Portal/daemon 就是拿它當「最新版」比),
  另外把**真正部署的 commit** 一起烙上去(/health 多吐 `bundle_commit`)→ 漂掉查得出來。
  查不到 release 就誠實退成 `YYYY-MM-DD+<commit7>`,不掰一個 semver 假裝已是最新。

順帶(都是同一條路上的東西):
· ref 先解析成 commit sha 再用 sha 下載 archive——不可變,順手解掉 branch tarball 被快取的老病
· Portal 版本行接受帶 build metadata 的 semver(`1.4.41+d61` 這種先前一律被當成「較舊版本」)
· cli 測試在 node 22 上本來一支都跑不起來(.js→.ts 解析 + parameter property),補上 resolve hook
  ——#97 那份「使用者的東西還在不在」的迴歸守衛也在其中,跑不起來的守衛等於沒有守衛
· types.ts 的 ARCRUN_BUNDLE_VERSION 重複宣告(TS2300)併回一處

驗證見 PR:cli 49/49 綠、cypher health 4/4 綠、Portal 版本行原始碼實跑五種情境、
對真實已部署 worker 的唯讀 dry-run。**未做**:真實實例上的 acr update 端到端
(本機唯一有憑證的帳號是 leo21c=紅線禁碰,youlin 無憑證)。

Refs: Leo/Arcrun#106, #97, #95

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 21:58:43 +08:00
uncle6me-web 10d150ac2b fix(mcp): MCP 用登入者的身分查詢,不再去找一把服務內部金鑰
leo 2026-08-12:「人類進 Portal 輸入帳密表示你是主人,可以查到你權限所有東西;
AI 透過輸入帳密的 MCP 查詢表示是授權的 AI,可以查到主人允許查的任何東西。」
「掛上 MCP 並輸入帳密,那個動作本身就是授權」⇒ 下游不得再要求第二次認證。

病根(不是金鑰沒同步,是身分沒接住):
  oauth/routes.ts 驗完 Portal 帳密只留下 `loginOk = res.ok` 一個布林值,身分當場丟棄,
  namespace 改從 `MCP_OWNER_NAMESPACE || "leo"` 拿。於是查詢時手上沒有身分可帶,
  只好用 KBDB_INTERNAL_TOKEN 直打 KBDB——那條路繞過 portal 所有庫過濾,
  而且不管誰登入都看到同一格、看到全部。CLI 也從不注入 MCP_OWNER_NAMESPACE,
  所以那個 "leo" 預設值是每台實例的實際行為,不是理論上的邊角。

修法(走既有那條路,不發明新的):
1. 接住身分:/authorize 解析 /portal/login 回應,把 portal session token +
   display_name/role/libraries 存進 authorization code → access token。
   /portal/login 補回 session_expires_in,access_token TTL 夾成
   min(自己的 TTL, portal session TTL)——不讓「MCP 還連著、底下 session 早死」。
   cypher 回 200 但沒給 session_token(舊版)→ 不發碼,不簽一張沒有身分的 token。
2. 攜帶身分:kbdb_* 全部改走 cypher `/portal/data/*`,Authorization 帶登入者的
   session。庫過濾/租戶注入/停用即時生效全在 server 側,與人類走 portal 網頁同一道閘。
   kbdb_graph_neighbors 因此不再需要 kbdb_base(server 自己知道查哪個庫)。
   藏書地圖(含連線時注入 instructions 的那份)同樣只回有權限的庫,快取改 per-session
   分格——地圖本身就是情報,不能讓先連上的人把視野留給下一個。
3. fail-closed:舊 token 沒有身分 → 誠實要求重新連線,不偷偷退回服務金鑰那條老路。
   服務級憑據(static token / partner key)維持既有 KBDB 直連,arcrun_* 零回歸。

新增 cypher portal 資料面端點(能力長在 API,MCP 只暴露;rule 07):
  GET  /portal/data/map、/portal/data/map/:library
  GET  /portal/data/templates、POST /portal/data/templates
  GET  /portal/data/records/by-template/:t、GET /portal/data/records/:id
  POST /portal/data/records
全部:呼叫端自帶 owner_id 一律不生效;越權與不存在同回 404;寫入 owner_id 由 server 定死。

KBDB base:`GET /records/:id` 與 by-template 補回 owner_id 欄位——原本不回,
呼叫端無從判斷「這筆是不是我的」,按 id 直讀等於沒有租戶邊界。

沒動:KBDB fail-closed 閘、任何金鑰、租戶字串仍不下發給呼叫端。

驗證:
  mcp        tsc 綠;vitest 113/113 綠(改前 48 綠 29 紅)
  cypher     vitest 400 綠 / 14 紅,14 紅與 base commit a24f291 逐條相同(既有)
  kbdb       vitest 208 綠 / 5 紅,5 紅同為既有(migrations/*.sql 被 gitignore)
  端到端     ◐ 未驗:需部署到 leo21c,那道閘要 leo 親手解(見 PR)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:33:12 +08:00
uncle6me-web f1370e2275 fix(engine): 等待搬回引擎——WASI 沙箱裡沒有「不花 CPU 地等」這種東西(Arcrun#101)
leo 在 youlin stage 實測(只有 input >> wait 兩個節點):
  ms=3000 → 38.9s 後 503(1102) / ms=20000 → 34.0s / ms=30000 → 34.9s / 寫死 3000 → 34.8s
四個值同一種死法、與 ms 無關 ⇒ 病不是「等待很貴」,是「等待從來沒成功過」。

修法:wait 移進 BUILTIN_COMPONENTS,由引擎 await 一個 timer。
只花 wall-clock、不記 CPU ⇒ 等 30 秒與等 3 秒同價(皆 ≈0)。
I/O 契約沿用 component.contract.yaml,既有 workflow 的 wait 節點定義不必改。

🔴 誠實標明:原本註解斷言「Workers 時鐘在同步執行期間凍結,所以自旋永不結束」。
寫測試去證,反而被打臉——workerd 裡自旋 2553 圈後 Date.now() 就前進了。
那條假斷言已刪除(不是改鬆),完整機制降級為推測。修法不依賴它:
純 WASI 沙箱本來就沒有睡覺這個手段,會等的只有宿主。

實測:
  npx vitest run tests/wait-builtin.test.ts  → 12 passed (12)
  npx vitest run(全套)                      → 386 passed / 14 failed
                                              (14 = 動工前的既有紅燈數,未新增)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:15:08 +08:00
uncle6me-web a5e4caf5cb fix(portal): 讀不到就說讀不到——總圖不再把「讀不到」畫成「你沒有」(Arcrun#100)
leo 2026-08-12 打開總圖看到:「0 個實體 · 0 條關聯/知識庫還沒有任何關聯——
上傳文件後 AI 會自動織網」。**而他庫裡有 1854 條三元組。**
那句話會叫他去做一件不需要做的事。

三個獨立的洞疊起來才變成那句謊:
  ① console-dashboard.ts 打 kbdb-graph-plugin 沒帶認證(同段落打 kbdb 的兩支都有帶)
  ② 那顆 worker 不在更新的部署清單裡 ⇒ token 一換它就落單
  ③ **畫面把 null 畫成 0**——後端已經誠實回 null 了,是前端把它變成謊話

為什麼一直沒被發現:graph plugin 原本身上沒有 token ⇒ 門開著 ⇒ 沒帶也進得去。
2026-08-12 輪替後它有了 token,門關上,401 才浮出來。

📍 repo:matrix/arcrun(cypher-executor/src/routes/、console-ui/public/)
📍 票:Leo/Arcrun#100
2026-08-12 13:33:53 +08:00
uncle6me-web 525faaf5d0 fix(cypher-executor): /cypher/search 不再誤報執行期原生零件 not_found(Arcrun#88)
病因:/cypher/search 只查 component registry(SUBMISSIONS_KV,經 submitComponent/
index-only 才有記錄);而 component-loader.ts 能直接解析、從不查 registry 的一整類
零件(trigger_workflow/BUILTIN_COMPONENTS/LOGIC_BINDING_MAP/WASM_HTTP_RUNNER_IDS,
如 if_control/http_request/switch)從未被 submit 過。leo21c 實例實測:
GET /components/catalog 回 404「零件 catalog 不存在」,search 因此對這些零件誠實地
回「兩庫都查過沒有」——但它們其實跑得動(leo 08-11 探測工作流已證)。

修法:從 component-loader.ts 匯出 RUNTIME_NATIVE_COMPONENT_IDS(既有三份執行期
解析白名單的聯集,非新清單),search-nodes.ts 在查 registry 之前先比對,
不受 registry 是否可達/是否已 backfill 影響。真正不存在的零件仍誠實回
not_found/unknown,not_found 的分型建議與相似候選機制不變。

刻意不做:不掃描 registry/components/* 目錄當清單來源——那含已標記待刪的死碼
(km_writer/kbdb_upsert_block),07-30 曾把這類死碼誤灌進 registry;也不投資
SUBMISSIONS_KV 的 backfill 腳本——decisions-summary.md D29 已定調 SUBMISSIONS_KV
併入「KV 退休戰」,不宜再加投資。

測試:cypher-executor/tests/search-nodes-runtime-native.test.ts 7 case 全綠,
複現 registry unreachable/registry 可達但目錄空(leo21c 實例的真實症狀)兩種情境。
全 suite 迴歸:357 pass(較修前 350 pass 多 7 個新測試),既有 14 個失敗與修前
數量、內容完全相同(pre-existing,與本次改動無關)。
2026-08-11 21:51:21 +08:00
uncle6me-web b6ef0f07dc fix(kbdb): 開通 PATCH /kbdb/records/:id proxy + 補三元組 library 補標測試
leo 2026-08-11:貼 wiki 卡「這裏就有 3 個三元組,如果三元組是 0 一定是有錯」——
三元組沒有不見(新式 1,633 筆/舊式 636 筆都還在),問題是地圖看不到:新式三元組
只有 171 筆填了 library slot,其餘因寫入時 template 還沒有該 slot 而被靜默丟棄
(record-crud.ts createRecord 只替「已宣告」的 slot 建值,不是替 caller 傳的
values 建值——順序錯了值就消失,不報錯)。

本次改動:
- cypher-executor/src/routes/kbdb-proxy.ts:補 PATCH /kbdb/records/:recordId。
  基本盤(kbdb/src/routes/records.ts)早有這個端點(mira-dissolve T2.1 的
  updateRecord),但這條 proxy 之前只轉發 GET/POST /kbdb/records,外部(工作流/
  CLI/補標腳本)打不到——能力在,通道沒開,補標三元組只能繞去改表(違 D38)。
  純轉發、無業務邏輯,比照既有 PATCH /kbdb/entries/:id 慣例。
- kbdb/tests/triplet-library-backfill.test.ts:真 node:sqlite 驗三件事——
  ①源頭順序(ensure-slot 必須在 write 之前,事後補救不了已寫的那筆,重現+
  驗證正解)②存量補標(地圖輸出前後對照)③不會重複做(同批跑兩次,第二次
  touch 0 筆)。順手記錄一個相關但不在本次範圍的小落差:liveTripletCountsByLibrary
  的 'general' fallback 桶與 recomputeLibraryMap 的精確比對語意沒對齊。
- cypher-executor/tests/kbdb-records-patch-proxy.test.ts:新端點的租戶閘/
  參數驗證/轉發/404 透傳。

全數綠燈:kbdb 181/181、cypher-executor 新增 8 案全過(既有 14 案失敗為
pre-existing,已用 git stash 比對確認與本次改動無關)。

kbdb-graph-plugin(實際的三元組寫入端 createTriplet/ingestEnvelope)需要同款
修正(library 補進 TRIPLET_SLOTS + 從 source_uri 推導),但該 repo 現行
0 份 active SDD,其 sdd-guard 明確要求人決定要不要開啟——本次不越界代為決定,
留在報告中交回。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:48:31 +08:00
uncle6me-web 797e7f751c fix(portal): 忘記密碼斷點——安裝器從沒注入 PORTAL_MAIL_RELAY_BASE(arcrun-rag#38/#69/#25)
leo 被鎖在自己的系統外面:1.4.35 已出 prod,「忘記密碼」畫面在線上,但按下去回
503「這台實例還沒有設定寄信服務」。根因在 products/arcrun-rag 的安裝器——
`grep -rn PORTAL_MAIL_RELAY installer/` 是零命中,即使 landing 那半(郵差)
D62 已經寫好。這個 repo 這邊改兩件配合修:

- /health 多回 `mail_relay_configured`(布林,不洩漏網址)——安裝器判斷「要不要
  重推」只比 bundle_version,但這個 var 是這次才第一次被注入,跟版本號無關;
  純比版本號的話,已經在最新版的實例(如 leo 那台)永遠不會因為「按更新」而
  重推,這個 var 永遠補不進去。安裝器那邊會讀這個欄位決定要不要強制重推。
- portal.ts 的 503 訊息改白話(不提「實例」)+給一條使用者自己走得完的路
  (重新執行安裝/更新),不再說「請管理員幫你改密碼」——單人使用者的管理員
  就是他自己,那是死路(同源病灶:arcrun-rag#25)。

安裝器那半的修法(PORTAL_MAIL_RELAY_BASE 注入+stale 判斷)在
products/arcrun-rag 同批修(installer/oauth-prototype/worker.js)。

測試:cypher-executor `npx vitest run tests/portal-auth.test.ts tests/health.test.ts`
37/37 綠(35+2,既有測試無回歸;本次未新增測試——改動是訊息文案與健康檢查欄位,
行為已由既有測試覆蓋的路徑保護)。
2026-08-11 12:59:57 +08:00
Claude c76e10d314 test(portal): 補 #66 與 D62 的回歸測試(8 條,35/35 綠)
#66(三條,斷言的是「session 有沒有被刪掉」而不只是狀態碼)
- 傳播空窗(加速器 key 還在)+讀不到 record → 503 auth_store_propagating,**KV 那筆還在**
- 非空窗+讀不到 record → 401 擋下,**KV 那筆仍然還在**(刪是清潔工,清掉的卻是唯一憑據)
- session 內容本身壞掉 → 401 且**該刪**(那是確定的事實,不是暫時讀不到)
  ⇒ 前兩條在舊碼上必紅:舊碼在這兩個情況都會 SESSIONS_KV.delete()

D62(五條)
- 帶 reset_token 呼叫 /portal/password/change:**不必登入、不必現有密碼**;
  且「先刪再回」=同一條連結第二次必定 400 reset_token_invalid,KV 裡也真的沒了
- 先「看一眼」票(GET /portal/password/reset)不會消耗它
- 亂猜 / 格式不對的 token → 400
- 沒 reset_token 又沒登入 → 401(修改密碼那一格仍要身分)
- 沒設代寄服務 → /portal/password/forgot 誠實 503 mail_relay_not_configured(不假裝寄出去了)

擺放順序有註解說明:#66 那組會故意把 per-isolate overlay 灌成空的(模擬空窗),
而 overlay 是模組級全域變數不隨 test 重置,故必須排在檔案最後。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8vF2zS2XpaZjzkC75Fjss
2026-08-10 13:55:53 +00:00
Claude 8d49d883c0 feat(portal): 改密碼與忘記密碼合成同一個機制,用連結不用一次性密碼(D62)
leo 2026-08-10 拍板:「這兩個機制其實是一個機制,可以簡化。」
  修改密碼:輸入**現有的** → 輸入新的 → 覆蓋
  忘記密碼:收到**「修改密碼」連結** → **不輸入現有密碼(忽略)** → 輸入新的 → 覆蓋
⇒ 同一個畫面、同一條寫入路徑,差別只有「現有密碼」那一格。

後端(cypher-executor/src/routes/portal.ts)
- POST /portal/password/change =**那一支**。帶 reset_token 就走連結那一格(忽略 current),
  沒帶就要登入 + 正確的 current。兩條路在 writeNewPassword 之後完全相同。
  /portal/me/password 保留成**別名轉呼同一支**(不留第二份實作,兩份必然漂移)。
- POST /portal/password/forgot(公開)/GET /portal/password/reset(看票,不消耗)
- 連結的安全性(承 D50,不可退讓):**一次有效**(用掉即刪,先刪再回)、
  **會過期**(KV TTL 30 分鐘)、**與註冊辨識碼不同源**(現場 crypto 亂數,
  只活在本實例 SESSIONS_KV,與 landing SIGNUPS 那組安裝辨識碼毫無關係)。
  KV 存的是 token 的 sha256,不是 token 本身。
- 🔴 不做一次性密碼(leo:「不要發一次性密碼太麻煩」)。
- 🔴 已否決不准寫回來的三條(D50):console 密碼救援/重裝重設密碼/直接用固定辨識碼。

中央代寄(arcrun-rag landing 那半在該 repo)
leo 給的職責切法:實例產生連結、管一次性/過期;arcrun.dev 只是郵差。
必須這樣切的硬理由:用戶自己的實例**沒有 send_email binding**,根本寄不了信。
🔴 總管紅線:**絕不把整條 URL 交給郵差**——寄件網域帶 DKIM,肯收「任意 URL+任意 email」
就是一台開放的釣魚中繼,燒的是整個網域信譽、不可逆。
故只交出本實例 origin + 一張回呼票,並新增 POST /portal/password/relay-verify:
郵差**回頭打這個 origin** 問「這張票是你發的嗎」,冒用別人網域會被那台實例自己否認
⇒ 主機屬於呼叫方這件事由郵差親自確認,不是相信宣稱。
信裡的連結落點 GET /portal/password/reset-link(主機刻意=被確認過的那個 origin)。

前端(console-ui/public/portal/index.html)
- 登入頁「忘記密碼」入口,**在 portal 不在 console**(leo:「是對 portal 不是對 console,
  這樣 youlin 雖然忘記,我還是可以去 portal 忘記密碼。」)
- 拆掉登入頁原有的「用管理主控台密碼救援自己」連結與「忘記密碼請聯絡管理員」
  ——兩條都是 D50 已否決的做法(console 與 portal 是同一組帳密的兩個鑰匙孔)。
- #v-reset 殼**沒有自己的密碼欄位**:真正的表單是設定頁那唯一一份 #pw-form,
  進入連結模式時被原封不動搬過去,只切換「現有密碼」那一格顯不顯示。
  同一個表單元素、同一支送出函式 —— D62「同一個畫面」的字面落地。

驗證:tsc 與 baseline 同為 23 個既有錯誤(零新增);27/27 既有測試綠;前端 JS node --check 過。
stage 實測見交付回報。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8vF2zS2XpaZjzkC75Fjss
2026-08-10 13:23:44 +00:00
Claude 417d69ceb3 fix(portal): 傳播空窗期不再銷毀 session——讀不到 ≠ 這個人不存在(arcrun-rag#66)
leo 2026-08-10 在 youlin stage 改完密碼被登出,之後怎麼登都回不去。
病灶是「這一瞬間讀不到」被當成「這個人不存在」,而且做的是**不可逆**的動作。

認證的家是 CF Workers Secret,改它會產生 worker 新版本,既有 isolate 讀到的
還是舊 env(#55 實測 ≥15 秒)。舊的 requirePortalUser 在那個空窗裡直接把
session 從 KV 刪掉——不是擋下讓你重試,是當場銷毀,等 secret 鋪開也回不來。
#55 補的「讀不到就再問一次加速器」只加在登入路徑(findAndVerifyUser),這道門沒有。

三處修法(缺一不可):

1. requirePortalUser 先問一次加速器再判定(與登入路徑同一招、同一支函式)
2. **永不因「讀不到」刪 session**;仍讀不到且正在傳播空窗 → 回 503
   `auth_store_propagating` 而不是 401(讀得到 record 的「已停用」照舊刪,那是確定的事實)
3. 前端 boot() 從「任何非 2xx 都 dropSession」收斂成**只有 401 才算被登出**
   ——後端不刪、前端卻自己丟掉 localStorage 的 token,症狀一模一樣

順帶修掉同一族的一個資料遺失路徑:mutateAuthStore 舊版拿「可能是舊版 env」當底稿做
read-modify-write,而 writeAuthStore 會重切分片並刪掉多出來的舊分片 ⇒ 底稿若是
「某帳號被建立之前」的版本,那個帳號會在這次寫入中被抹掉且無法還原(secret 是唯一真相源)。
改成先問加速器、再把 env 版與 overlay 版取聯集當底稿;刪除仍有效(fn() 在聯集之後才跑)。

驗證:tsc 與 baseline 同為 23 個既有錯誤(零新增);tests/portal-auth.test.ts 27/27 綠。
stage 實測見交付回報。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8vF2zS2XpaZjzkC75Fjss
2026-08-10 13:22:34 +00:00
uncle6me-web c4cee35adb feat(auth): 認證與資料分離——搬動知識資料時登入不再跟著壞掉(D61 / arcrun-rag#55)
leo 2026-08-10 下令:「登入認證資料要分離⋯⋯就算只有我一個人存在單獨的 json 檔也好,
它不能被改資料庫的連結導致無法登入。」「昨天不能登入 portal,今天不能登入 mcp,
這根本就是一個問題。」

病:portal 帳號住 KBDB(owner_id = {CONSOLE_TENANT}::portal),console 管理員帳密住
SESSIONS_KV。兩者都靠 binding 指過去,重裝/遷移一定會被重新指一次 ⇒ 保險箱的鑰匙
放在保險箱裡。2026-08-09 leo 資料一個位元組都沒動,卻被鎖在門外。

修:認證搬到 CF Workers per-script Secrets(掛在 script 上,與 bindings 兩套資源,
重部不會洗掉;journeys/gemini-key-lost-on-reinstall.md 與 installer worker.js:1148 皆有實證)。
- 新增 lib/portal-auth-store.ts:自足的 JSON,讀取零網路呼叫,>4.6KB 自動溢位分片
- portal.ts 的帳號讀寫全部改走它;KBDB 只留為舊實例的回退讀路徑,登入成功順手搬過去
- console-auth.ts 的第二份認證資料同樣搬離 KV
- 不牴觸 D38:KBDB 三張核心表不增不減,本案是把東西搬出去
- 沿用 credentials.ts 既有的 putWorkerSecret/deleteWorkerSecret,不另造第二套寫入路徑(D36)

明顯失敗(把 #10「寧可明顯失敗,不要靜默錯置」套到門鎖上):
- 「這台實例讀不到任何登入資料」回 503 + code=auth_store_empty,且**不計入 5 次鎖定**
  (08-09 leo 就是被系統自己的誤判鎖了 15 分鐘)
- /console/setup 遇既有帳號改說「你剛才輸入的密碼沒有被採用」,不再只說「已設定過」
- /health 與 /console/auth-status 吐 auth_store 狀態(住哪、寫不寫得進去)

stage 實測撞到並修掉的坑:改 secret 會產生 worker 新版本,既有 isolate 讀到的還是舊 env
⇒ 「建好帳號立刻登入」有 15 秒以上 401,還被算進鎖定。加一層短 TTL 的 KV 加速器
(非真相源,只在 secret 查不到/密碼對不上時問一次),換 KV 不影響不變量。

驗收:stage(youlin)把知識資料庫換成另一顆空的 + 換租戶代號 + SESSIONS_KV 換成空的,
三樣一起換之後 portal / MCP /authorize / console 三條登入路徑仍全綠(複跑 3 次)。
對照組(舊版程式碼同樣換庫):登入回「email 或密碼錯誤」,5 次後鎖 15 分鐘。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:13:37 +08:00
uncle6me-web 19c82df05f fix(portal): 管理員忘記密碼自救援出口(arcrun-rag#25)
唯一 admin 忘記 portal 密碼就永久卡死:requirePortalAdmin 系列端點全部要
先有 portal session 才進得去,bootstrap 又只能跑一次——登入頁只會叫他
「聯絡管理員」,而他自己就是管理員,沒有下一步。

新增 POST /portal/admin/recover-password,複用 bootstrap 已在用的 console
owner session 當人閘(與 portal 密碼完全獨立存放的另一組帳密)。畫面入口:
/console → 設定 → 「Portal 帳號密碼救援」;/portal 登入頁加一行連結指過去。

本機真瀏覽器 E2E 驗證(wrangler dev 18787/18788 + 本機靜態伺服,真的走一輪
forgot-password 狀態):first-time setup 建帳號 → 故意打錯密碼確認鎖死
(email 或密碼錯誤)→ 點連結進 /console → 用 console 密碼登入 → 設定頁輸入
portal email → 產生新密碼 BPq2Rs4p7dBWMd6d → 回 /portal 用新密碼登入成功。

cypher-executor 4 個新測試 + 既有 59/60 綠(唯一失敗是既有 pre-existing
/portal HTML 殼 404,與本次無關,git stash 驗證過)。
2026-08-09 15:06:17 +08:00
uncle6me-web 8eb10049b8 P8:節點輸出 KV 寫入只服務 PIPE 讀者——拆掉比 neurons 更短的隱形短板
leo 08-09「不需要換模型,要調整每個 CF 數字搭配」。盤點發現真正最短的板不是
Workers AI neurons(119 檔/日),是 EXEC_CONTEXT KV:每節點(含 FOREACH 每圈)
put 一次、rag 工作流一張卡 15 次,但唯一讀點是 PIPE 邊——rag 系全無 PIPE 邊,
15 次全是白燒 ⇒ KV 1,000/日 ÷ 15 ≈ 66 檔/日,兩條路(免金鑰/Gemini)都被卡。

修法=寫入前檢查「節點有 PIPE 出邊」。PIPE 工作流與 resume 路徑行為不變。
實測(youlin stage):修前同構卡留 6 node key(15 put)→ 修後零 key,
blocks/triplets 照常寫入。單元測試鎖住兩側行為。
另復原 08-08 重部誤拔的 [ai] binding(extract 501→200,KEEP_AI=true)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:26:16 +08:00
uncle6me-web 894d9abeb1 Revert "P8:/portal/daemon/extract 萃取模型 scout → qwen3-30b(真筆記實測:品質不降、額度天花板翻倍)"
This reverts commit aa6b899276.
2026-08-09 00:51:41 +08:00
uncle6me-web 4ca23c256a feat(kbdb): 執行紀錄保留期可設定(P7,leo 08-08 confirm)
背景:08-07 事故修復(60688c3)已把執行紀錄從 KV 搬到 KBDB/D1(entries 表,
API-as-Wall),解掉「稽核資料放在會揮發、被額度打斷的地方」這個結構性錯誤,
也順帶把 Evan 撞到的 1,070 次寫入牆退到 D1 額度層級。但那次修復留了一個誠實
的缺口:MCP list_recent_executions 的說明文字寫著「無固定保留期」——保留期
可設定這件事還沒做。本次補上。

P7 規格(system-dev/docs/3-specs/pending-changes.md「P7」,leo 08-08 confirm):
執行紀錄是稽核資料,預設保留 90 天(3 個月)過期即清;租戶可自訂天數,也可
設「不刪除」(企業稽核,leo:「我願意花很多錢保存,不要刪除」)。

實作(kbdb/src/actions/execution-log.ts,牆內):
- getRetentionDays/setRetentionDays:沿用 execution_log_usage 的 upsert 慣例,
  單一 entries 列/租戶(entry_type='execution_log_retention_config'),零建表。
- cleanupExpiredLogs:分兩段掃——有自訂天數的租戶各自 cutoff;其餘(含無租戶)
  套預設 90 天,排除「不刪除」與已處理過的租戶。每次呼叫界限刪除量
  (CLEANUP_BATCH_LIMIT=500),長期多次呼叫可逐步清完累積量。

路由(kbdb/src/routes/execution-log.ts):GET/PUT /execution-log/retention、
POST /execution-log/cleanup,沿用既有的 Bearer token 全域守衛(fail-closed)。

清理觸發(cypher-executor/src/scheduled.ts):不新增排程基礎設施(wrangler.toml
[triggers] 是受保護檔案)——搭 cypher-executor 既有的每分鐘 cron tick 便車,
固定 UTC 02:30 那一分鐘 fire-and-forget 打一次 KBDB 的 cleanup 端點,一天一次,
不是輪詢。

Portal 薄殼(cypher-executor/src/routes/portal.ts):GET/PUT
/portal/admin/execution-log-retention(role=admin 閘),讓本實例的租戶
(portalTenant)能實際設定保留天數,不只是 KBDB 內部端點。

測試(kbdb/tests/execution-log.test.ts):新增 27 個測試(含原有測試共 27 通過
於本檔),真 SQLite 驗證 cutoff 邏輯、自訂天數隔離、「不刪除」永不清、壞資料
容錯、混合租戶情境、路由層 400/200。測試治具需要「插入指定 created_at 的過期
紀錄」這個正式寫入路徑刻意不開放的能力,做成 kbdb/src/actions/execution-log.ts
內匯出的 testInsert*/testCount* 函式(牆內執行 SQL),測試檔本身零原生 SQL。

量測(不是推論):youlin(yuga3bse)實例上,redeploy 後對 graph_neighbors
webhook 發送 1,200 次併發請求(超過 Evan 實測失敗的 1,070 次)——全部 HTTP 200;
ANALYTICS_KV 的 key 數量在請求前後維持 663 不變,證明新寫入路徑完全不碰 KV,
Evan 撞到的那道牆的成因已被物理移除,不只是延後。

讀取端驗證(真呼叫,非 curl):透過綁定 yuga3bse 的 MCP 連線實際呼叫
arcrun_list_recent_executions(回傳含本次量測寫入的 D1 紀錄)與
arcrun_get_execution_trace(正確回 404 not_found,非崩潰);portal 前端
(https://arcrun-rag-ui.youlin-hsieh-dev.workers.dev/portal)瀏覽器實際載入,
無 console 錯誤、無異常紅色橫幅。

舊資料:KV 裡既有的 stats:* 沿用 60688c3 的既有決定——不搬移,任其依現有 90
天 TTL 自然過期(那是統計快取不是真相源);新的 D1 execution_log 保留政策只
管新資料,不回溯處理。

部署:cypher-executor + kbdb 已手動部署到 youlin(yuga3bse,AI 測試場,leo
08-08 令),未動 prod(uncle6)。本次僅程式碼行為變更、無新增/修改 D1 binding、
無新表——三個既有 D1 binding(CREDENTIALS_DB×2+kbdb DB)維持原樣,未新增第四個。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 00:43:55 +08:00
uncle6me-web aa6b899276 P8:/portal/daemon/extract 萃取模型 scout → qwen3-30b(真筆記實測:品質不降、額度天花板翻倍)
免金鑰路短板=Workers AI 免費 10,000 neurons/日。leo 真實筆記 8 篇 × 5 模型實測
(arcrun-rag docs/benchmarks/p8-extractor-quality/,usage.neurons 為 CF 原生計量):
scout 84 n/檔(119 檔/日)→ qwen3-30b 43 n/檔(232 檔/日);格式合規 8/8、
三元組 7.6 條全可解析(scout 5.0)。granite 最便宜但 5/8 缺段=實測否決。
聊天 recipe(workers_ai_chat)不動;只 commit 本 hunk,工作區另有 P7 進行中改動未收。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 00:40:52 +08:00
uncle6me-web 07cc7f51b5 fix(cypher): 同一台實例的 portal 一律自動放行,不再依賴 UI_ORIGINS 被注入
2026-08-08 事故根因:leo 的 youlin 實例登入整個斷掉,瀏覽器實證
  blocked by CORS policy: No 'Access-Control-Allow-Origin' header
真因=該台 UI_ORIGINS 沒被設。

同一天發生兩次同款:這些變數只有安裝器會注入,任何手動 wrangler deploy
就會漏掉,而漏掉時系統看起來完全正常(worker 上線、200、版本號對),
只有真人點下去才會發現。leo:「這麼危險的問題已經發生 2 次,不可以再有一次。」

⇒ 治法不是「記得注入」,是讓它不需要被注入:portal 與 cypher 是同一個
workers.dev 子網域下的兄弟,位址推導得出來。少一個必須注入的變數,
就少一個會被漏掉的東西。UI_ORIGINS 仍有效(自訂網域用),只是不再是
「登得進去」的前提。

對照:改動前後 tsc 錯誤數同為 7(皆為既有、不在本檔)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:47:32 +08:00
uncle6me-web 93b1140bf1 feat(portal): 新增 GET /portal/daemon/diagnostics(t213 matrix/arcrun 半部)
InkStoneCo 總管交辦(arcrun-rag repo t213):leo 拿真診斷檔實測四個真實問題,只答得出一題
——其餘三題需要地端資料(daemon 本機 manifest 總量/失敗分類/自我更新狀態),而現有
GET /portal/data/diagnostics 只有 portal session 認證,daemon 背景行程沒有 session
(密碼只在連線精靈當下用過就丟,不落地),構不到這支端點。

本次只做 matrix/arcrun 半部(雲端這半):
- 把 /portal/data/diagnostics 的核心查詢邏輯抽成 buildDiagnostics(env, tenant)
  (portal.ts),薄殼原則:能力只實作一次
- 新增 GET /portal/daemon/diagnostics,認證比照既有 /portal/daemon/extract
  (X-Arcrun-API-Key,非 session);apiKey 當 owner_id 用,不與 portalTenant(env)
  比對(t189 教訓:daemon 的 api_key 不保證等於 worker 的 CONSOLE_TENANT)
- /portal/data/diagnostics 改呼叫共用函式,回應形狀完全不變
- 按 leo 指示刪掉「需在失敗當下由封測者截圖」那句 notes(本機那半資料到位後這句話
  失去意義)

地端那半(arcrun-app 讀 manifest 合併 + 匯出按鈕)在 products/arcrun-rag repo 進行,
待另一隻處理 t210 的 agent 落地 app.go/main.js 改動後再接線,本次不動 arcrun-app。

驗證:
- npx tsc --noEmit:與 stash 前錯誤數相同(4 個),零新增(pre-existing,與本次改動無關)
- npx vitest run:314→315 通過(+3 新測試涵蓋 401/apiKey 當 owner_id 不比對 tenant/
  回應形狀與 session 版一致),14 個既有失敗與改動前完全相同(HTML 殼 404,測試環境
  static asset 問題,與 portal.ts/portal-data.ts 邏輯無關)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 12:32:10 +08:00
uncle6me-web 7dbd4f59e7 fix(portal): 檢修孔 library_count/triplet_count 恆 0 的根因+加統計自我檢查
真因:/portal/data/diagnostics 讀 GET /map,那是 library_map 快取 block,只能
靠 POST /map/recompute 產生;核實過整個 repo 沒有任何呼叫點會打 /map/recompute
(library-map SDD 的 ingest 自動重算 M3 從沒接上,status: draft)。⇒ /map 對任何
租戶恆回空 libraries,與實際資料量無關——2026-08-07 leo 實測抓到:3 庫、大量
triplets,診斷檔卻回 0/0。

修法:改走 GET /portal/admin/libraries 已在用、驗證過的即時查詢組合(不依賴任何
快取):listRecordsByTemplate(portal_library) + /entries/libraries(t52 蓋章即
現身)+ /records/triplet-stats(t142 即時聚合 SQL)。

附帶:加 library_scope_check 統計自我檢查(呼應 embedding.self_test 的精神)。
兩個計數都是 0 時,用完全不同的查詢路徑(不分庫/模板,只問這個租戶底下有沒有
任何 entries)交叉驗證,區分「真的是空」與「查詢方式或 owner_id 對不上」
(2026-08-01 t161 前科同型病:手動補的 record owner_id 存成 None,全量查得到、
按 owner_id 過濾的畫面永遠空)。

三個 diagnostics 測試全綠:即時查對出正確 library_count/triplet_count(且不再
外流庫名/內容,只回數字)、真空情境自我探測誠實回空、t161 同型病情境自我探測抓到
「查得到但統計回 0」的矛盾。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:12:54 +08:00
uncle6me-web 046ceba29c fix(kbdb): credential 目錄自癒搬遷(D38 收尾)——修 youlin 20/20 全敗事故
根因(2026-08-07 youlin 測試實例):7ba7855 把 credential 讀寫端從舊表
credentials(0002)改走 KBDB entries(entry_type='credential'),但舊表
資料的搬遷 migration(0006)要人手動觸發部署才會跑。實查 youlin 的 D1:
credentials 表有 1 筆(yuga3bse/kbdb_internal_token),entries 對應筆數
為 0——新讀取端上線、舊資料還沒搬,20 次 workflow 全部找不到 credential。

leo 追加硬要求:credential 資料住在用戶自己的 CF 帳號,換讀取路徑=每個
既有實例都要遷移,但用戶不准做任何手動步驟——搬遷必須內建在既有更新流程
裡、天然無感。

解法(kbdb/src/actions/credential-legacy-migration.ts):把「搬」變成
「讀」的副作用而非獨立步驟。KBDB worker(D38 唯一允許碰 SQL 的牆內)在
每次查詢某租戶的 credential 目錄前,先確認舊表資料是否已搬進 entries
——沒有就搬(per-owner scoped、NOT EXISTS 冪等),有就是零成本的
sqlite_master 短路檢查。呼叫時機掛在 GET /entries?entry_type=credential
(cypher-executor 熱路徑本來就會打的端點),故只要更新 KBDB worker,
下一次任何人跑 workflow 該租戶就自動搬好,不需要用戶或安裝器多做任何事。
刻意不執行退場(DROP TABLE)——多個實例搬遷時間點不同,舊表留著才能讓
「已搬」與「還沒搬」的實例同時安全運作;退場留給之後獨立的清理步驟。

kbdb/tests/credential-legacy-migration.test.ts:反向驗證重建 2026-08-07
事故的確切前置狀態(真 SQLite + 0001/0002/0005 migration 原檔),證明補丁
加入前 entries.length 回 0(事故重現),加入後回 1(修好);另驗冪等
(連呼叫三次不重複搬)、多租戶互不干擾、舊表已清理時的終態安全。

cypher-executor/tests/credentials.test.ts:補齊 7ba7855 留下的刻意紅燈
(原 placeholder 五項清單),涵蓋租戶隔離的讀寫、真刪除(非 deprecated
標記)、零原生 SQL 原始碼掃描、密文本體不落 KBDB。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:51:31 +08:00
uncle6me-web 83aa1f6bb2 feat(portal): GET /portal/data/diagnostics —— 檢修孔聚合端點
leo 2026-08-07 直接指令:「一顆按鈕在設定裡,按鈕下載一個檔案,把檔案發給我,你看那個
檔」。本端點是那個檔的資料來源:聚合 embed 模組健康狀態(module_enabled/cards_embedded/
cards_pending/self_test)、知識庫規模(library_count/triplet_count)、bundle_version、
instance_url。

紅線落實:
- 只轉發數字/布林/字串狀態,KBDB /map 回應裡的 narrative/top_entities(卡片內容)讀出
  triplet_count 後即丟棄,測試 portal-data.test.ts 新增案專門斷言回應不含內容字樣。
- 認證沿用既有 requirePortalUser session 閘,不對外公開。

3 個新測試全綠(未登入 401/完整聚合含隱私斷言/embed 未開時誠實回 false 不假裝)。
既有 1 個失敗案(/portal HTML 殼 404)為 stash 驗證過的既有失敗,與本次改動無關。
2026-08-07 18:38:03 +08:00
uncle6me-web 7ba78552a4 D38:credential 目錄改走 KBDB API(零原生 SQL)+舊表退場;測試刻意留紅燈
存取層:credentials.ts / auth-dispatcher.ts / portal.ts 全改走 kbdbBase()+fetch
到 /entries(照 execution-logger.ts 既有慣例),.prepare/.exec/.batch 命中 0。
資料層:0005 seed credential template;0006 把舊表資料搬進 entries 後拆表;
0002 標退役、deploy.ts 不再套用(加 kbdb-sql-ok 留痕,純歷史對照)。

總管親驗四項(不聽 agent 自評):
1 三個檔 .prepare/.exec/.batch 命中 0;六個檔全部通過 kbdb-api-wall-guard
2 0006 的 INSERT 欄位(id/entry_type/owner_id/page_name/metadata_json/
  created_at/updated_at)與 0001_base 的 entries 表逐一對得上
3 不可逆風險查官方:D1 batch 是 transaction、任一句失敗整批 rollback;
  exec 出錯則「執行停止、後續不執行」=> 兩種語意下 INSERT 失敗都不會跑到
  DROP TABLE,用戶 credential 目錄不會遺失
4 0006 搬在拆之前、冪等(NOT EXISTS 防重複)、豁免標記有留痕且理由正當
  (拆表是牆內施工,API 不提供也不該提供拆表)

🔴 抓到一個假綠並修正:agent 中途被中斷,把 111 行的 credentials.test.ts
砍成一行「// placeholder — see edit below」,那個 edit 從來沒發生,
且 setup.ts 被刪。vitest 對這種檔案回報「Tests: no tests」,
很容易被讀成「沒失敗=通過」——正是 CP 記過的
「這條 route 曾整條消失過沒人發現」同型。
處置:還原 setup.ts/vitest.config.ts,credentials.test.ts 改成
**刻意會失敗的紅燈**並在檔頭列出要補的五項。空檔會被誤認為綠,紅燈不會。

未部署。本批要先上 stage 驗過才進 prod(leo 08-07 定)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:27:18 +08:00
uncle6me-web 60688c3108 fix(kv-quota): workflow 執行紀錄搬離 KV,改走 KBDB template 機制(A1/A2/A7)
事故:cypher-executor/src/actions/execution-logger.ts 舊版每跑完一次 workflow 就
ANALYTICS_KV.put() 一筆新 key(註解寫「避免覆蓋」)= 只增不減,封測者 Evan 處理約 690 個
檔案就把 KV 免費層 1,000 write/日打爆(實測 1,070 write),整個實例 429。

A1 少記:workflow 執行紀錄改走 KBDB template 機制(entries 表 entry_type='execution_log',
kbdb/migrations/0004_execution_log_template.sql 只 seed 一列 template 定義,零建表/改表)。
儲存精神比照既有 recipe_stat(kbdb/src/actions/recipe-stat.ts):template 只負責文件化,
實際一筆執行是 entries 表一列(1 次執行=1 次 D1 寫入,不走 entry_values 全展開)。欄位收斂:
時間/workflow/verdict/duration/錯誤訊息/(可得的)目標;成功記最少,失敗多記(訊息截斷長度
不對稱:200 vs 2000 字)。target 只認 trigger context 的 page_name/path,不整包存 input。

A2 自我降級:D1 額度仍與知識卡共用同一顆 100,000 rows/日,本模組自設 20% 軟上限(可用
EXECUTION_LOG_DAILY_WRITE_LIMIT 覆寫),超過 80% 降成只記失敗、超過 100% 完全停止記錄,
但 workflow 執行永遠照跑(cypher-executor 端 fire-and-forget 永不 throw)。

A7 讀取端:/workflows/:name/executions、/portal/data/workflows 的 last_execution、MCP
list_recent_executions 全部改打 KBDB HTTP API(GET /execution-log、/execution-log/latest),
取代原本的 ANALYTICS_KV list/get(免費層 list 也是 1,000/日)。

架構鐵律修正(本次施工中兩度被抓到走偏,過程留痕於 commit 訊息供後續參考):
- KBDB 三張表打天下(entries/templates/entry_values),永遠不加新 table——新資料類型
  一律用 template + entries,不建表、不 ALTER TABLE。
- KBDB = API-as-Wall,零 SQL:cypher-executor 端一律走 KBDB 的 HTTP API(連法比照既有
  recordRecipeStats/kbdbFetch 慣例),不直連任何 D1、不對 arcrun-kbdb 下任何原生 SQL。

順帶修復:kbdb/src/actions/entry-crud.ts listEntries 的 ORDER BY 補 `, rowid DESC` 二級
排序——entries.created_at 是 unixepoch() 秒級解析度,高頻寫入(execution_log 一秒內多筆)
常同秒,單靠 created_at DESC 不保證「最新一筆」正確,此為本次測試(latestExecutionLog)
發現的既有潛在缺陷,順手補上決定性排序,不改變任何既有查詢在 created_at 不同時的行為。

隔離:portal-data.ts INTERNAL_ENTRY_TYPES 加入 execution_log/execution_log_usage(與既有
value/workflow 同層級排除),避免用戶知識搜尋混進執行 log;本模組從不設 metadata_json.embed,
故永不進 Vectorize 語意搜尋索引。

不動:registry/src/actions/recordAnalytics.ts(零件市場統計,獨立 Worker、獨立 KV 命名空間、
不同資料模型,非本次事故根因所指範圍);cypher-executor/{wrangler.toml,kbdb/wrangler.toml}
未變動(repo 層級 deny 規則保護這兩個生產設定檔不被 AI 編輯)——ANALYTICS_KV binding
因此仍留在 wrangler.toml 宣告中但程式碼零讀寫點(見 PR 說明的完整 grep 佐證)。

KV 裡既有的 stats:* 舊資料不搬移(是統計不是真相源,維持原樣任其依 90 天 TTL 自然過期)。

測試:kbdb/tests/execution-log.test.ts(13 個,含零建表證明/少記/A2 降級/route)、
cypher-executor/tests/execution-logger.test.ts(payload 正確性/永不 throw)、
cypher-executor/tests/executions-route.test.ts(讀取端轉發)、portal-data.test.ts 對應區塊
改寫。kbdb 全測試 104/104 通過;cypher-executor 320 個測試中 9 個失敗為 main 既有(與本次
改動無關,改動前後 stash 對照確認)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:13:00 +08:00
uncle6me-web 0ff369f818 修語意搜尋全 0 命中:分數門檻沒跟著 bge-m3 換代(換模型漏掉的第五處)
leo 2026-08-05 實撞:「新上傳的『loop-engine-north-star.md』語義 0 命中,
但舊的『人力媒合系統規劃書』語義 2 命中」。

## 根因不在向量——向量是好的
直打 Vectorize 實測(youlin 實例、bge-m3、1024 維 index):
  搜「閉環機」→ loop-engine-north-star.md 穩坐第 1-4 名(0.638/0.603/0.588/0.552)
D1 與 Vectorize 也對得上:659 筆 embeddable 全 is_embedded=1、index vectorCount 659。

真兇是 **min_score 門檻綁在舊模型的分數尺度上**:
  舊 bge-base-en-v1.5:中文分數全擠 0.65-0.90(沒區辨力)⇒ t183 取 0.75 砍雜訊,對
  新 bge-m3          :尺度整體下移(相關 0.5-0.85、雜訊 0.4 上下)⇒ 0.75 砍掉的是正解
leo 看到的「舊檔中、新檔不中」由此而來——「人力媒合系統規劃書」拿 0.842 僥倖存活,
其餘全被門檻掃掉。08-05 換 bge-m3 的「四處同步」清單(embed.ts/deploy.ts/
deploy-all.mjs/worker.js)**漏了這第五處**,因為它不在 kbdb 而在 portal 呼叫端。

## 改動
· kbdb/src/embed.ts:新增 DEFAULT_MIN_SCORE=0.5,**緊鄰 DEFAULT_EMBED_MODEL**
    ——門檻是模型的性質,放模型旁邊,下次換模型的人一定會看到
· cypher-executor/src/routes/portal-data.ts:拿掉硬寫的 0.75,
    只在使用者顯式指定時才傳 min_score(不再各自持有一份數字=不再漂移)
· console-ui/public/portal/os-split.test.mjs:修好被今天 fe0ee82 弄壞的自測
    (結尾標記寫死文案 ⇒ 改文案就炸「抽不到函式區塊」;改成錨定結構)
    +斷言同步改成 Mac 給 DMG

## 0.5 怎麼來的(實測分布,不是猜的)
「閉環機」            0.638/0.603/0.588/0.552 全是目標檔 ── 斷崖 ── 0.446 才是雜訊
「火星座標 奧林帕斯山」 0.750…0.500 全對,0.475 以下才是雜訊
「人力媒合系統規劃書」   0.842 對,0.550 起是雜訊
誠實 trade-off:0.5 非每個查詢都乾淨(「AI 上課名冊」0.658 的 ax-academy 會擠進來),
但「偶有雜訊」遠優於現況「什麼都搜不到」。

## 驗
· kbdb 87/87 綠(三筆斷言隨新契約更新:預設不再是「不過濾」)
· cypher-executor 9 failed/301 passed=**與改動前逐數相同**(git stash 前後各跑一次)⇒ 既有債
· portal os-split 自測 10/10 綠

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:00:43 +08:00
uncle6me-web 6c5706ba7d 補上合併漏收的修改檔(t189/t181/CIS portal 本體)
上一筆 5d78806 我只 add 了新增檔(favicon 三件套),四個修改檔沒收進去
⇒ 合併內容只推了一半,t189 的 tenant 比對修復其實還躺在工作區。

本次補上(實測與已刪分支 HEAD 23f2fd4 逐檔相同):
· cypher-executor/src/routes/portal.ts      +81(t181 /portal/daemon/extract、t189 tenant)
· cypher-executor/src/routes/portal-data.ts +21
· console-ui/public/portal/index.html      +169(CIS 視覺、版本卡)
· cypher-executor/tests/portal-admin.test.ts +45

教訓:merge --no-commit 後要 git add -A,不能只 add status 裡看到的第一類。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:31:57 +08:00
uncle6me-web 66f1b59f7f t176:雲端不再管地端 LLM 設定(leo 08-03 緊急)
leo 回報四件事:①雲端 Gemini 無法用 ②搞不清楚設定 AI 是設雲端還是地端
③地端不會萃、每個人都死掉因為都去抓 Claude ④雲端還要設置明明就有 Workers AI。
目標:「雲端裝好就能用,地端輸一個 Gemini API Key,然後就開始用。」

根因:extractor_config 的 KV key 由 portalTenant() 組出,而 portalTenant 是
**worker 層級**環境變數(portal.ts:43)⇒ **全租戶共用一把**。任一處設了 claude,
所有人的 daemon 都收到 claude;沒裝 Claude Code 的機器萃取全滅,而 portal 的
Claude 勾選框又恆 disabled(daemon 從未實作 report-capabilities ⇒ daemon_caps 永遠空)
⇒ 用戶自己解不開(awindhon 08-03 實證:雲端同步成功、金鑰有效、零張卡)。

本次(雲端側):
- POST /portal/daemon/config **不再下發** extractor/gemini_api_key/llm_model,
  只回連線欄位。⚠️ route 本身與 daemon/libraries 資料夾管理完全不動(leo 明確劃界:
  「雲端拉地端檔案夾部分不要刪,刪掉從雲端設置地端 LLM 選項部分」)。
- 移除 POST|GET /portal/admin/extractor(t122)——這是「雲端指定地端引擎」的入口,
  且無任何伺服器端驗證,打一下就能把全租戶設成 claude。
- 移除 POST /portal/daemon/report-capabilities(t131)——daemon 端從未實作該呼叫。
- 移除 t131 那組重複註冊的 /portal/admin/ai。**它是重複 route**:後段(arcrun-rag#10)
  另有同路徑一組,Hono 先到先比 ⇒ 舊的一直贏,後段修好的「金鑰真的寫進 credentials」
  形同死碼。保留後段那組(只管 Gemini 金鑰、走 storeCredential),並拿掉 Claude 偏好欄。
- portal 設定頁「AI 設定」整塊移除,改成一句話說明:雲端不需設定(Workers AI 免金鑰),
  地端請在同步小幫手的「AI 設定…」填 Gemini Key。順手清掉 main 上既有的 conflict 標記。
- 清掉隨之孤兒化的 helper(ExtractorConfig/AiConfig/DaemonCapabilities/
  syncExtractorFromAiConfig/readAiPref 等)。

測試:**相對 merge 前 main 基準線,新增失敗 = 0**(基準線本就 9 紅:console 藏書地圖 6/
portal HTML 殼 2/POST execute 1,皆與本次無關)。t122/t131 兩組測試改寫成
**回歸守衛**(斷言那些端點/欄位確實 404、確實不存在),不是刪掉充綠。
順手修 health bundle_version 測試與實作對齊(實作刻意省略該欄,測試卻期待空字串)。

未送達:本 commit 只到 code,尚未部署上線。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 22:11:45 +08:00
Leo 47c6aaea03 feat(t152): workers_ai_chat 種子(auth: binding,免金鑰)+ 修 /init/seed 吃掉 3.12 欄位+ 修 D1 LIKE 長查詢 500
SDD: workflow-discovery 3.12/3.13(不是新規格;3.12 已 confirmed 並實作完成)

## 1) workers_ai_chat 種子(新)
Cloudflare Workers AI 走 env.AI binding ⇒ 用戶不必填任何 API 金鑰就能問答。
放種子表而非產品安裝器:「裝好後預設有哪些 recipe」是平台能力(rule 07 薄殼原則)。
換模型/換供應商=改這一筆 recipe,workflow 不動。

選型實測(1.4.4 實例,真實長度 RAG prompt,每個模型連跑 2 次):
  llama-4-scout-17b        2373/2173 ms   答案最完整、引用正確 ← 選它
  llama-3.3-70b-fp8-fast   3261/2147 ms   可用但波動較大
  mistral-small-3.1-24b    3560/3631 ms
  qwen2.5-coder-32b        3572/3353 ms
  gpt-oss-120b             1971/2295 ms   回應形狀不同,response 取不到文字
  gemma-3-12b-it            5018 帳號無權限
對照舊路徑 Gemini gemma-4-31b-it:同型提問 16.87 s,且吐整段英文思考草稿。

## 2) 修 /init/seed 靜默吃掉 3.12 欄位
3.12 給 RecipeDefinition 加了 body_template/response_map/auth/binding_name,
但 /init/seed 是**列舉欄位重建** recipe record ⇒ 不在名單上的欄位被丟掉。
最惡劣的地方是「哪裡都不會紅」:recipe 查得到、endpoint 對,只有跑起來像沒設定過。
與 08-02 syncManifest 吃掉 manifest.daemon 欄同型(教訓:東西還在不在也要進機械閘)。
加 tests/init-seed-recipe-fields.test.ts:拿掉修復會紅、補回會綠(已實測會擋)。

## 3) 修 D1 LIKE pattern 50 bytes 上限造成的 500
/entries/search?q=… 只要 q 超過 48 bytes 就回 HTTP 500,沒有錯誤訊息。
逐 byte 二分:48→200/49→500;中文 16 字→200/17 字→500。
判別實驗:q 固定 48 bytes、其他 filter 全塞滿讓 SQL 變很長 → 仍 200
⇒ 爆的是 LIKE 的 pattern('%'+q+'%' = 50),不是 statement 長度。
中文問句超過 16 字是常態,而 rag_chat 用整句問題當 q ⇒ 聊天對正常問句等於不能用。
(=InkStoneCo status.md 待辦第 1 條「KBDB keyword 長查詢會炸」的根因。)
修法:q ≤ 48 bytes 走原路(行為逐字不變),超過才拆詞/切 UTF-8 邊界片段。
kbdb 全套 83 測全綠(含新增 8 項)。

## 4) 順手
- 移除被 commit 進 repo 的 node_modules 壞 symlink(指向 leo Mac 的絕對路徑,
  害任何 fresh clone 裝不起來、切分支還會把裝好的蓋掉——本次撞了兩次)。
- pending-changes.md 加 P2 提案(fan-out 並行執行)+等裁決,未動引擎。

驗證:cypher-executor 新增測試 17/17 綠;tsc 與基線逐字相同;
全套測試失敗集合與基線**逐字相同**(基線 14 個失敗,本分支 t173 既有,非本次引入)。
2026-08-03 02:56:53 +00:00
Leo 5b983c47b8 Merge branch 'main' into fix/merge-main-into-batch-t173
# Conflicts:
#	console-ui/public/portal/index.html
#	cypher-executor/src/routes/health.ts
#	cypher-executor/src/routes/portal.ts
#	registry/components/kbdb_upsert_block/component.contract.yaml
#	registry/examples/km-wiki-ingest/workflow.yaml
2026-08-02 23:43:16 +08:00
uncle6me-web 36cdc8fba6 #10 補測試守衛+console 兩頁同族 fallback 一併拔
① tests/portal-admin-ai.test.ts(6 項綠)=**route 消失就會紅**的迴歸防線。
   這條 route 以前不存在、前端卻一直在打它 ⇒ key 從來沒存進去(藍字=假綠),
   所以最該守的就是「它還在不在」。覆蓋:未登入 401(**不是 404**=route 真的在)/
   非 admin 403/GET 回 has_key 布林且回應不含任何金鑰欄位(D36)/
   空 body 400 不假裝成功/只改偏好不誤報 has_key/同測試內寫→讀一致。
   註:vitest-pool-workers 預設 isolatedStorage=true ⇒ 跨 it 的 KV 會還原,
   驗來回一致必須在同一個 it 內完成(我第一版寫成跨 it,測試如實抓出來了)。

② console/index.html 與 console/dashboard/index.html 也有同一個寫死
   `|| "https://cypher.arcrun.dev"` fallback(與 portal 同族)⇒ 一併拔掉。
   靜態 config.js 保留無妨:build-ui-bundle.mjs 的 SKIP 會跳過它、改由 worker 動態產生。

驗證:cypher tsc 綠、全套 248 passed(9 既有失敗未變);
UI 產物 grep 寫死 fallback 歸 0;stage verify.sh 仍 11/3(無迴歸)。
2026-07-31 20:57:05 +08:00
uncle6me-web 0d49989c19 修 arcrun-rag#10:補 /portal/admin/ai(key 從來沒存進去的真因)+UI 已輸入態
真因(不是重裝洗掉,是從來沒存進去):前端唯一寫入路徑 POST /portal/admin/ai
**後端從來沒有這條 route** ⇒ 用戶填 key → 404 → 什麼都沒存,畫面卻像成功了(藍字=假綠)。
產物層鐵證:bundle tier2/ui grep 'portal/admin/ai'=1、tier2/cypher=0;兩實例實打皆 404。
反證(別改不壞的東西):走正確端點寫入 → 完整重裝 24/24 → credential 仍在、
rag_chat verdict failed→success ⇒ **重裝不洗 credential,資料層本來就是保留式的**。

① cypher 補 GET|POST /portal/admin/ai(routes/portal.ts)
   - POST 內部轉呼 credentials.ts 既有 storeCredential()=**唯一寫入路徑**,不另造第二套
   - GET 只回 has_key 布林,**永不回傳 key 本身**(D36)
   - credential 的 api_key 欄=租戶 slug(portalTenant),與安裝器 seedCredential 寫
     kbdb_internal_token 用的 ns 同值 ⇒ 同租戶分區,彼此查得到
   - use_claude_for_extract 存 KV(單一布林偏好,不為它開 KBDB template)
   - claude_available 目前無訊號來源(該由小幫手回報)⇒ **誠實回 false 不假綠**
   - 寫入失敗回 502 帶原因——本 bug 的教訓就是「不能讓前端以為存好了」
   - 兩者皆未提供 → 400,避免「看起來成功但什麼都沒做」

② 前端(console-ui/public/portal/index.html)
   - 拔掉寫死 `|| "https://cypher.arcrun.dev"` fallback ⇒ 缺 config.js 就顯示紅色橫幅明顯報錯。
     **寧可明顯失敗,不要靜默把用戶金鑰送去中央實例。**
     ⚠️ 更正我先前的錯誤診斷:config.js **有**正確注入(實查 leo 實例指向自己的 cypher),
     跨租戶錯置未發生;這條是未爆彈不是現行災情。
   - leo 規格:已存 → 唯讀「已輸入」+「修改」鈕(不顯示 key);按修改才變回輸入框;
     未存 → 一般輸入框。舊版只改 placeholder=讀起來像臨時提示,是它像 UI bug 的原因。
   - 只有真的送了新 key 才切回「已輸入」(單純改 Claude 勾選不誤報)

tsc 綠;三個 script 區塊 node --check 全過。
卷:system-dev/docs/3-specs/journeys/gemini-key-lost-on-reinstall.md
2026-07-31 19:56:47 +08:00
uncle6me-web eefbed98b6 合流 batch:步驟 3/4/5/6 併成一版出貨(同一顆 cypher worker,bundle 是 worker 粒度)
鐵律(08-01 血淚):CP 可分步驗收,但 bundle 是 worker 粒度——凡改同一顆 worker 的多步,
出貨前必先合流成一版再重生 bundle,否則 stage 只拿到一步(三條分支各自建 bundle 的坑)。

base=feat/step3-missing-guidance(最前緣,已含 step4+step6+t158-t162 prod 修)
merge=feat/step5-conditional-edges(引擎條件邊+recipe 三層+教材世代修)
2026-07-31 16:56:09 +08:00
uncle6me-web a9a47b7c37 🔴 步驟5 補斷點:教材說「引擎不支援條件分支」+意圖語法收不到分支標籤
備考 haiku 真考時自查發現的兩個斷點——**若不補,考試必掛,且掛的是我自己的教材**。
形狀=步驟1 那個「世代脫節」的翻版:能力做好了,但教 AI 的地方還停在舊世代。

斷點①:三處教材主動說「沒有條件分支」,還教了正好造成腹語術的替代法
  - mcp/src/mcp-handler.ts:44「**沒有** ON_TRUE/ON_FALSE——引擎目前不支援條件分支」
  - registry/skills/write_intent_workflow.md:39「不要寫 ON_TRUE/ON_FALSE…
    需要判斷時**寫成一個獨立節點**再接 ON_SUCCESS」← 這正是「判斷退回 code」的入口
  - registry/skills/INDEX.md:44「已知的坑:引擎沒有條件分支」
  ⇒ 三處全部更新成現況(三顆零件都輸出 data.branch、引擎依標籤選路、
    查零件回應附 branch_hint 照著接即可),並保留「不要寫 ON_FAILURE」(那個真的沒有)。

斷點②(更隱蔽,靜默失效):`graph-builder` 只認得 `對每個 X` 的參數化 label,
  `ON_BRANCH(branch_active)` 帶括號會落到 toEdgeType 預設值 **PIPE**
  ⇒ 我在 skill 教的寫法,編圖收不到,而且**不報錯**——AI 以為分支了、實際全走同一條。
  「教了語法但引擎不收」比沒做更糟,故與文件同批補上:比照 FOREACH 抽 iterator 的作法
  抽 branch 標籤(半形/全形括號都收),寫進 edge.branch。

新增 tests/intent-branch-syntax.test.ts(7 項綠):守「文件教的寫法,編圖真的收得到」
——ON_TRUE/ON_FALSE 不退化成 PIPE、中文「成立時/否則」、ON_BRANCH(標籤) 抽得出 branch、
全形括號、try/catch 標籤;零變化:ON_SUCCESS 仍是 ON_SUCCESS、對每個 X 的 iterator 不受干擾。

全套 234 passed(前 227 +7),失敗數維持既有 9 筆;tsc 綠。

SDD: workflow-discovery 3.11|CP: arcrun-usable 步驟 5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 16:51:19 +08:00
uncle6me-web e688d805ea 步驟5 抽驗補證:三型分支用「真零件實跑輸出」端到端驗過(不再是推論)
總管 08-01 抽驗要求(正確):ON_CASE/ON_CATCH grep=0,我用 ON_BRANCH 通用承接,
但既有測試是用 Input 節點**手餵分支形狀**——那只證明「引擎依標籤選邊」,
**沒證明「真零件吐的標籤對得上」**。leo 特別點名 switch/try_catch,故補實測。

真零件實跑(wasmtime 跑 .component-builds/*.wasm,原文抄進測試當 given):
  if_control  active   → {"data":{"branch":"true","result":true},"success":true}
  if_control  inactive → {"data":{"branch":"false","result":false},"success":true}
  switch      case1    → {"data":{"branch":"branch_active"},"success":true}
  switch      case3    → {"data":{"branch":"branch_pending"},"success":true}
  switch      無匹配   → {"data":{"branch":"branch_default"},"success":true}
  try_catch   成功     → {"data":{"branch":"try","result":{"value":42}},"success":true}
  try_catch   失敗     → {"data":{"branch":"catch","error":"boom"},"success":true}
⇒ 三顆的標籤與 readBranch 讀的 data.branch **完全對得上**,非「理論上支援」。

新增 tests/branch-real-components.test.ts(8 項綠):把上列真輸出送進引擎,
驗 if 兩路/switch 多路(含**第 3 條 case** 證明第 N 路走對)+default/
try_catch ok 與 catch 兩路,每項都驗「該走的走、不該走的沒走」。

新增 tests/branch-hint-response.test.ts(7 項綠):驗逐顆查三顆的回應
自帶 branch_field/branches/edge_types/usage/example,並把 AI 實際看到的內容印出來
(總管要求「貼回應」)。另驗不分岔零件(http_request/code)無此欄=不加噪音。

全套 227 passed(前 212 +15),失敗數維持既有 9 筆未變;tsc --noEmit 綠。

SDD: workflow-discovery 3.11|CP: arcrun-usable 步驟 5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 16:43:25 +08:00
uncle6me-web c9feb15a37 步驟5 驗收與 SDD 落帳:判斷骨架重寫實測 1201→350 字元、if×8→0(降 71%)
tests/step5-acceptance.test.ts(3 項綠):
- 多路分流+失敗路三條各自到位、全程零 code 節點、success=true
- 布林兩路(if_control)同樣零 code
- 字元數對照:舊寫法(判斷全塞進一個 code 節點的 JS)1201 字元 if×8
  → 新寫法(分支邊+recipe response_map 宣告)350 字元 if×0

誠實標記(寫進測試檔頭與 tasks,不讓後人誤讀):線上那顆 assemble(5509 字元 if×23)
住在 arcrun-rag 實例、本 repo 無其定義 ⇒ 本次是把它的**判斷骨架**以新能力重建成等價
工作流,證明判斷不必寫在 JS 裡,**不是**直接改寫線上節點。真正改寫與 haiku 逐顆查場景
=stage 端到端(features/09)才算數,故 3.13 標 ◐ 不標 。

tasks.md:3.11/3.12 標 [x] 附證據,3.13 標 [◐] 附待辦界線。
全套 212 passed(基線 179 +33 新),失敗數維持既有 9 筆未變;tsc --noEmit 綠。

SDD: workflow-discovery 3.11/3.12/3.13|CP: arcrun-usable 步驟 5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 16:34:20 +08:00
uncle6me-web 5f5c0a89e2 步驟5 缺口②:recipe 補 payload/回應正規化/binding 三層(leo 三層模型的第③層)
問題:舊 recipe schema 只有 {canonical_id, endpoint, method, auth_service}(body 有但淺)
⇒ ①帶 body 的 API 只能繞過 recipe 把整包寫進 workflow code
  ②回應解析綁死單一供應商(rag_chat 的 finalize 2786 字元全在對付 Gemini 形狀)
  ③Cloudflare binding(env.AI/VECTORIZE/BROWSER/QUEUE)整類被「只認 HTTP+金鑰」的抽象排除
⇒ 換 LLM 供應商=改 workflow,而非換 recipe,違背「外部 API 只有一條一致的路」。

新增 lib/recipe-payload.ts(純函式,好測):
- renderBodyTemplate:遞迴插值,單一 {{x}} 保留原型別、混合文字拼字串、
  支援 dot path、取不到保留原樣(不靜默吞掉,看得見才好 debug)。
  語義刻意與 graph-executor 的 interpolateData 一致,不新造第二種插值行為。
- applyResponseMap:text_path 取值/thinking_model 剔除 thought=true 取最後一個非 thought/
  answer_marker 用 lastIndexOf(自檢清單內文也會提到標記)/strip_prefixes 循環剝殼
  (實撞三型「Draft: 【答】」「* 【答】」「Answer: * 【答】」,單趟剝不乾淨)。

RecipeDefinition 加四個**全選填**欄位:body_template/response_map/auth/binding_name。
- component-loader:body_template 優先於 body,兩者皆無才沿用 ctx 當 body(既有行為)
- response_map 有設才附 text 欄,未設原樣回傳 ⇒ 既有 recipe 行為完全不變
- 新增 makeBindingRecipeRunner+pickRecipeRunner:auth='binding' 走平台 binding(免金鑰、
  開機即可用),其餘一律走既有 HTTP 路徑。binding 缺綁定/無 run() 時回可操作錯誤,不假綠。
  這型不是為 Workers AI 開特例——一次打開 env.AI/VECTORIZE/BROWSER/QUEUE 整排。

payload 用法要「查得到」(同 branch_hint 動機,leo 08-01 的 n8n 式逐顆查):
buildPayloadHint() 讓 recipe 的查詢回應說得出「payload 怎麼填、回應怎麼取值、
認證誰負責」,wire 進 discover 混搜/legacy 逐顆/target=recipe 三條路徑。
金鑰鐵律 D36:hint 只說「走 auth recipe X,金鑰由系統注入、你不必也不該填」,不吐值。

測試:tests/recipe-payload-response.test.ts 14 項全綠(含三家形狀 Gemini/Claude/Workers AI
用不同 path 都取得出文字=換源=換 recipe 的實證;未設 response_map 原樣回傳的相容性)。
全套 209 passed(前 179 +30 新),失敗數維持既有 9 筆未變;tsc --noEmit 綠。

SDD: workflow-discovery task 3.12|CP: arcrun-usable 步驟 5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 16:31:58 +08:00
uncle6me-web 323ccc8475 步驟5 缺口①:引擎通用具名分支邊(ON_TRUE/ON_FALSE/ON_BRANCH)=Arcrun#5 根治
問題(「全變成 code」的根):if_control 回 {result, branch} 卻沒有邊讀得懂它,
圖的邊只有 ON_SUCCESS/IF/FOREACH ⇒ 就算照規矩用零件,仍得寫 code 判斷走哪條。
leo 08-01 追問「你改了 if,有改 switch 嗎?switch 更嚴重」——確認 switch(N 路)
與 try_catch(try/catch) 同病,故一次做成通用機制,不留「為 switch 再改一次」的債。

設計:三顆流程控制零件的 output_schema 本來就都收斂到同一形狀 data.branch: string
(if_control→true/false;switch→case 名或 default_branch;try_catch→try/catch)
⇒ 引擎只需「依標籤選邊」一個機制 ON_BRANCH;ON_TRUE/ON_FALSE 是布林路的語法糖,
底層同一條路(測試已證等價)。讀不出分支=不走(誠實,不亂挑一條)。

- types/schemas/constants:新增三邊型(純新增,既有列舉不動)+ GraphEdge.branch
- graph-executor:readBranch() 依 data.branch → branch → data.result → result 四層相容
- 中文語意詞:成立時/為真時=ON_TRUE,不成立時/為假時/否則=ON_FALSE,BRANCH=ON_BRANCH

分支用法要「查得到」(leo 08-01:AI 可能像 n8n 那樣逐顆查、自己組圖):
新增 lib/branch-hints.ts,讓 if_control/switch/try_catch 的查詢回應自帶 branch_hint
(branch_field/branches/edge_types/usage/example)——只看這一顆的回應就知道怎麼接下一步,
不必回頭讀 skill。四條回應路徑全wire:catalog found/legacy 逐顆/步驟4 substitution/
target=component 名字搜尋(=n8n 式那條)。不分岔的零件不加此欄,避免噪音。

測試(先寫測試再改引擎,紅線要求):tests/conditional-edges.test.ts 16 項全綠
——if 兩路/switch 多路+default/try_catch 成功與失敗路/語法糖等價/
context 傳遞/同分支 fan-out/無匹配不走/混合邊時 PIPE 不受影響/schema 放行。
零變化保證:195 passed(前 179 +16 新),失敗數維持既有 9 筆未變(console/portal
HTML 資產未建置+executor 斷言字串漂移,皆與本次無關);tsc --noEmit 綠。
現存 workflow/example 用到新邊型=0 筆(grep 實查)⇒ 既有行為不可能被改動。

SDD: workflow-discovery task 3.11|CP: arcrun-usable 步驟 5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 16:26:26 +08:00
uncle6me-web 9f777ff43e t161+t162 修 leo prod 實撞兩病:庫清單靜默空白/「需要更新」假警報迴圈
t162(假警報,實錘):daemon cloudVersionStale() 讀 cypher /health 的 bundle_version
比對 minCloudBuilt(2026-07-28),但 **/health 從來只回 {ok:true}、沒有這個欄位**
⇒ 恆讀到空字串 ⇒ 恆判 stale ⇒ 「知識庫需要更新」永遠不消失,重裝也沒用
(leo:「按下就跳到 install,但重新更新後並不會消失,這個訊息是幹嘛的?」)。
安裝器其實一直有注入 ARCRUN_BUNDLE_VERSION(worker.js:819),只是沒人吐出來。
修:/health 讀該 var 誠實回報(未注入則省略欄位=老實例,daemon 判 stale 是對的)。
新 bundle 注入值 2026-07-31+<commit7> ≥ 2026-07-28 ⇒ 重裝後警報自然消失。

t161(庫清單空白):portal 前端 guard401 → dropSession() **靜默**清 token 跳登入頁,
畫面不說任何原因 ⇒ 用戶看到的是「庫不見了」而非「請重新登入」(leo:「實際上根本
沒連上任何庫,清單空白」)。修:dropSession 帶原因寫進 #login-status——
「你的登入已過期,請重新登入(你的資料都還在,不會遺失)。」
資料側無恙(portal_library records=2 已驗),純讀側 session 過期+靜默 UX 缺陷。

驗:cypher tsc 全綠;vitest 9 failed/187 passed=基線不變。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 15:34:27 +08:00
uncle6me-web 341bcb13c8 t160 測試跟上規格:人工建庫 POST → 404(庫只從 daemon 同步來)
舊測試驗「POST 建庫 200+重複 409」=已刪端點的規格;vitest 回基線 9 failed/187 passed。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 14:57:22 +08:00
uncle6me-web 832f270f52 Merge branch 'feat/step6-success-rate' into feat/step3-missing-guidance 2026-07-31 14:54:25 +08:00
uncle6me-web 5c5109cd45 Merge branch 'feat/step4-node-substitution' into feat/step3-missing-guidance
# Conflicts:
#	system-dev/docs/3-specs/workflow-discovery/tasks.md
2026-07-31 14:54:25 +08:00
uncle6me-web e744ad1f96 t160 世代債清除(leo:「如果你會搞不清楚,就把錯的東西刪掉」)——repo 只留一個 UI 世代
事故鏈(07-31 深夜,leo 刷新撞到):t159 重打包 staging bundle 時,build-ui-bundle
吃了 feat/step3 分支的 console-ui/public/——該分支基底早於 main d7fab7c
「拿掉登記新庫」,public 停在舊世代 ⇒ youlin 實例 UI 被打回被淘汰的
「登記新庫」人工表單典範(07-27 mistakes 已記帳的 src/public 雙世代債,
這次由「分支副本停舊代」路徑引爆——債沒還,任何一個部署路徑都會引爆它)。

刪掉的(舊世代,git rm 實體刪除):
- console-ui/src/(console.ts/console-dashboard.ts/portal-ui.ts)=07-22 從
  cypher-executor 搬出的 renderer 快照,自 07-27 起與 public/ 真身脫鉤、只會
  重產舊 UI
- console-ui/scripts/build.mjs=從上述舊 renderer 重產 public 的 build 步驟
  (deploy.mjs 原自動跑它=引爆器本體)
- cypher POST /portal/admin/libraries(人工建庫端點)=死端點(e28e190 起 UI
  零呼叫;leo 規格「要直通 daemon,同步,沒有登記這回事」)。GET 列表/PATCH
  管理/t159 daemon 自動登記照舊

新唯一源:console-ui/public/=手改演進的真身(本 commit 同步到 main e28e190
世代+註解字樣改寫達成「登記新庫」全檔 0 命中);deploy.mjs 改為直接託管
public(無 build 步)+世代閘(缺「不需要人工新增」或含「登記新庫」拒部署)。

驗:tsc 全綠;deploy.mjs node --check 過;重打 ui bundle 242KB 過世代閘,
指紋「登記新庫」=0/「不需要人工新增」=1。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 14:36:48 +08:00
uncle6me-web 062757f1b1 t159 修 portal 庫目錄空:補上 daemon 一直在打、但從未存在的登記端點
診斷更正(重要):kbdb 裡 50+ 筆 entry_type=value(content=資料夾名)**不是孤兒**——
那是 91 筆 triplet records 的 library slot 正常構造(records 全部組裝完整,資料同步
一切正常,無失敗無重試)。真病灶=**POST /portal/daemon/libraries 這個 route
從來不存在**:daemon registerLibraries(arcrun-tray main.go:505,t52「用戶可以看到
我有 2 個庫」)連線精靈時打它 → 404 → 被 daemon「失敗不擋連線」設計靜默吞掉
→ portal_library 登記簿永遠 0 筆 → portal「庫目錄管理」空。

- 新 route 契約照 daemon 既有呼叫:{email, password, libraries:[{name, display_name}]}
- 帳密驗證沿用 /portal/session 同一套(isLocked/findUserRecordId/verifyPassword/
  recordLoginFail;daemon 只在精靈那刻拿帳密不存)
- 冪等:listRecordsByTemplate 比 name,已登記跳過(重跑精靈不堆重複)
- 庫名走 isValidLibraryName 同閘(拒 "*" 與非法字元)

驗:tsc 全綠;youlin 實例資料層已手動補登記(探針 record PATCH 成
youlinhsieh-test1+新建 test2,portal_library count=2);route 部署驗證
隨下批 staging bundle(假帳密應回 401 而非 404)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 14:07:05 +08:00
uncle6me-web 9c9aff0046 t158 P1 素材:workflow export/import 原語(definition 端點+CLI 命令,未接線)
leo 07-31:「你要做的就是一個叫 export,另一個是 import……現在如果我要把我做的
工作流分享給同事,我要怎麼 export?他要如何 import?是缺了功能用 search 來湊嗎?」

- cypher GET /webhooks/named/:name/definition:吐可攜定義(graph+config+description
  =record 原樣)——export 的引擎端;import 端直接 POST /webhooks/named 即送進任何實例
- cli/commands/workflow.ts:acr workflow export <name>(definition→.workflow.yaml,
  flow 從 graph.edges 反推供人讀)/import <file>(graph 直 POST,零編圖零 search
  零存在性驗證=V2 純複製;手寫 yaml 無 graph → 指去 acr push)
- ⚠️ 未接線:index.ts 尚未掛 workflow 命令組(P1 收尾:接線+分享場景 A export→
  B import 驗收+parser 共用模組化)

安裝器側 P0(rag-installer 5a6539d)已用同形狀(打包期預編 graph+純上傳)上 prod。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 13:49:23 +08:00
uncle6me-web d48f83ae6f t159 步驟4 意圖節點→真實零件/recipe 替換+/cypher/search 加 target 指定搜尋對象
CP arcrun-usable 步驟 4(目的:AI 只要填 payload——系統把「傳到 telegram」
翻成 http_request+recipe telegram_send)+leo 07-31 追加:
「難道我不能指定要搜尋工作流或節點或 recipe 嗎?」

替換(只動 discover,t158「部署≠發現」邊界不碰):
- 兩庫 exact 落空後,在 t158 一次抓好的清單記憶體內媒合,零新增 round-trip
- 規則 A 服務詞→recipe:名字全部服務詞命中同一 recipe 且唯一才換
  (google_slides_create 不被 google_sheets 誤吃)
- 規則 B 強欄位斷詞→零件:canonical/display/aliases 強命中×10+弱命中,
  需至少一強命中且分數唯一最高(aes_encrypt 無強命中不換)
- 換到=status resolved+substitution{from,componentId,recipe,reason},
  cypher 圖節點直接帶真實 componentId;換不到照舊 not_found+3.7 指路

target 參數(各走既有機制,不新造第二套搜尋):
- triplets+target=component|recipe=只查該庫
- query+target=名字搜尋:component→registry /components/search(=MCP
  arcrun_search_components 同路);recipe→私庫 RECIPES KV(回應註明公庫走
  arcrun_recipe_search);workflow→新抽 lib/workflow-search.ts,
  GET /workflows/search 與 target=workflow 共用(=arcrun_search_workflows 同路)
- 防呆:compile+target 400/target=workflow 吃 query 不吃 triplets/非法 target 400

驗(本地 wrangler dev,registry 種 20 合約+init/seed 10 recipe):
- 「判斷有沒有新資料 >> ON_SUCCESS >> 傳到 telegram」→ if_control(resolved)
  +telegram_send(substitution.componentId=http_request)=feature 06 驗法過
- 機械考 27/27 全綠(01 組×5+03 組×4 迴歸+06 組×8+target×8+compile 迴歸×2)
- 冷啟第一發 94ms、熱 8–13ms(t158 病史對照:舊 25.7s);compile 39ms unchecked 照舊
- tsc 全綠;vitest 9 failed/179 passed=t158 基線完全相同

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 13:49:09 +08:00
uncle6me-web ea1c0571c1 步驟6 執行統計回寫:每顆零件跑完回寫 success_rate(task 29,design.md「執行統計設計」)
- registry 新增 POST /analytics/record:ANALYTICS_KV 計數器(stats:{hash_id}:{version})
  為唯一真相源,衍生值(success_rate/avg_duration_ms/call_count)回填 comp: 記錄
  ——查詢讀取端讀哪就寫哪,不開第二真相源。KV 無 CAS,誠實標註非原子。
- cypher-executor execution-evaluator 從 stub 改真實作:執行收尾(/cypher/execute
  成功與 ExecutionError 路徑+webhook 路徑)對 trace 裡每顆 Component 節點
  fire-and-forget 回寫,waitUntil 包、不增加執行同步延遲(仿 recordRecipeStats 慣例)。
  成敗判定=trace error 或 output.success===false(makeHttpRunner 非 2xx 不 throw)。
- registry 位置沿 search-nodes 慣例:REGISTRY_BASE_URL 覆蓋,未設走 wasmWorkerUrl。
- 新增單測 13 個全綠;本地雙 wrangler dev 端到端實測:http_request 跑 5 次
  (3 成功+2 失敗)→ success_rate 1→0.6、call_count 0→5,/cypher/search 同步可見。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 13:45:05 +08:00